[教學]一起來開發遊戲吧(三) - Scene Builder, Manager Loader, Enemy Behavior


本章的篇幅很多,內容包括了:

  • Scene Builder
  • Camera Controller (multi-target)
  • Don't Destroy Object & Manager Loader
  • Enemy Behavior Structure (Scriptable Object)
  • Move and Attack Behavior Making
  • Multi-type Enemy Spawn
如果沒有看過之前的文章,請從頭開始。

本教學的目的和對象:<前言
本教學的目錄:<目錄

為方便看大圖,如使用 Chrome 的話,請安裝 <Hover Zoom+>。



成功人士手上永遠不缺資源,因為他們不會浪費時間去證明什麼沒有用,而會去為眼前所有東西創造價值。
學習也是一樣,不是去證明那些東西沒有用,而是去為每項東西製造價值。


<Tags, Sorting Layers, Layers>

到上一章為止都沒有太多東西在畫面中,所以沒有說明過,但本章會生成非常多的東西在畫面上,所以先設定好這些東西吧。


<Background Builder 背景建立器>

本 Project 都會以 Scene Builder 的形式來創建場景內容,也就是整個 Scene 基本不會有任何實體存在,一切會在讀入場景後,由程序去生成出來。
在比較複雜的遊戲系統中,即時生成遊戲物件是很常見的事情,所以 Scene Builder 能有效練習這方面的技能。
所需圖片檔<下載>。


<Black Hole Builder 黑洞建立器>

玩家由黑洞生成,這裡會先生成黑洞,再由黑洞生成玩家。


<Game Manager 遊戲管理者>

由於是程序生成,就會出現執行次序的問題,例如要確保所有東西都生成好後才開始執行遊戲控制項目這一類的情況。
Game Manager 是遊戲系統的中心,除了獨一無二外,還可以幫助管理遊戲裡的大小異事。


<Bullet Destroyer 子彈銷毀器

由於本來的 CollisionController 中使用的 OverlapCollider 不知有什麼問題,所以轉回使用 Unity 的 Physics Engine 來做碰撞處理,但會把其效能成本降至最低。


<Camera Controller 相機控制器>

有不少人會在編寫控制器時出現不少問題,然後不斷複雜化,產生更多的問題,又要再加入更多的程序或限制功能來作解決。
這基本都是策劃能力不足所引致,但並不是錯誤,只要學習如何策劃就可以。

策劃最重要的是把東西完整和清楚地草擬出來,再簡單分類。
相機的運動離不開 Move 和 Zoom,只要知道兩個運動的條件,然後集合相關數據,就可以簡單完成。
如果有看過本人之前的一些開發影片分享,就會看到更複雜的相機運動,也只是 Move 和 Zoom 兩個動作而已。


<Game Over 事件>

讓角色去死吧。。。這不叫 Game Over 事件。
Game Over 是可以讓遊戲重新開始的事件,這裡先不會處理介面,只會在 console 上顯示 Game Over 訊息,以及按下 Space 重新開始。


<Don't Destroy Object 不被銷毀的物件>

所有遊戲引擎都會提供這樣的物件,因為遊戲會以一個一個場景 Scene 來讀入和建立所需的物件。
在一個場景完結時,場景中的所有物件都會被銷毀,但有些物件是希望被保留下來,可以保留其狀態下直接帶到下一個場景中使用,這時就需要把讓物件設成 Don't Destroy Object。

對還不太清楚明白程序特性的新人來說,Don't Destroy Object 和 static 是最大難題之一。
如果不太理解也沒關係,不用心急,這個 Project 現在只有一個 Scene,之後會出現多一些 Scene 後就自然會了解。


<Manager Loader>

上一章有說明 static 會被保留,但現在 GameOver 時重讀 Scene 時則出現問題了。
這是因為 Scene 被重讀,代表了 Scene 的所有物件先被銷毀後,再重新建立出來,但一些 static 的項目因為被指向了被銷毀的物件而無法正常執行。
利用 ManagerLoader 來為 Don't Destroy Object 初始化,而不是 Don't Destroy Object 的獨一無二物件則不使用任何 static 除 instance 外,以此來解決問題。


<Optimization 優化>

自訂 Extension Method 可以讓編程變得更輕鬆方便。


<Behavior Structure 行為架構>

利用繼承的方法,可以很有系統地編寫不同種類的敵人,這是遊戲編程基本中的基本。
如果連繼承都不會,請先回去復習或重讀。
這裡會把繼承方法用在 Scriptable Object 上,實現多重行為架構。

再說一次,策劃是很重要的,先清楚地草擬出來,一切都會變得簡單。


<Enemy Move Behavior 敵行移動行為>

先來做移動行為吧。


<Enemy Attack Behavior 敵行攻擊行為>

攻擊行為比移動行為要單純得多,也沒有復合執行的必要。


<White Hole Builder>

完成敵人的行為,就到製造生成敵人的白洞了。
白洞的生成方法和上面測試敵人行為的生成法一樣,就是生成時間間隔不同而已。


<Optimization 優化>

最後的優化作業。


<測試影片>




<本次的程式碼>


using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum ActionType
{
    Fire, Pause,
}

public enum EnemyTarget
{
    BlackHole,
    Player,
    WhiteHole,
}

public enum AttackTarget
{
    Forward,
    Player
}

[Serializable]
public struct EnemySpaw
{
    public string poolName;
    public int interval;
}

[Serializable]
public struct ObjPoolSetting
{
    public string name;
    public GameObject prefab;
    [Range(20, 1000)]
    public int Quantity;
    public bool enableInPool;
}

public class ObjPoolInfo
{
    private Transform pool;
    private GameObject prefab;
    private readonly bool enableInPool;
    public int totalObj;
    public int outObj;
    public int inObj;
    public int maxOut;
    public int addMoreCounter;
    private Dictionary objList;
    public Coroutine corou;

    public ObjPoolInfo(Transform pool, GameObject prefab, bool enableInPool)
    {
        this.pool = pool;
        this.prefab = prefab;
        this.enableInPool = enableInPool;
        totalObj = outObj = inObj = maxOut = addMoreCounter = 0;
        objList = new Dictionary();
    }

    public GameObject AddNewObj()
    {
        GameObject newObj = GameObject.Instantiate(prefab, pool.transform);
        newObj.SetActive(enableInPool);
        newObj.transform.position = pool.transform.position;
        newObj.name = string.Concat(prefab.name, " (", totalObj + 1, ")");
        objList.Add(newObj, false);
        totalObj++;
        inObj++;
        return newObj;
    }

    public Transform Take()
    {
        if (objList == null || objList.Count == 0 || inObj == 0) return null;
        Transform t = null;
        foreach (GameObject obj in objList.Keys)
            if (!objList[obj])
            {
                outObj++;
                inObj--;
                objList[obj] = true;
                obj.SetActive(true);
                t = obj.transform;
                break;
            }

        if (outObj > maxOut)
            maxOut = outObj;

        return t;
    }

    public void Return(GameObject obj)
    {
        if (!objList.ContainsKey(obj)) return;
        outObj--;
        inObj++;
        obj.SetActive(enableInPool);
        obj.transform.SetParent(pool);
        obj.transform.SetPositionAndRotation(pool.position, pool.rotation);
        objList[obj] = false;
    }
}

[Serializable]
public struct MoveBehavior
{
    public EnemyMoveBehavior behaviors;
    public float weight;
}

using UnityEngine;

public static class ExtensionMethods
{
    public static Vector3 x(this Vector3 v, float x)
    {
        return new Vector3(x, v.y, v.z);
    }

    public static Vector3 x(this Vector3 v, float y, float z)
    {
        return new Vector3(v.x, y, z);
    }

    public static Vector3 y(this Vector3 v, float y)
    {
        return new Vector3(v.x, y, v.z);
    }

    public static Vector3 y(this Vector3 v, float x, float z)
    {
        return new Vector3(x, v.y, z);
    }

    public static Vector3 z(this Vector3 v, float z)
    {
        return new Vector3(v.x, v.y, z);
    }

    public static Vector3 z(this Vector3 v, float x, float y)
    {
        return new Vector3(x, y, v.z);
    }

    public static float Remap(this float value, float from1, float to1, float from2, float to2)
    {
        if (((to1 - from1) * (to2 - from2)) == 0)
            return 0;

        return (value - from1) / (to1 - from1) * (to2 - from2) + from2;
    }
}

using UnityEngine;

public class ManagerLoader : MonoBehaviour
{
    [Header("Settings")]
    public GameObject gameManager;

    // local use
    private GameManager gm;
    private MainController mc;
    private GameObject i;

    private void Awake()
    {
        if (GameManager.Instance == null)
        {
            i = Instantiate(gameManager);
            i.name = gameManager.name;
            i.SetActive(true);
        }

        InitManager();
    }

    private void InitManager()
    {
        gm = FindObjectOfType();
        mc = FindObjectOfType();

        gm.Init();
        mc.Init();
    }

    private void Start()
    {
        Destroy(gameObject);
    }
}

using UnityEngine;

public class BackgroundBuilder : MonoBehaviour
{
    [Header("Settings")]
    public Sprite background;
    public Vector2 gridSize;

    private void Start()
    {
        if (background == null || gridSize == Vector2.zero) return;

        SetBackground();

        GameManager.bgReady = true;
    }

    private void SetBackground()
    {
        Transform bg = new GameObject("BackGround").transform;
        bg.gameObject.isStatic = true;
        bg.position = Vector2.zero;
        SpriteRenderer sr = bg.gameObject.AddComponent();
        sr.sprite = background;
        sr.drawMode = SpriteDrawMode.Tiled;
        sr.size = gridSize;
        sr.sortingLayerName = "Background";
    }
}

using UnityEngine;

public class BlackHoleBuilder : MonoBehaviour
{
    [Header("Settings")]
    public Transform blackHole;
    public Vector3 position;

    private void Start()
    {
        GameObject obj = Instantiate(blackHole.gameObject, position, blackHole.rotation);
        obj.isStatic = true;

        GameManager.bhReady = true;
    }
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WhiteHoleBuilder : MonoBehaviour
{
    [Header("Spawn Settings")]
    public string poolName;
    public Vector3 centerPosition = Vector3.zero;
    [Range(5, 60)]
    public int interval = 15;

    // local use
    private List<Transform> whiteHoles = new List<Transform>();

    private void Start()
    {
        StartCoroutine(SpawnWhiteHole());
    }

    private IEnumerator SpawnWhiteHole()
    {
        while (!GameManager.SceneReady) yield return null;

        while (enabled)
        {
            Vector2 spawnPoint = Vector2.zero;
            float spawnRadius = CameraController.Instance.GetSize().y;
            bool pointOK = false;

            while (!pointOK)
            {
                spawnPoint = Random.insideUnitCircle * spawnRadius;
                pointOK = CheckWhiteHoleNeigbour(spawnPoint);
            }

            Transform whiteHole = ObjectPool.Instance.TakeFromPool(poolName);
            whiteHole.SetParent(SceneBuilder.Instance.WhiteHoleParent);
            whiteHole.position = spawnPoint;
            whiteHoles.Add(whiteHole);

            yield return new WaitForSeconds(interval);
        }
    }

    private bool CheckWhiteHoleNeigbour(Vector2 spawnPoint)
    {
        if (Vector2.Distance(spawnPoint, centerPosition) < CameraController.Instance.GetSize().y * 0.6f)
            return false;

        foreach (Transform t in whiteHoles)
        {
            if (Vector2.Distance(spawnPoint, t.position) < 2f)
                return false;
        }

        return true;
    }
}

using System.Collections.Generic;
using UnityEngine;

public class SceneBuilder : MonoBehaviour
{
    private static SceneBuilder instance = null;
    public static SceneBuilder Instance { get { return instance; } }

    // local use
    private Transform whiteHoleParent, enemyBulletParent, myBulletParent, enemyParent;
    public Transform WhiteHoleParent { get { return whiteHoleParent; } }
    public Transform EnemyBulletParent { get { return enemyBulletParent; } }
    public Transform MyBulletParent { get { return myBulletParent; } }
    public Transform EnemyParent { get { return enemyParent; } }

    private void Awake()
    {
        instance = this;

        CreateParents();
        GameManager.sceneManagerReady = true;
    }

    private void CreateParents()
    {
        List<Transform> parents = new List<Transform>();
        whiteHoleParent = new GameObject("WhiteHole").transform;
        parents.Add(whiteHoleParent);
        enemyBulletParent = new GameObject("Enemy Bullet").transform;
        parents.Add(enemyBulletParent);
        myBulletParent = new GameObject("My Bullet").transform;
        parents.Add(myBulletParent);
        enemyParent = new GameObject("Enemy").transform;
        parents.Add(enemyParent);

        foreach (Transform t in parents)
        {
            t.position = Vector3.zero;
            t.SetParent(null);
        }
    }
}

using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    private static GameManager instance = null;
    public static GameManager Instance { get { return instance; } }

    public static bool sceneManagerReady, poolReady, bgReady, bhReady, playerReady;
    public static bool SceneReady
    {
        get { return sceneManagerReady && poolReady && bgReady && bhReady && playerReady; }
    }
    public static bool gameOver;

    public static Transform blackHole;
    public static Transform player;
    public static float blackHoleRadius;

    public static Dictionary<Transform, EnemyAgent> AgentList =
        new Dictionary<Transform, EnemyAgent>();

    private void Awake()
    {
        if (instance == null)
            instance = this;
        else if (instance != this)
            Destroy(gameObject);

        DontDestroyOnLoad(gameObject);
    }

    public void Init()
    {
        poolReady = bgReady = bhReady = playerReady = false;
        gameOver = false;
        AgentList.Clear();
    }

    public static void GameOver()
    {
        gameOver = true;
        Debug.Log("Game Over.\n   Press [ SPACE ] to replay ...");
    }

    public static float CalculateBlackHoleForce(Transform agent)
    {
        float distanceToBlackHoleEdge = 
            Vector2.Distance(agent.position, blackHole.position) - blackHoleRadius;
        return Mathf.Clamp(5f - distanceToBlackHoleEdge, 0f, 5f).Remap(0f, 5f, 1f, 5f);
    }
}

using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
    public static void Reload()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
}

using UnityEngine;

public class BulletDestroyer : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D col)
    {
        if (col.CompareTag("Bullet"))
            KillBullet(col.gameObject);
    }

    void KillBullet(GameObject bullet)
    {
        ObjectPool.Instance.ReturnToPool(bullet);
    }
}

using UnityEngine;

public class CameraController : MonoBehaviour
{
    private static CameraController instance = null;
    public static CameraController Instance
    {
        get { return instance; }
    }

    [Header("Settings")]
    [Range(5, 10)]
    public int minZoom = 5;

    [HideInInspector]
    public Transform player, blackHole;

    // local use
    private Camera cam;

    private void Awake()
    {
        instance = this;
        player = null;
        blackHole = null;
        cam = GetComponent();
    }

    private void Update()
    {
        if (!GameManager.SceneReady) return;

        Move();
        Zoom();
    }

    private void Move()
    {
        transform.position = GetCenterPoint();
    }

    private void Zoom()
    {

    }

    private Vector3 GetCenterPoint()
    {
        var bounds = new Bounds(player.position, Vector3.zero);
        bounds.Encapsulate(player.position);
        bounds.Encapsulate(blackHole.position);
        return bounds.center.z(-10);
    }

    public Vector2 GetSize()
    {
        float height = 2f * cam.orthographicSize;
        float width = height * cam.aspect;

        return new Vector2(width, height);
    }
}

using UnityEngine;

public class CharacterController : MonoBehaviour
{
    [Header("Settings")]
    [Range(1, 20f)]
    public int moveSpeed = 2;
    [Range(1, 20f)]
    public int rotateSpeed = 10;
    [Range(0, 1f)]
    public float pointerRadius = 0.5f;

    [Header("Bullet Spawn Settings")]
    public Transform bulletSpawn;
    public string bulletPool;

    // local use
    private Vector3 destPosition = Vector3.zero;
    private Vector3 oldVelocity = Vector3.zero;

    private void Awake()
    {
        RegisterActions();
        CameraController.Instance.player = transform;
        GameManager.player = transform;
        GameManager.playerReady = true;
    }

    private void RegisterActions()
    {
        MainController.RegisterPointerAction(SetDestination);
        MainController.RegisterActionAction(ActionType.Fire, Fire);
    }

    private void OnTriggerEnter2D(Collider2D col)
    {
        if (GameManager.gameOver) return;

        GameManager.GameOver();
    }

    private void Update()
    {
        if (GameManager.gameOver) return;

        Move();
        Rotate();
    }

    private void Move()
    {
        Vector3 velocity = transform.up.normalized * moveSpeed * Time.deltaTime;
        if (Vector3.Distance(velocity, Vector3.zero) > Vector3.Distance(transform.position, destPosition))
            velocity = (destPosition - transform.position) * 0.90f;

        float distance = Vector3.Distance(transform.position + velocity, destPosition);

        if (distance > pointerRadius)
            transform.position += velocity;
    }

    private void Rotate()
    {
        Vector3 dir = destPosition - transform.position;
        transform.rotation = Quaternion.Lerp(
            transform.rotation, 
            Quaternion.LookRotation(transform.forward, dir), 
            rotateSpeed * Time.deltaTime
        );
    }

    private void SetDestination(Vector2 dest)
    {
        destPosition = dest;
    }

    private void Fire()
    {
        //Instantiate(bullet, bulletSpawn.position, bulletSpawn.rotation);
        Transform t = ObjectPool.Instance.TakeFromPool(bulletPool);
        if (t == null) return;

        t.SetParent(SceneBuilder.Instance.MyBulletParent);
        t.SetPositionAndRotation(bulletSpawn.position, bulletSpawn.rotation);
    }
}

using System;
using System.Collections.Generic;
using UnityEngine;

public class MainController : MonoBehaviour
{
    private static MainController instance = null;
    public static MainController Instance
    {
        get { return instance; }
    }

    [Header("Components")]
    public Transform pointer;

    [Header("Settings")]
    public bool HideCursor = true;

    private static Action pointerAction;
    private static Dictionary actionRegistry = 
        new Dictionary();

    // local use
    private Camera cam;

    private void Awake()
    {
        if (instance == null)
            instance = this;
    }

    public void Init()
    {
        Cursor.visible = !instance.HideCursor;
        instance.cam = Camera.main;
        pointerAction = null;
        actionRegistry.Clear();
    }

    private void Update()
    {
        if (!GameManager.SceneReady) return;

        if (GameManager.gameOver)
        {
            CheckReplayKey();
        }
        else
        {
            CheckMousePointer();
            CheckMouseBotton();
        }
    }

    private void CheckReplayKey()
    {
        if (Input.GetKey(KeyCode.Space))
            SceneLoader.Reload();
    }

    private void CheckMousePointer()
    {
        Vector2 pointerPosition = 
            cam.ScreenToWorldPoint(pointer.position);
        OnMousePointerMove(pointerPosition);
    }

    private void CheckMouseBotton()
    {
        if (Input.GetMouseButtonDown(0))
            OnMouseButtonDown(ActionType.Fire);

        if (Input.GetMouseButtonDown(1))
            OnMouseButtonDown(ActionType.Pause);
    }

    private void OnMousePointerMove(Vector2 position)
    {
        pointerAction(position);
    }

    private void OnMouseButtonDown(ActionType type)
    {
        if (!actionRegistry.ContainsKey(type)) return;

        actionRegistry[type]();
    }

    // Register Method

    public static void RegisterPointerAction(Action act)
    {
        pointerAction = act;
    }

    public static void RegisterActionAction(ActionType type, Action act)
    {
        if (actionRegistry.ContainsKey(type))
            actionRegistry[type] = act;
        else
            actionRegistry.Add(type, act);
    }
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WhiteHoleController : MonoBehaviour
{
    [Header("Settings")]
    [Range(1f, 10f)]
    public float neighborRadius = 1.5f;
    [Range(0f, 1f)]
    public float avoidanceRadiusMultipier = 0.5f;
    public LayerMask enemyLayer;

    [Header("Spawn Settings")]
    public EnemySpaw[] enemySpaw;

    // local use
    private List<EnemyAgent> agents = new List<EnemyAgent>();

    private float sqNeighborRadius;
    private float sqAvoidanceRadius;
    public float SqAvoidanceRadius { get { return sqAvoidanceRadius; } }

    private void Start()
    {
        sqNeighborRadius = neighborRadius * neighborRadius;
        sqAvoidanceRadius = sqNeighborRadius * avoidanceRadiusMultipier * avoidanceRadiusMultipier;

        foreach(EnemySpaw es in enemySpaw)
            StartCoroutine(SpawnAgents(es.poolName, es.interval));
    }

    private IEnumerator SpawnAgents(string poolName, int interval)
    {
        while (!GameManager.SceneReady) yield return null;

        while (enabled)
        {
            yield return new WaitForSeconds(interval);

            Transform agentObj = ObjectPool.Instance.TakeFromPool(poolName);
            EnemyAgent newAgent = GameManager.AgentList[agentObj];
            agentObj.SetParent(SceneBuilder.Instance.EnemyParent);
            agentObj.SetPositionAndRotation(transform.position, Quaternion.Euler(Vector3.forward * Random.Range(0f, 360f)));
            newAgent.controller = this;
            agents.Add(newAgent);
        }
    }

    public void KillAgent(EnemyAgent agent)
    {
        agents.Remove(agent);
    }

    public List<Transform> GetNearbyAgents(EnemyAgent agent)
    {
        List<Transform> colList = new List<Transform>();
        Collider2D[] cols = Physics2D.OverlapCircleAll(agent.transform.position, neighborRadius, enemyLayer);

        foreach(Collider2D col in cols)
            if (col != agent.AgentCollider)
                colList.Add(col.transform);

        return colList;
    }
}

using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Collider2D))]
public class EnemyAgent : MonoBehaviour
{
    [Header("Settings")]
    public FieldOfView fov;

    [Header("Movement Settings")]
    public EnemyMoveBehavior moveBehavior;
    [Range(0.1f, 20f)]
    public float rotateSpeed = 0.5f;
    [Range(1f, 100f)]
    public float moveSpeed = 1.0f;
    [Range(0.1f, 1f)]
    public float amp = 0.25f;

    [Header("Attack Settings")]
    public EnemyAttackBehavior attackBehavior;
    public Transform bulletSpawn;
    public string bulletPool;
    [Range(0.02f, 5.0f)]
    public float interval = 0.15f;

    //local use
    private float adjMoveSpeed, adjRotateSpeed;
    private float attackTimer;
    private float additionalForce;
    private Collider2D agentCollider;
    public Collider2D AgentCollider { get { return agentCollider; } }
    [HideInInspector]
    public WhiteHoleController controller;

    private void OnEnable()
    {
        attackTimer = 0;
    }

    private void Awake()
    {
        if (!fov)
            fov = GetComponent<FieldOfView>();
        GameManager.AgentList.Add(transform, this);
    }

    private void Start()
    {
        agentCollider = GetComponent<Collider2D>();
        adjMoveSpeed = moveSpeed * Random.Range(1, 1 + amp);
        adjRotateSpeed = rotateSpeed * Random.Range(1, 1 + amp);
    }

    private void Update()
    {
        if (!GameManager.SceneReady) return;

        if (moveBehavior)
        {
            List neighborList = controller.GetNearbyAgents(this);
            Move(moveBehavior.CalculateMove(this, neighborList, controller));
        }

        if (attackBehavior)
            Attack(attackBehavior.CalculateAttack(this));
    }

    private void OnTriggerEnter2D(Collider2D col)
    {
        if (!col.CompareTag("Player"))
        {
            controller.KillAgent(this);
            ObjectPool.Instance.ReturnToPool(gameObject);
        }
    }

    private void Move(Vector2 velocity)
    {
        additionalForce = GameManager.CalculateBlackHoleForce(transform);
        float power = Vector2.Distance(velocity, Vector2.one);

        transform.up = Vector3.Lerp(transform.up, velocity, adjRotateSpeed * power * additionalForce * Time.deltaTime);
        transform.position += transform.up * adjMoveSpeed * additionalForce * Time.deltaTime;
    }

    private void Attack(Vector2 velocity)
    {
        if (velocity == Vector2.zero)
        {
            attackTimer = 0;
            return;
        }

        attackTimer += Time.deltaTime;

        if (attackTimer > interval)
        {
            Transform t = ObjectPool.Instance.TakeFromPool(bulletPool);
            if (t == null) return;

            t.SetParent(SceneBuilder.Instance.EnemyBulletParent);
            t.position = bulletSpawn.position;
            t.up = velocity;

            attackTimer = 0;
        }
    }
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FieldOfView : MonoBehaviour
{
    [Header("Settings")]
    public float frequency = 0.2f;

    [Header("Field of View")]
    public float viewRadius = 5f;
    [Range(0, 360)]
    public float viewAngle = 160f;
    public LayerMask viewLayer;
    public LayerMask obstacleLayer;

    // public use
    [HideInInspector]
    public List<Transform> visibleTargets = new List<Transform>();
    [HideInInspector]
    public Transform nearestTarget, farthestTarget;
    [HideInInspector]
    public float nearestDist, farthestDist;

    // local use
    private Coroutine corou;

    private void OnEnable()
    {
        if (corou == null)
            corou = StartCoroutine("FindTargetsWithDelay", frequency);
    }

    private void OnDisable()
    {
        if (corou != null)
            StopCoroutine(corou);
        corou = null;
    }

    private IEnumerator FindTargetsWithDelay(float delay)
    {
        while (!GameManager.SceneReady)
            yield return null;

        while (true)
        {
            yield return new WaitForSeconds(delay);
            FindVisibleTargets();
        }
    }

    private void FindVisibleTargets()
    {
        visibleTargets.Clear();
        Collider2D[] targetsInViewRadius =
            Physics2D.OverlapCircleAll(transform.position, viewRadius, viewLayer);
        float nDist = viewRadius + 1f;
        float fDist = 0f;
        foreach (Collider2D col in targetsInViewRadius)
        {
            Transform target = col.transform;
            Vector3 dirToTarget = (target.position - transform.position).normalized;
            float angle = Vector2.Angle(dirToTarget, transform.up);
            if (angle < viewAngle * 0.5f)
            {
                float destToTarget = Vector2.Distance(transform.position, target.position);

                if (!Physics.Raycast(transform.position, dirToTarget, destToTarget, obstacleLayer))
                {
                    visibleTargets.Add(target);
                    if (destToTarget < nDist)
                    {
                        nDist = nearestDist = destToTarget;
                        nearestTarget = target;
                    }
                    if (destToTarget > fDist)
                    {
                        fDist = farthestDist = destToTarget;
                        farthestTarget = target;
                    }
                }
            }
        }

        if (visibleTargets.Count == 0)
        {
            nearestTarget = null;
            farthestTarget = null;
            nearestDist = 0;
            farthestDist = 0;
        }
    }
}

using System.Collections.Generic;
using UnityEngine;

public abstract class EnemyMoveBehavior : ScriptableObject
{
    public abstract Vector2 CalculateMove(EnemyAgent agent, List<Transform> neighborList, WhiteHoleController controller);
}

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Enemy Behavior/Toward Target")]
public class TowardTargetBehavior : EnemyMoveBehavior
{
    [Header("Settings")]
    public EnemyTarget target;
    [Range(0f, 5f)]
    public float powerToPlayer = 2.0f;

    public override Vector2 CalculateMove(EnemyAgent agent, List<Transform> neighborList, WhiteHoleController controller)
    {
        FieldOfView fov = agent.fov;
        Transform blackHole = GameManager.blackHole;
        Transform player = GameManager.player;
        Vector3 dir = Vector3.zero;
        float power = 1;

        switch (target)
        {
            case EnemyTarget.BlackHole:
                dir = blackHole.position - agent.transform.position;
                break;

            case EnemyTarget.Player:
                Vector3 target = fov.visibleTargets.Contains(player) ? player.position : blackHole.position;
                dir = target - agent.transform.position;
                power = powerToPlayer;
                break;

            default:
                break;
        }

        dir.Normalize();
        return dir * power;
    }
}

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Enemy Behavior/Avoidance")]
public class AvoidanceBehavior : EnemyMoveBehavior
{
    public override Vector2 CalculateMove(EnemyAgent agent, List<Transform> neighborList, WhiteHoleController controller)
    {
        if (neighborList.Count == 0)
            return Vector2.zero;

        Vector2 AvoidanceDir = Vector2.zero;
        int nAvoid = 0;
        foreach (Transform item in neighborList)
        {
            if (Vector2.SqrMagnitude(item.position - agent.transform.position) < controller.SqAvoidanceRadius)
            {
                nAvoid++;
                AvoidanceDir += (Vector2)(agent.transform.position - item.position);
            }
        }
        if (nAvoid > 0)
            AvoidanceDir /= nAvoid;

        return AvoidanceDir;
    }
}

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Enemy Behavior/Composite Move Behavior")]
public class CompositeMoveBehavior : EnemyMoveBehavior
{
    public MoveBehavior[] moveBehaviors;

    public override Vector2 CalculateMove(EnemyAgent agent, List<Transform> neighborList, WhiteHoleController controller)
    {
        if (moveBehaviors.Length == 0)
            return Vector2.zero;

        Vector2 velocity = Vector2.zero;
        for (int i = 0; i < moveBehaviors.Length; i++)
        {
            EnemyMoveBehavior behavior = moveBehaviors[i].behaviors;
            float weight = moveBehaviors[i].weight;
            Vector2 vel = behavior.CalculateMove(agent, neighborList, controller) * weight;
            if (vel != Vector2.zero)
            {
                if (vel.sqrMagnitude > weight * weight)
                {
                    vel.Normalize();
                    vel *= weight;
                }
                velocity += vel;
            }
        }

        return velocity;
    }
}

using UnityEngine;

public abstract class EnemyAttackBehavior : ScriptableObject
{
    public abstract Vector2 CalculateAttack(EnemyAgent agent);
}

using UnityEngine;

[CreateAssetMenu(menuName = "Enemy Behavior/Attack Target")]
public class AttackTargetBehavior : EnemyAttackBehavior
{
    public AttackTarget target;

    public override Vector2 CalculateAttack(EnemyAgent agent)
    {
        FieldOfView fov = agent.fov;
        Transform me = agent.transform;
        Transform player = GameManager.player;

        switch (target)
        {
            case AttackTarget.Player:
                return fov.nearestTarget == player ? player.position - me.position : Vector3.zero;

            default:
                return me.up;
        }
    }
}


<下載>

<PPT> <Unity Project>


<下章預告>

  • 黑洞成長
  • 角色成長
  • 白洞成長
  • Boss 事件
  • 看篇幅再追加其他內容

留言

此網誌的熱門文章

[教學]一起來開發遊戲吧 - Unity C# 基礎

QUMARION

[教學]一起來開發遊戲吧(二) - Character Controller, Pool System