[教學]一起來開發遊戲吧(二) - 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,而是親手輸入會一行程序。
這次較長的更新期,就當成是一個消化期吧!
留言