[Unity] 設計可以在建置前自我檢查的場景 - Make GameObject Check Itself

我在製作遊戲 UI,或者場景中有些會不斷在顯示、隱藏之間切換的物件時,會利用物件的 SetActive() 來進行操作;或者有其他原因,會在場景中佈置一些關閉的物件,之後再依據遊戲流程重新啟動。

在這樣的情境下,我常常發生一個失誤:在編輯場景時為了確認某個效果,而將應該關閉的物件啟動,或者將應該啟動的物件關閉,最後忘記復原就進行了儲存與遊戲建置,然後理所當然地出錯了。

但畢竟場景配置不像程式碼撰寫,會在失誤時提示錯誤。於是這次就嘗試建立一套機制,可以對場景配置進行自動檢查,來避免人為的失誤造成遊戲運行的錯誤。

步驟一:在儲存場景的時候自動執行一段檢查腳本

這邊利用的是 AssetModificationProcessor.OnWillCreateAsset 這個 Unity 所提供的 API 接口,這段腳本必須放在 Editor 資料夾中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneSaveProcessor : UnityEditor.AssetModificationProcessor {
static string[] OnWillSaveAssets (string[] paths) {
foreach (GameObject obj in SceneManager.GetActiveScene ().GetRootGameObjects ()) {
DoTaskInTransform (obj.transform);
}
return paths;
}
private static void DoTaskInTransform (Transform parent) {
Task (parent);
foreach (Transform trans in parent.GetComponentsInChildren<Transform> (true)) {
if (trans != parent.transform) {
DoTaskInTransform (trans);
}
}
}
private static void Task(Transform trans) {
Component[] components = trans.GetComponents<Component> ();
foreach (Component component in components) {
if (component is IBeforeSaveScene) {
(component as IBeforeSaveScene).BeforeSaveScene ();
}
}
}
}

這段程式碼會在有 Asset 被儲存時由 Unity 自動執行,其中也包括場景儲存時。

當這段程式碼被執行時,首先會利用 SceneManager.GetActiveScene ().GetRootGameObjects () 將當前場景的所有 根物件(Root GameObject) 找出來,並進一步利用 遞迴(Recursion) 找出所有場景中不論啟動或關閉的物件,針對物件執行 Task ()。

為了找出關閉的物件,GetComponentsInChildren (true) 必須加上 true。

每當 Task () 被執行了,便會找出當下物件中的所有 Component,並對有 IBeforeSaveScene 介面的 Component 執行 BeforeSaveScene () 方法。

IBeforeSaveScene 介面的設計如下,這段腳本不必放在 Editor 資料夾中:

1
2
3
public interface IBeforeSaveScene {
void BeforeSaveScene ();
}

這個介面在任何 Component 都可以添加繼承,用於給 Task () 進行辨認以及轉型後的呼叫。如果有需要,也可以依照專案需求更動方法,或者再添加其他介面,只要 Task () 有對應進行呼叫的修改即可。

步驟二:在需要檢查的物件上加上對應的介面與方法

自動檢查的動作有了,再來就是要依照需求在特定物件上加上檢查條件與對應動作,這邊我舉兩個可行例子進行說明,但一定還有更多的應用可以被發揮。

  • 固定於場景中關閉物件:
1
\[code language="csharp"\] using UnityEngine; public class AlwaysInactiveOnSave : MonoBehaviour, IBeforeSaveScene { public bool AlwaysInactive = true; public void BeforeSaveScene () { if (AlwaysInactive && this.gameObject.activeSelf) { Debug.Log (this.gameObject.name + " is automatically turn off"); this.gameObject.SetActive (false); } } } \[/code\]

以上腳本被掛載於物件上時,物件將在儲存時被檢查是否關閉,若未關閉則顯示一條 Log 並自動關閉。

這個檢查的目的是針對因為需求,而預設關閉的物件,確保沒有因為測試而被人為失誤開啟,反之也可以撰寫腳本檢查物件是否有啟動。

  • 自動更新 List:
1
2
3
4
5
6
7
8
9
using UnityEngine;
public class BeforeSceneSaveTest : MonoBehaviour, IBeforeSaveScene {
public void BeforeSaveScene () {
HaveGameObjectList targetComponent = GameObject.FindObjectOfType<HaveGameObjectList> ();
if (targetComponent.gameObjectList.Find(p => p.transform == this.transform) == null) {
targetComponent.gameObjectList.Add (this.gameObject);
}
}
}

這段腳本的使用情境是,物件會自行檢查自身是否有被參照於另一個特定的腳本之中,若沒有在對方的 List 則自動補上。

通常為了管理大量物件的共同動作,可能會在某個腳本中儲存了一系列的物件參照,可是當場景編輯過程中物件有被刪減,卻未必會記得對相關的物件參照進行更新,這個檢查便可以將相關動作自動化,避免人為失誤。

更多的說明

這段機制設計之目的,是希望將一些場景的檢查工作從人工處理換成自動處理,是在自動化建置與專案管理中的一環。同樣的工作其實也可以將所有檢查設定撰寫於 OnWillSaveAssets() 方法之中,而非逐一分散於每個物件之上。

分散式設計的好處是,如果檢查的對象有所變動,並不一定要對整套檢查的腳本進行修改,只要替換對應的腳本即可。甚至在不同專案之中,同樣的檢查腳本可以不斷被重複應用,有著更高的彈性與可利用性。

但相對有個缺點是,檢查的動作之間是平行的工作而非有次序的工作,對於一些有先後次序要求的檢查工作可能較難以勝任。另外就是專案中的腳本數量可能會應為檢查工作大量增加。

最近發現 Unity 提供的一些 Asset Processor 相關接口充滿著各種可能性,目前這套機制對我來說尚已足夠,或許未來有新的需要可能會再進一步調整。

希望這篇心得可以讓期待將許多工作自動化的你得到一些幫助。

參考