Unity2019最新科技VFX初探

Unity2019最新VFX技術研究

前言

最近這段時間都沒有時間寫博客,一方面是因爲ECS項目遇到了瓶頸,實在無法忍受太多不完善的地方,又或者我對ECS的理解有誤。但是最基本的遊戲組件都無法使用,實在是讓我開發起來十分喫力,儘管已經把地圖開發進度強行推進到了9/30,但是離我的目標:無限地圖,還有非常遙遠的距離。期間花了一個星期來研究Shader Graph,又花一天時間來研究Visual Effect,中秋節又收到朋友邵偉的大作《Unity2017虛擬現實開發標準教程》,這裏特別推薦一下:
書面
目錄1
目錄2
彩頁
這本書內容非常詳實,全是頂級乾貨,太讚了!而且還是官方授權認可的教材,對虛擬現實技術感興趣的值得入手,希望此作大賣,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寫的遊戲開發框架,有興趣的朋友可以去研究一下。
就是這個東西在作怪,有些腳本,即使我們不去調用它,腳本也會自動運行。
Boot場景
這些對象大家可以挨着看一遍,根本沒有調用切換場景之類的方法,但是所有的東西都自動運行。

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

這裏的管理器類型是寫在程序集裏面的,源碼已開源,有興趣的朋友可以去看看:
Gameplay Ingredients
這裏就不去深究了,完全是個無底洞!
但是通過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

今天就先研究到這裏好了,洗洗睡了!如果有朋友對這個感興趣的話請留言,這樣也許會繼續下一篇,否則就偷懶不寫了。

作者的話

Alt

如果喜歡可以點贊支持一下,謝謝鼓勵!如果有什麼疑問可以給我留言,有錯漏的地方請批評指證!
技術難題?加入開發者聯盟:566189328(QQ付費羣)提供有限技術探討,以及,心靈雞湯Orz!
當然,不需要技術探討也歡迎加入進來,在這裏劈柴、遛狗、聊天、擼貓!( ̄┰ ̄*)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章