蒼時弦也
蒼時弦也
資深軟體工程師
發表於

Unity3D - 元件化的遊戲製作

這篇文章一部份是寫給同學看的,在做遊戲中似乎挺常使用到的,而這個「特性」如果能夠理解的話,未來同學在製作遊戲應該也是會順利很多,另一方面就是 SITCON 的投稿,大概就是以這個為主軸來討論。

依照網路上的說法,這個概念應該是在 Unity3D 熱門之後,才比較被大眾所熟悉。而我個人是在 2013 年的 COSCUP 在半路大的演講第一次獲得「元件化」的關鍵字,這確實也是一個不一樣的概念。

至於後續如何呢?讓我們一起看下去。 (關於非遊戲的討論,我之後會另外寫文章,這邊以 Unity3D 的 Component 概念為主。)

元件化程式設計

為了要投稿 SITCON 所以我做了不少準備,至少是把 Component Based Programing 整個概念都稍微研究過了一遍。

首先,我們可以從維基百科上面看到一些關於 Component Based Programing 的介紹與相關知識。

最另外意外的是像是 Laravel (PHP Framework) 使用到的 IoC (單純是因為我以這個框架為契機了解 IoC 的概念)亦或是這幾年 Web Service 最常使用的 REST API 都屬於 Component Based Programing 的運用。

也就是說,若要討論其實是很多可以討論的。

這邊簡述一下我的理解。

「某一個物件因為經常需要擴充或者調整功能,或者是某一個物件類型因為需要應對不同的變化,所以從原本較為固定的方式衍生出一種相對彈性的方式。」

好像敘述的有點不太好,昨晚稍微努力想了些比較適當的「實例」也許大家會比較好理解。

  • WordPress 為了應對使用者不同的需求,提供了 Plugin 機制,讓使用者可以選擇對應的 Plugin 來使用。 > WordPress (物件) 可以透過 Plugin (元件) 來增減功能
  • 電腦可以透過安插各種零件來強化或者調整硬體應各種用途 > 電腦(物件)透過零件(元件)來調整硬體

不知道有沒有比較具象一點,大致上來說就是某個東西可以利用類似 Solt (插槽) 去擴充或者減少功能,然後調整成適當的狀態這樣的概念。 (從一些 Open Source 的遊戲引擎來看,其實有點像是 USB 插槽跟 USB 裝置的感覺,大多是實作一個 Interface 去讓物件繼承,從而相容這樣。)

Unity3D 的元件化設計

開始之前,我在 Google 找到了一篇關於遊戲的 Component Based Programing 的翻譯文章,大家可以先看看,個人感覺有點類似歷史簡介的東西,其實不管哪種做法都是有缺點(Ex. 關於 CPU 消耗的問題)不過在適當的情況使用,改善開發我想還是最主要的目標。

關於如何在 Unity3D 正確使用 Component 也有一份簡報非常不錯,裡面提到了正確拆解元件的方法以及概念。

在讀完這些資料後,其實大家應該都對 Component-Based Programing 這個概念有所了解。下面就開始來討論 Unity3D 的 Component 吧!

Unity3D 的 GameObject 與 Component

打開 Unity3D 產生一個 Cube (GameObject) 然後看看 Inspector 上的設定。

螢幕快照 2014-01-03 上午10.50.34.png

在 Unity3D 官網的 Component 解釋中,提到「所有可以附加在 GameObject 的基礎類別」

GameObject 上的 Component

那麼,我們試著對這個 Cube 加上一個 Script 並且嘗試用 GetComponents 來看看這個 Cube 上有哪些 Component 在上面。

 1#pragma strict
 2
 3var components:Component[];
 4
 5function Start () {
 6	components = GetComponents(Component);
 7	var i:int = 0;
 8	for(i = 0; i < components.Length; i++) {
 9		Debug.Log(components.GetValue(i));
10	}
11}
12
13function Update () {
14
15}

螢幕快照 2014-01-03 上午11.05.21.png

從結果來看,我們觀察到第一個: Inspector 上的都是 Component,一個 GameObject 的表現由 Component 決定

也就是說,對 Unity3D 來說,一個 GameObject 該如何呈現在場景中,是由元件所決定的。 如:Cube 透過 MeshFilter 決定外形,由 MeshRenderer 決定色彩、外觀、材質以及被繪圖引擎繪製。

元件的複製

接著稍微修改剛剛的程式,我們將物件的 ID 印出來,並且複製一次這個物件,來觀察當我們使用 Instantiate 方法時,元件會怎麼變化。

 1#pragma strict
 2
 3var components:Component[];
 4static var clone:GameObject;
 5
 6function Start () {
 7	Debug.Log("Object Name: " + name);
 8
 9	components = GetComponents(Component);
10	var i:int;
11	var currentComponent:Component;
12	Debug.Log("Total Component: " + components.Length);
13	for(i = 0; i < components.Length; i++) {
14  	# NOTICE: Unity3D will warning you
15		currentComponent = components.GetValue(i);
16		Debug.Log(currentComponent.GetType() + " - ID: " + currentComponent.GetHashCode());
17	}
18	
19	if(!clone) {
20		clone = GameObject.Instantiate(gameObject);
21	}
22}
23
24function Update () {
25
26}

螢幕快照 2014-01-03 上午11.27.05.png

看起來每次 Instanticate 出來的 GameObject 都是一個「獨立的物件」並不會參照到原本的物件上。

關於這點,如果認真看官網的 Instanticate 解釋,裡面這樣說「這類似於編輯器的複製動作,當你克隆一個元件時所有子物件以及元件都會被克隆一遍」

其實這邊會有關於物件複製的疑問,全體克隆的行為其實仔細思考就會想通。至於大量的物件複製不會很慢的問題,就比較難想,目前我也想不到。不過,有時候必要的花費是必須的,這樣理解也許會比較好。

那麼關於 InstantiateDestroy 的使用,就會很重要,畢竟要盡可能減少大量產生物件。

在製作 Unity3D 的作業時,我就碰到複製怪卻讓遊戲會卡住一瞬間的問題。 經過搜集資料後,發現 InstantiateDestory 其實算是蠻昂貴的動作(從的解釋來看,假設現在我這是一個非常複雜上面有數百個元件的物件,那我複製一次要花多久去拷貝這個物件?)也因此,會建議大家使用 GameObjectPool 的方式管理,也就是產生後不刪除,而是先 Deactive 直到下次需要被使用才再次 Active。 (Asset Store 上有販賣 GameObjectPool 的套件,網路上也有不少語法,但是建議仔細看討論跟分析程式碼來選擇適合的。)

了解 Component 化的設計

據說這邊卡很久,因為我在研究簡報跟運用

前面有提到一個介紹 Unity3D 的元件簡報,裡面的「Component 化」的含義,我反覆閱讀之後才終於頓悟,原來是這麼個一回事。

首先,我們要再次複習 GameObject 和 Component 可以做的事情才行,還有再次釐清特性。

GameObject

  • 可以附加 Component
  • 可以有 Child Object

Component

  • 不能附加 Component
  • 不能有 Child Object
  • 從 MonoBehaviour 類別繼承 (Component 類別的子類)
  • 可以重複疊加到 GameObject 上

我先說明一下為什麼要搞清楚特性,最初我希望在 Component 下增加 Component 但是不能,於是我改為 GameObject 下增加,但是我的測試語法是一個透過 frame 數來切換 Buffer 的 Component (更改顏色) 但是我卻發現 Component 會無限制地被疊加上去,這並不是我所期望的。

並且我對「簡報」中的「階層」感到疑惑,難道我的做法有問題嗎?透過一段時間的檢查思考方式,我得出了一個結論。

在較複雜的遊戲物件(如:玩家、敵人)我們應該適當的增加 GameObject 的階層,來分離與釐清一個物件的行為。 (約簡報 45 頁,大致上就是最終的目標)

註:動態的為物件增加元件(AddComponent)不一定是好的做法,因為可能不小心附加過多的元件到物件上,透過 Prefab (類似物件版模的東西)與階層化的物件處理,會比較恰當。

實作 Component 化的設計

這部分我會實作一個 Player 物件,然後擁有 Collider 可以處理滑鼠點擊,並且用這個方式控制粒子特效的開啟或者關閉。 (範例檔案:下載

為了將物件的任務個別分離,我這樣設計了我的物件階層。

  • Player - 控制、狀態 (Empty GameObject)
    • Model - 外觀 (Cube)
      • BufferFx - 粒子特效 (Particle System)

並且寫了三個 Script (Component) 去控制開啟與關閉粒子特效。

 1#pragma strict
 2
 3private var bufferFx:GameObject;
 4protected var bufferActive:boolean = false;
 5
 6function Start () {
 7	bufferFx = transform.FindChild("Model/BufferFx").gameObject;
 8}
 9
10function Update () {
11
12}
13
14function ActiveBuffer() {
15	if(bufferActive) {
16		bufferFx.SendMessage("DeactiveBuffer", SendMessageOptions.DontRequireReceiver);
17	} else {
18		bufferFx.SendMessage("ActiveBuffer", SendMessageOptions.DontRequireReceiver);
19	}
20	bufferActive = !bufferActive;
21}

在 Unity3D 中似乎沒有提供直接取得 GameObject Child 的方法,因此需要從 Transform 中取得下階層的 GameObject 物件(是否有更好的做法不確定,另外父子階層在 Unity3D 是由 Transform 控制的)

 1#pragma strict
 2
 3function Start () {
 4
 5}
 6
 7function Update () {
 8
 9}
10
11function OnMouseDown() {
12	SendMessageUpwards("ActiveBuffer", SendMessageOptions.DontRequireReceiver);
13}

這部分就比較簡單,當物件被按下去時觸發 Buffer 動作(也許可以把 Collider 改到 Player 階層,讓 Player 來處理)

 1#pragma strict
 2
 3function Start () {
 4}
 5
 6function Update () {
 7
 8}
 9
10function ActiveBuffer() {
11	particleSystem.Play();
12}
13
14function DeactiveBuffer() {
15	particleSystem.Stop();
16}

最後是播放粒子特效與停止粒子特效的處理,透過從 Buffer.js 用 SendMessage 收到的訊息來處理。 (據說使用 SendMessage 也是較為昂貴的操作,不過那也是後話了~)

註:如果粒子系統一開始就播放,那很可能是 Play on Awake 被勾選了,似乎 Unity3D 預設會幫音效、粒子特效等可播放的元件勾選起來。

後記

其實到後來發現有一點「考察」的感覺,不過實際上簡單體驗使用 Component-Based Programing 後,真的會發現相對物件導向的繼承式做法來設計遊戲,更為容易理解。

這篇文章應該還有不少部分寫得不好,有高人知道改進的方法希望可以留言提醒一下。

參考資料