前言
最近這段時間都沒有時間寫博客,一方面是因爲ECS項目遇到了瓶頸,實在無法忍受太多不完善的地方,又或者我對ECS的理解有誤。但是最基本的遊戲組件都無法使用,實在是讓我開發起來十分喫力,儘管已經把地圖開發進度強行推進到了9/30,但是離我的目標:無限地圖,還有非常遙遠的距離。期間花了一個星期來研究Shader Graph,又花一天時間來研究Visual Effect,中秋節又收到朋友邵偉的大作《Unity2017虛擬現實開發標準教程》,這裏特別推薦一下:
這本書內容非常詳實,全是頂級乾貨,太讚了!而且還是官方授權認可的教材,對虛擬現實技術感興趣的值得入手,希望此作大賣,VR技術革命更快到來!
ECS的項目需要暫停一段時間,因爲要等待官方更新更多的組件,例如物理/動畫/渲染之類的基本組件,混合開發是可行的,但是加大了開發的複雜度,還不如直接用OOP。所以還是要等待純粹的ECS開發到來,進行Pure ECS開發。
當然一些大神已經在使用Pure ECS開發遊戲了,但是我的技術實在有限,很多地方困難重重,以至於無法有效解決。
所以這一篇不再寫ECS,而是研究VFX(Visual Effect,可視化特效)。
準備工作
這裏使用的學習案例是官方的SpaceShip示例,源碼下載地址,特別要注意的是:需要安裝Git LFS。
0下載Unity編輯器(2019.2.0f1 or 更新的版本),if(已經下載了)continue;
1克隆:git clone [email protected]:Unity-Technologies/SpaceshipDemo.git --recurse
或下載發佈包
2注意:不要下載Github的Zip包,下載發佈包。然後將Spaceship Demo添加到Unity Hub項目中;
3用Unity Hub打開的開源項目:Spaceship Demo,等待Unity進行編譯工作;
4打開項目後,啓動場景在Scenes目錄下,打開Boot場景。
之所以不下載Zip包,而是發佈頁面的壓縮包,是因爲Github不能生成正確的LFS存檔,所以點擊發布包的鏈接下載。
啓動
先啓動整個示例看一下炫酷的特效場景好了,哇噢,哇噻,炫酷的粒子特效,屌炸天的全息控制檯……
總之,這個項目的起點是AAA級遊戲。
其實網絡上早就有這個項目的演示視頻了,我也想錄制一個,不過,我還是喜歡文字來記錄一下,這樣比較節約讀者的寬帶。
實際上是比較懶的緣故,就沒有錄製視頻,還是大家自己運行一下比較爽。
那麼這個項目的啓動場景裏面其實沒有幾個腳本,看了一遍以後發現根本不明白他的運行機制是什麼,直到我們注意到Gameplay Ingredients框架,這是大佬peeweek寫的遊戲開發框架,有興趣的朋友可以去研究一下。
就是這個東西在作怪,有些腳本,即使我們不去調用它,腳本也會自動運行。
這些對象大家可以挨着看一遍,根本沒有調用切換場景之類的方法,但是所有的東西都自動運行。
Initializing all Managers…
UnityEngine.Debug:Log(Object)
GameplayIngredients.Manager:AutoCreateAll() (at LocalPackages/net.peeweek.gameplay-ingredients/Runtime/Managers/Manager.cs:37)
我們發現Debug中有這樣一行日誌,於是點進去:
[RuntimeInitializeOnLoadMethod]
static void AutoCreateAll()
{
var exclusionList = GameplayIngredientsSettings.currentSettings.excludedeManagers;
Debug.Log("Initializing all Managers...");
foreach(var type in kAllManagerTypes)
{
if(exclusionList != null && exclusionList.ToList().Contains(type.Name))
{
Debug.Log($"Manager : {type.Name} is in GameplayIngredientSettings.excludedeManagers List: ignoring Creation");
continue;
}
var attrib = type.GetCustomAttribute<ManagerDefaultPrefabAttribute>();
GameObject gameObject;
if(attrib != null)
{
var prefab = Resources.Load<GameObject>(attrib.prefab);
if(prefab == null) // Try loading the "Default_" prefixed version of the prefab
{
prefab = Resources.Load<GameObject>("Default_"+attrib.prefab);
}
if(prefab != null)
{
gameObject = GameObject.Instantiate(prefab);
}
else
{
Debug.LogError($"Could not instantiate default prefab for {type.ToString()} : No prefab '{attrib.prefab}' found in resources folders. Ignoring...");
continue;
}
}
else
{
gameObject = new GameObject();
gameObject.AddComponent(type);
}
gameObject.name = type.Name;
GameObject.DontDestroyOnLoad(gameObject);
var comp = (Manager)gameObject.GetComponent(type);
s_Managers.Add(type,comp);
Debug.Log(string.Format(" -> <{0}> OK", type.Name));
}
}
原來在Manager腳本中自動調用了這個方法,我們注意到這個靜態方法使用了[RuntimeInitializeOnLoadMethod]
定語標籤來修飾,我們想貓膩就在這裏,於是我寫了一個方法來進行測試:
[RuntimeInitializeOnLoadMethod]
static void AutoCall()
{
Debug.Log("[RuntimeInitializeOnLoadMethod]...呃,這個定語標籤起作用了,自動調用了所修飾的靜態方法!!!");
}
編譯後運行,果然得到了對應的日誌:
[RuntimeInitializeOnLoadMethod]…呃,這個定語標籤起作用了,自動調用了所修飾的靜態方法!!!
UnityEngine.Debug:Log(Object)
GameplayIngredients.Manager:AutoCall() (at LocalPackages/net.peeweek.gameplay-ingredients/Runtime/Managers/Manager.cs:29)
要知道,我們根本沒有調用這個方法,而是這個靜態方法被系統自動調用了,這就是Gameplay Ingredients框架的魔力了。
可惜作者並沒有說詳細的使用方法,於是我們只能摸着石頭過河了。
既然已經打開了Manager腳本,就從這裏開始研究好了,看了一會兒,我們就知道Manager腳本是所有Manager類的抽象基類,典型的OOP設計模式。在Manager抽象類中定義了一個字典來對所有的Manager進行管理:
private static Dictionary<Type, Manager> s_Managers = new Dictionary<Type, Manager>();
所有的遊戲管理器都被保存在這個字典裏面,在需要的時候可以通過公開的Get方法來獲取:
public static T Get<T>() where T: Manager
{
if(s_Managers.ContainsKey(typeof(T)))
return (T)s_Managers[typeof(T)];
else
{
Debug.LogError($"Manager of type '{typeof(T)}' could not be accessed. Check the excludedManagers list in your GameplayIngredientsSettings configuration file.");
return null;
}
}
這裏使用了泛型T來代表繼承了Manager的管理器子類,只需要從字典裏返回出去即可,方便快捷,也是OOP常用的方式。
接下來是用一個靜態只讀的字段來保存所有管理器的類型:
static readonly Type[] kAllManagerTypes = GetAllManagerTypes();
它又調用了:
static Type[] GetAllManagerTypes()
{
List<Type> types = new List<Type>();
foreach(var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type[] assemblyTypes = null;
try
{
assemblyTypes = assembly.GetTypes();
}
catch
{
Debug.LogError($"Could not load types from assembly : {assembly.FullName}");
}
if(assemblyTypes != null)
{
foreach (Type t in assemblyTypes)
{
if (typeof(Manager).IsAssignableFrom(t) && !t.IsAbstract)
{
types.Add(t);
}
}
}
}
return types.ToArray();
}
這裏的管理器類型是寫在程序集裏面的,源碼已開源,有興趣的朋友可以去看看:
這裏就不去深究了,完全是個無底洞!
但是通過Debug日誌,我們發現它加載了以下的管理器:
. AudioManager 音頻管理器
- SubtitleManager 字幕管理器
- DebugPOVManager 調試第一人稱視點管理器
- VFXDebugManager 可視化特效調試管理器
- SettingManager 設置管理器
- ShakeManager 震動管理器
- FullScreenFadeManager 全屏淡入管理器
- GameManager 遊戲管理器
- GameSaveManager 遊戲保存管理器
- UIEventManager 用戶界面事件管理器
- VirtualCameraManager 虛擬相機管理器
- LevelStreamingManager 關卡加載管理器
總共有12個管理器,這些管理器會優先在Resources資源文件夾加載預設,如果沒有預設就用腳本生成,相關代碼如下:
[RuntimeInitializeOnLoadMethod]
static void AutoCreateAll()
{
var exclusionList = GameplayIngredientsSettings.currentSettings.excludedeManagers;
Debug.Log("Initializing all Managers...");
foreach(var type in kAllManagerTypes)
{
if(exclusionList != null && exclusionList.ToList().Contains(type.Name))
{
Debug.Log($"Manager : {type.Name} is in GameplayIngredientSettings.excludedeManagers List: ignoring Creation");
continue;
}
var attrib = type.GetCustomAttribute<ManagerDefaultPrefabAttribute>();
GameObject gameObject;
if(attrib != null)
{
var prefab = Resources.Load<GameObject>(attrib.prefab);
if(prefab == null) // Try loading the "Default_" prefixed version of the prefab
{
prefab = Resources.Load<GameObject>("Default_"+attrib.prefab);
}
if(prefab != null)
{
gameObject = GameObject.Instantiate(prefab);
}
else
{
Debug.LogError($"Could not instantiate default prefab for {type.ToString()} : No prefab '{attrib.prefab}' found in resources folders. Ignoring...");
continue;
}
}
else
{
gameObject = new GameObject();
gameObject.AddComponent(type);
}
gameObject.name = type.Name;
GameObject.DontDestroyOnLoad(gameObject);
var comp = (Manager)gameObject.GetComponent(type);
s_Managers.Add(type,comp);
Debug.Log(string.Format(" -> <{0}> OK", type.Name));
}
}
貌似這段代碼一開始引用過了,就當是湊字數好了,可惜沒有字數要求,所以算是白湊字數了Orz。
加載完成就會打印日誌:-> 某某管理器 OK
今天就先研究到這裏好了,洗洗睡了!如果有朋友對這個感興趣的話請留言,這樣也許會繼續下一篇,否則就偷懶不寫了。
作者的話
如果喜歡可以點贊支持一下,謝謝鼓勵!如果有什麼疑問可以給我留言,有錯漏的地方請批評指證!
技術難題?加入開發者聯盟:566189328(QQ付費羣)提供有限技術探討,以及,心靈雞湯Orz!
當然,不需要技術探討也歡迎加入進來,在這裏劈柴、遛狗、聊天、擼貓!( ̄┰ ̄*)