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


接續上一章,本章會依照流程圖把 Main Controller 和 Character Controller 的大部份完成。
另外也會進行 Player 的發射動作,並把最基本的 Pool System 完成。

本章的篇幅比上兩章要多,因為不希望把 Pool System 分兩次完成。
不過本章的 C# 教學非常少,但牽涉到比較多 C# 的程序應用。
如果上兩章的 C# 還沒有搞懂,建議先試著做些改動當成實驗,讓自己更了解 C# 的語法和在 Unity 上的基本應用。

如果沒有看過之前的文章,請從頭開始。

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

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




成功者和失敗者最大分別是什麼?
分別多的是,但其中佔很大比重的是浪費程度,成功者不會浪費時間在無益的事情上。


<Unity 安全性更新>

請把 Unity 更新到 2018.3.7f1 版本,本教學的 Project 也會更新至這個版本。
<官方安全性說明>


<C# 基本教學>

本章的基本教學很少,只是一些架構上的說明。
大部份的東西都在上兩章說明過,也以實例作出應用示範,接下來的深入應用會在 Project 製作上進行說明和應用。


<系統底層程序>

Monobehaviour 是 Unity Engine 提供的底層構架,讓繼承的 Class 成為一個 Component。
而不繼承 Monobehaviour 的東西,都會變成系統(開發環境)的底層程序。



<跟 MainController 連接>

以上一章流程的方法,來把 CharacterController 跟 MainController 連接使用。

除了 Pause 以外,MainController 都依照流程設計一樣完成了。


<角色 - 順滑移動和轉向>

在上一章,角色的 position 都是直接等於鼠標的 position,也就是角色的移動速度等於鼠標的移動速度,這不是一個遊戲應有的表現。

現在角色會依設定的速度來移動和轉向了。


<更好的控制>

由於遊戲設計是以滑鼠來控制角色的移動,只要鼠標移到遊戲畫面以外,角色也會移動到遊戲畫面以外,這也是不正常的遊戲現象。
這裡用一個最常用和最簡單的方法 - 假鼠標。

把假鼠標完成後,角色的移動就沒有問題了。


<角色移動上的臭蟲>

Bug 臭蟲,除了是程序上的錯誤外,還包括即使程序沒問題,但構成上會引至問題的地方。
這次是運作上因真假鼠標的 position 不同而引起的操作誤導。

現在操作上沒有誤導成分,角色和鼠標的控制會更合適。


<製造子彈>

這是一款射擊遊戲,在發射子彈之前,當然要先製造子彈。
物件自己做好自己的事情,這是 Unity 使用 MonoBehaviour 下最好的配置,所以把子彈會做的事情都做出來,讓子彈可以自生自滅。

這樣,子彈就會自己移動,自己決定何時消滅自己了。


<子彈生成器>

Spawn 是產卵的意思,在遊戲世界就是生成器。
在系統中,並不會重複製作同樣的物件,同樣的物件會儲存成 Prefab,有需要時再 Instantiate 生成出來,而負責生成的地方就是 Spawn。

不費吹灰之力,就可以讓角色發射子彈了。


<Pool 物件池系統>

遊戲程序只會在一個場景讀取時,向系統索取必要的資源來使用,也就是程序並沒有資源分給子彈!
在 Instantiate 時,程序會再向系統索取一次資源,再次為場景進行 Mesh 優化等工序,在 Destroy 時會歸還資源給系統,再一次為場景進行優化工序,效能成本很高。
Pool 系統能在場景生成時,把一定數量的物件先生成出來並存放起來,這樣程序就會有資源留給一堆子彈,在程序中就不用 Instantiate 和 Destroy,場景也不用為此而進行優化工序。

一個易借易還的 Pool 基本完成了。


<優化>

優化是非常重要的,程序即使沒有錯誤,但不代表是最好,有時運行上好像是最好,但對效能並不是最好。
本章加入的東西主要是子彈的自生自滅和 Pool 系統,就這兩部份來看看有沒有優化的地方吧。

本章到此為止。。。本來是這樣預定的,不過並不想把 Pool System 分成兩次說明,下次的更新可能會更花時間,也正好讓新手們有更多時間去消化 C# 的部份,所以追加了 Pool System 完成部份。


<更好的 Pool>

一個好的系統,能夠在有需要時作出適當的對應,並對開發者或使用者提出適當的建議,來避免重滔覆轍。
一個好的 Pool 系統,可以應付物件池被清空的情況,也會為設定過大而作出建議。

一個最基本的易借易還 Pool System 完成了。


<測試影片>



<本次的程式碼>

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

public enum ActionType
{
    Fire, Pause,
}

[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<GameObject, bool> 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<GameObject, bool>();
    }

    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;
    }
}

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

public class MainController : MonoBehaviour
{
    [Header("Components")]
    public Transform pointer;

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

    private static Action<vector2> pointerAction;
    private static Dictionary<ActionType, Action> actionRegistry = 
        new Dictionary<ActionType, Action>();

    // local use
    private Camera cam;

    private void Awake()
    {
        Cursor.visible = !HideCursor;
    }

    private void Start()
    {
        cam = Camera.main;
    }

    private void Update()
    {
        CheckMousePointer();
        CheckMouseBotton();
    }

    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<vector2> act)
    {
        pointerAction = act;
    }

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

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;

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

    private void Awake()
    {
        RegisterActions();
    }

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

    private void Update()
    {
        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 = transform.position - destPosition;
        float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg + 90;
        transform.rotation = Quaternion.Lerp(
            transform.rotation, 
            Quaternion.AngleAxis(angle, Vector3.forward), 
            rotateSpeed * Time.deltaTime
        );
    }

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

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

        t.SetParent(null);
        t.SetPositionAndRotation(bulletSpawn.position, bulletSpawn.rotation);
    }
}

using UnityEngine;

public class PointerController : MonoBehaviour
{
    public RectTransform pointerRange;

    private void Update()
    {
        Move();
    }

    private void Move()
    {
        Vector3[] v = new Vector3[4];
        pointerRange.GetWorldCorners(v);

        Vector2 topLeft = v[0];
        Vector2 bottomRight = v[2];

        Vector3 newPos = Input.mousePosition;

        transform.position = new Vector2(
            newPos.x < topLeft.x ? topLeft.x : newPos.x > bottomRight.x ? bottomRight.x : newPos.x,
            newPos.y < topLeft.y ? topLeft.y : newPos.y > bottomRight.y ? bottomRight.y : newPos.y
        );
    }
}


using UnityEngine;

public class BulletController : MonoBehaviour
{
    [Header("settings")]
    public float moveSpeed = 5;
    public float lifeTime = 3f;

    // local use
    private float timer;

    private void OnEnable()
    {
        timer = 0;
    }

    private void Update()
    {
        Move();
    }

    private void Move()
    {
        timer += Time.deltaTime;

        if (timer < lifeTime)
            transform.position += transform.up.normalized * moveSpeed * Time.deltaTime;
        else
            KillMe();
    }

    private void KillMe()
    {
        ReturnToPool();
    }

    private void ReturnToPool()
    {
        ObjectPool.ReturnToPool(gameObject);
    }
}


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

public class CollisionController : MonoBehaviour
{
    [Header("Settings")]
    public LayerMask returnToPoolLayer;

    // local use
    private Collider2D col;
    private ContactFilter2D contactFilter = new ContactFilter2D();

    private void Start()
    {
        col = GetComponent<collider2d>();
        col.isTrigger = false;
        contactFilter.useLayerMask = true;
        contactFilter.layerMask = returnToPoolLayer;
    }

    private void FixedUpdate()
    {
        CheckCollision();
    }

    private void CheckCollision()
    {
        Collider2D[] colList = new Collider2D[10];
        int colliderCount = Physics2D.OverlapCollider(col, contactFilter, colList);

        if (colliderCount == 0) return;

        for (int i = 0; i < colliderCount; i++)
            ObjectPool.ReturnToPool(colList[i].gameObject);
    }
}


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

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

    [Header("Settings")]
    public ObjPoolSetting[] objPool;

    // local use
    private static Dictionary<string, ObjPoolInfo> poolInfo = 
        new Dictionary<string, ObjPoolInfo>();
    private static Dictionary<GameObject, string> poolObjList =
        new Dictionary<GameObject, string>();

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

        CreatePoolObject();
    }

    private void CreatePoolObject()
    {
        foreach (ObjPoolSetting ops in objPool)
        {
            GameObject pool = new GameObject(ops.name);
            pool.transform.SetParent(transform);
            poolInfo.Add(
                ops.name,
                new ObjPoolInfo(pool.transform, ops.prefab, ops.enableInPool)
            );
            for (int i = 0; i < ops.Quantity; i++)
            {
                GameObject newObj = poolInfo[ops.name].AddNewObj();
                poolObjList.Add(newObj, ops.name);
            }
        }
    }

    private void OnApplicationQuit()
    {
        foreach(ObjPoolSetting ops in objPool)
        {
            string pool = ops.name;
            int maxUse = poolInfo[pool].maxOut;
            string recAmt = 
                poolInfo[pool].addMoreCounter > 0 || ops.Quantity - maxUse > 15 ? 
                (maxUse + 15).ToString() : "-";
            Debug.Log(string.Concat(
                "Pool [ ", pool, " ] max out value: ", maxUse, " (", recAmt, ")\n")
            );
        }
    }

    public static Transform TakeFromPool(string pool)
    {
        Transform t = poolInfo[pool].Take();
        if (poolInfo[pool].inObj < 10)
            instance.AddMore(pool);

        return t;
    }

    private void AddMore(string pool)
    {
        if (poolInfo[pool].corou == null)
            poolInfo[pool].corou = StartCoroutine(AddMoreProcess(pool));
    }

    private IEnumerator AddMoreProcess(string pool)
    {
        poolInfo[pool].addMoreCounter++;
        int addAmt = (int)(poolInfo[pool].totalObj * 0.2f);
        if (addAmt < 10) addAmt = 10;

        for (int i = 0; i < addAmt; i++)
        {
            GameObject newObj = poolInfo[pool].AddNewObj();
            poolObjList.Add(newObj, pool);
            yield return null;
        }

        poolInfo[pool].corou = null;
    }

    public static void ReturnToPool(GameObject obj)
    {
        poolInfo[poolObjList[obj]].Return(obj);
    }
}



<下載>

<PPT> <Unity Project>


<下章預告>

下載內容:

  • 相機移動
  • 敵人 AI 行為
  • 敵人生成
  • 玩家死亡
  • Game Manager - Game Over 控制
  • 篇幅應該和今次差不多吧。。。

<後話>

預計下一章的更新會是兩星期至一個月後,到這裡為止的 C# 內容,差不多已涵蓋了所有初級、中級以及一部份的高級應用。
如最初所說,在使用上的文法並沒有多大變化,只要多作出嘗試就會很快理解,重點是不要依賴去複製貼上本人的 code,而是親手輸入會一行程序。

這次較長的更新期,就當成是一個消化期吧!

留言

此網誌的熱門文章

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

[瀬戸のテーマ] HTC Desire 最強體驗(S)預告