Unity的50個使用技巧(2016 Edition)

開發流程

1. 確定開始的縮放比例,並以相同縮放比例構建所有原型。否則,你可能需要後續重做assets(例如,無法總是正確地縮放動畫)。對於3D遊戲,採用1 Unity單位= 1m通常是最佳的。對於不使用照明或物理的2D遊戲,採用1 Unity單位 = 1 像素(在“設計”分辨率階段)通常是較好的。對於UI(以及2D遊戲),選擇設計分辨率(我們使用HD或2xHD,並將所有assets設計爲以此分辨率縮放。

 

2. 使每個場景都可以運行。這樣可以避免爲了運行遊戲而必須轉換場景,從而加快了測試速度。如果要在所有場景中必需的場景加載之間持續存在對象,這可能需要技巧。一種方法是當持續對象不存在於場景中時,使它們作爲可自行加載的單例模式。另一個技巧中將詳述單例模式。

 

3. 使用源代碼控制,並學習如何有效地使用它。 

將assets序列化爲文本。實際上,它並不會提高場景和Prefab的可合併性,但它會使變化更容易觀測。


採用場景和Prefab共享策略。一般來說,多個人不應在同一場景或Prefab工作。對於小型製作團隊,只要在開始工作前確保沒有人制作場景或Prefab即可。交換表示場景所有權的物理標記可能很有用(如果桌面上有場景標記,你僅可以在某一場景中工作)。

 

將標籤作爲書籤。

確定並堅持採用分支策略。由於場景和Prefab不能平滑地合併,分支稍顯複雜。然而當你決定使用分支時,它應該結合場景和Prefab共享策略使用。

 

使用子模塊時要小心。子模型可能是維護可重用代碼的最佳途徑。但需注意幾個警告事項:

元數據文件通常在多個項目中不一致。對於非Monobehaviour或非Scriptable object代碼而言,這通常不是問題,但對於MonoBehaviours和Scriptable objects使用子模塊可能會導致代碼丟失。

如果你參與許多項目(包括一個或多個子模塊項目),倘若你必須對幾次迭代中的多個項目執行獲取—合併—提交—推送操作以穩定所有項目的代碼,有時會發生更新崩潰(並且如果其他人同時進行變更,它可能會轉變爲持續崩潰)。一種最大程度上降低此效應的方法是在項目初始階段對子模塊進行更改。如此一來,總是需要推送僅使用子模塊的項目;它們從來無需推回。

 

4. 保持測試場景和代碼分離。向存儲庫提交臨時資源和腳本,並在完成後將它們移出項目。

 

5. 如果你要更新工具(尤其是Unity),必須同時進行。當你使用一個與先前不同的版本打開項目時,Unity能夠更好地保留鏈接,但倘若人們使用不同的版本,有時仍然會丟失鏈接。

 

6. 在一個乾淨的項目中導入第三方assets,並從中導出一個可供自己使用的新的資源包。當你直接向項目導入這些資源,它們有時會導致問題:

可能存在衝突(文件或文件名),尤其對於在插件目錄根中存在文件或者在實例中使用StandardAssets中assets的資源。

這些資源可能被無序地放入到自有項目的文件中。如果你決定不使用或者想要移除這些assets,這可能成爲一個重要問題。

 

請按照下述步驟使assets導入更安全:

1)創建一個新項目,然後導入asset

2)運行實例並確保它們能夠工作。

3)將asset排列爲一個更合適的目錄結構。(我通常不對一個資源強制排列自有的目錄結構。但是我確保所有文件均在一個目錄中,同時在重要位置不存在任何可能會覆蓋項目中現有文件的文件。

4)運行實例並確保它們仍可以工作。(有時,當我移動事物時會導致assets損壞,但這通常不應該是一個問題)。

5)現要移除所有無需的事物(如實例)。

6)確保asset仍可編譯,並且Prefab仍然擁有所有自身的鏈接。若留下任何需運行的事項,則對它進行測試。

7)現選定所有assets,並導出一個資源包。

8)導入到你的項目中。

 

7. 自動構建進程。甚至對於小型項目,這步很有用,但對於以下情況尤爲適用:

你需要構建許多不同的遊戲版本。

其他擁有不同程度技術知識的團隊成員需要進行構建,或者

你需要對項目進行小幅調整後才能進行構建。

詳見Unity構建編譯:對於如何執行的較好指導的基本和高級可能性。

 

8. 爲你的設置建立文檔。大部分記錄應在代碼中,但是某些事項應記錄在代碼外。製作設計師通過耗時的設置來篩選代碼。文檔化的設置可以提高效率(若文檔是最新的)。

對下述內容建立文檔:

         標籤使用。

         圖層使用(對於碰撞,剔除和光線投射—從本質上來說,每個圖層對應的使用)。

         圖層的GUI深度(每個圖層對應的顯示)

         場景設置。

         複雜Prefab的Prefab結構。

         常用語偏好。

         構建設置。

      

通用編碼

9. 將所有代碼放入一個命名空間中。這避免了自有庫和第三方代碼之間可能發生的代碼衝突。但不要依賴於命名空間以避免與重要類衝突。即使你會使用不同的命名空間,也不要將“對象”、“動作”或“事件”作爲類名稱。

 

10. 使用斷言。斷言對於代碼中不變量的測試非常有用,它能夠輔助清除邏輯錯誤。Unity.Assertions.Assert類提供了可用的斷言。它們都可以測試一些條件,但如果不符合條件,則在控制檯中寫入錯誤信息。如果你不熟悉如何有效地使用斷言,請參考使用斷言編程的優點(a.k.a.斷言語句)。

 

11. 切勿對顯示文本以外的任何事項使用字符串。尤其應注意,不要使用字符串來標識對象或Prefab。但存在一些例外情形(仍然有一些內容只能通過Unity中的名稱訪問)。在這種情形下,將這些字符串定義爲“AnimationNames”或 “AudioModuleNames”等文件中的常量。倘若這些類變爲不可管理,使用嵌套類後便可類似命名AnimationNames.Player.Run

 

12. 不要使用“Invoke”和“SendMessage”。這些MonoBehaviour方法通過名稱調用其他方法。通過名稱調用的方法難以在代碼中追蹤(無法找到“Usages”,而“發SendMessage”的範圍更寬,因此更難以追蹤)。

較簡便的方法是使用Coroutines和C#操作推出“Invoke”:

1
2
3
4
5
6
7
8
9
10
11
public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, float time)
{
   return monoBehaviour.StartCoroutine(InvokeImpl(action, time));
}
 
private static IEnumerator InvokeImpl(Action action, float time)
{
   yield return new WaitForSeconds(time);
    
   action();
}

你可以參考monoBehaviour模式:

 this.Invoke(ShootEnemy);   //其中ShootEnemy是一個無參數的void法。

如果你實現自己的基礎MonoBehaviour,你可以向其中添加自己的“Invoke”。

另一種較安全的“SendMessage”方法更難以實施。與之相反,我通常使用“GetComponent”變量以獲取父對象,當前遊戲對象或子對象的組件,並直接執行調用。

 

13. 當遊戲運行時,不要讓派生對象混亂層次結構。將它們的父對象設爲場景對象,以便在遊戲運行時更容易找到內容。你可以使用一個空遊戲對象,或者甚至使用一個無行爲的單例模式(詳見本文後面的部分),從而更容易地從代碼進行訪問。將此對象命名爲“DynamicObjects”。

 

14. 明確是否要將空值(null)作爲一個合法值,並儘量避免這麼做

空值可輔助檢測錯誤代碼。但是,如果你使“if”默默地通過空值成爲一種習慣,錯誤代碼將很快運行,同時你只能在很久之後纔會注意到錯誤。此外,隨着每個圖層通過空變量,它可以在代碼深度暴露。我嘗試避免將空值整體作爲一個合法值。

 

我優先採用的常用語不是進行任何空檢查,倘若它是一個問題,讓代碼失敗。有時,在“可重用”方法中,我將檢查出一個值爲空的變量,並拋出一個異常,而不是將它傳遞至其它可能失敗的方法。

在某些情形下,值可以合法爲空,並且需要採取不同的方式處理。在此類情況下,添加註釋來解釋什麼時候某些內容可能爲空,並說明爲什麼可能爲空。

常見場景通常用於inspector配置的值。用戶可以指定一個值,但如果未指定任何值,則使用一個默認值。最好結合包含T值的可選類。(這有點像“可爲空”)。你可以使用一個特殊的屬性渲染器來渲染一個勾選框,若勾選,則僅顯示數值框。

(但切勿直接使用泛型類,你必須擴展特定T值的類)。

1
2
3
4
5
6
7
[Serializable]
public class Optional<T>
{
   public bool useCustomValue;
   public T value;
}


在你的代碼中,你可以採取這種使用途徑:

health= healthMax.useCustomValue ? healthMax.Value : DefaultHealthMax;

 

15. 如果你使用“協程”,學習如何有效地使用它。

“協程”是解決許多問題的一種最有效的方法。但是難以對“協程”進行調式,同時你可以很容易地對它進行混亂的編碼,從而使其他人,甚至包括你自己也無法理解其意義。

你應該知道:

        1)如何併發執行協程。

      2)如何按序執行協程。

      3)如何從現有程序中創建新的協程。

        4)如何使用“CustomYieldInstruction”創建自定義協程。

     

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//This is itself a coroutine
IEnumerator RunInSequence()
{
   yield return StartCoroutine(Coroutine1());
   yield return StartCoroutine(Coroutine2());
}
 
public void RunInParallel()
{
   StartCoroutine(Coroutine1());
   StartCoroutine(Coroutine1());
}
 
Coroutine WaitASecond()
{
   return new WaitForSeconds(1);
}

 

16. 利用擴展法來協同共享接口的組件。有時可以方便地獲取實施某個接口的組件,或者找到這些組件相應的對象。

下述實例使用typeof而不是這些函數的通用版本。通用版本無法協同接口使用,但typeof卻可以。下面的方法將其整潔地套入通用方法之中。

1
2
3
4
5
public static TInterface GetInterfaceComponent<TInterface>(this Component thisComponent)
   where TInterface : class
{
   return thisComponent.GetComponent(typeof(TInterface)) as TInterface;
}


 

17. 利用擴展法使語法更簡潔。例如:

1
2
3
4
5
6
7
8
9
10
11
public static class TransformExtensions
{
   public static void SetX(this Transform transform, float x)
   {
      Vector3 newPosition =
         new Vector3(x, transform.position.y, transform.position.z);
  
      transform.position = newPosition;
   }
   ...
}

 

18. 使用另一種防禦性GetComponent方法。有時通過RequiredComponent強制組件關係可能難以操作,但是這總是可能和可取的,特別是當你調用其它類上的GetComponent。作爲一種替代方法,但需要某個組件打印找到的錯誤信息時,可以使用下述GameObject擴展。

1
2
3
4
5
6
7
8
9
10
11
12
public static T GetRequiredComponent(this GameObject obj) where T : MonoBehaviour
{
   T component = obj.GetComponent();
  
   if(component == null)
   {
      Debug.LogError("Expected to find component of type "
         + typeof(T) + " but found none", obj);
   }
  
   return component;
}


 

19. 避免對相同的事項使用不同的常用語。在許多情況下,有多種常用法。此時,對整個項目選擇一種常用法。其原因在於:

      1)某些常用語不能一起工作。在某個方向中使用一種常用語強行設 計可能不適合另一種常用語。

      2)對於整個項目使用相同的常用語能夠使團隊成員更容易理解進展。它使結構和代碼更容易理解。這樣就更難犯錯。


常用語組示例:

        協程與狀態機。

 嵌套的Prefab,互相鏈接的Prefab和超級Prefab

        數據分離策略。

      對2D遊戲中狀態使用sprites的方法。

        Prefab結構。

        派生策略。

        定位對象的方法:按類型,按名稱,按標籤,按圖層和按引用關係(“鏈接”)。

        分組對象的方法:按類型,按名稱,按標籤,按圖層和按引用數組(“鏈接”)。

        調用其他組件方法的途徑。

      查找對象組和自注冊。

        控制執行次序(使用Unity的執行次序設置,還是使用yield邏輯,利用Awake / Start和Update / Late Update依賴,還是使用純手動的方法,或者採用次序無關的架構)。

        在遊戲中使用鼠標選擇對象/位置/目標:SelectionManager或者對象自主管理。

        在場景變換時保存數據:通過PlayerPrefs,或者是在新場景加載時未毀損的對象。

      組合(混合、添加和分層)動畫的方法。

      輸入處理(中央和本地)

 

20. 維護一個自有的Time類,這可以更容易實現遊戲暫停。包裝一個“Time.DeltaTime”和“Time.TimeSinceLevelLoad”來實現暫停和遊戲速度的縮放。它使用時有點麻煩,但是當對象運行在不同的時鐘速率下就容易多了(例如界面動畫和遊戲動畫)。

 

21. 需要更新的自定義類不應該訪問全局靜態時間。相反,它們應將增量時間作爲它們Update方法的一個參數。當你如上所述實施一個暫停系統,或者當你想要加快或減慢自定義類的行爲時,這樣使這些類變爲可用。

 

22. 使用常見結構進行WWW調用。在擁有很多服務器通信的遊戲中,通常有幾十個WWW調用。無論你是使用Unity的原始WWW類還是使用某個插件,你可以從生成樣板文件的頂部寫入一個薄層獲益。

我通常定義一個Call方法(分別針對Get和Post),即CallImpl協程和MakeHandler。從本質上來說,Call方法通過採用MakeHandler法,從一個解析器,成功和失敗的處理器構建出一個super hander。此外,它也調用CallImpl協程,創建一個URL,進行調用,等待直至完成,然後調用super handler。

其大概形式如下:

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
28
public void Call<T>(string call, Func<string, T=""> parser, Action<T> onSuccess, Action<string> onFailure)
{
    var handler = MakeHandler(parser, onSuccess, onFailure);
    StartCoroutine(CallImpl(call, handler));
}
 
public IEnumerator CallImpl<T>(string call, Action<T> handler)
{
    var www = new WWW(call);
    yield return www;
    handler(www);
}
 
public Action<WWW> MakeHandler<T>(Func<string, T=""> parser, Action<T> onSuccess, Action<string> onFailure)
{
   return (WWW www) =>
   {
      if(NoError(www))
      {
         var parsedResult = parser(www.text);
         onSuccess(parsedResult);
      }
      else
      {
         onFailure("error text");
      }
   }
}


它具有一些優點:

        它允許你避免編寫大量樣板代碼。

        它允許你在中央位置處理某些事項(例如顯示加載的UI組件或處理某些通用錯誤)。

 

23. 如果你有大量文本,將它們放在同一個文件中。不要將它們放入inspector將編輯的字段中。使其在無需打開Unity編輯器,尤其是無需保存場景的前提下易於更改。

 

24. 如果你想執行本地化,將所有字符串分離到同一個位置。有很多方法可以實現這一點。一種方法是針對每個字符串定義一個具有public字符串字段的Text類,例如默認設爲英文。其他語言將其子類化,並使用同等語言重新初始化這些字段。

一些更復雜的技術(其適用情形是正文本較大和/或語言數量較多時)將讀取到一個電子表格中,並基於所選語言提供選擇正確字符串的邏輯。

 

類的設計

25. 確定實現可檢查字段的方法,並將其確立爲標準。有兩種方法:使字段public,或者使它們private並標記爲[可序列化]。後者“更正確”但不太方便(當然不是Unity本身常用的方法)。無論你選擇哪種方式,將它確立爲標準,以便於團隊中開發人員知道如何解釋一個public字段。

    可檢查字段是public的。在這種情況下,public表示“設計師在     運行時更改此變量是安全的。避免在代碼中設置該值”。

    可檢查字段是private,並被標記爲“可序列化”。 在這種情  況下,public表示“在代碼中更改此變量是安全的”(因此,你不應該看到太多,並且在MonoBehaviours 和ScriptableObjects中不應該有任何public字段)。

        

26. 對於組件,切勿使不應在inspector中調整的變量成爲public。否則,它們將被設計師調整,特別是當不清楚它是什麼時。在某些罕見的情況下,這是無法避免的。此時,使用兩條,甚至四條下劃線對變量名添加前綴以警告調整人員:

public float __aVariable;

 

27. 使用Property Drawers使字段更加用戶友好。可以使用Property Drawers自定義inspector中的控制。這樣可以使你能夠創建更適合數據性質的控制,並實施某些安全保護(如限定變量範圍)。

 

28. 相較於Custom Editors,更偏好採用PropertyDrawers。Property Drawers是根據字段類型實現的,因此涉及的工作量要少得多。另外,它們的重用性更佳—一旦實現某一類型,它們可應用於包含此類型的任何類。而Custom Editors是根據MonoBehaviour實現的,因此重用性更少,涉及的工作量更多。

 

29. 默認密封MonoBehaviours一般來說,UnityMonoBehaviours的繼承友好不高:

        類似於Start和Update,Unity調用信息的方式使得在子類中難以使用這些方法。你稍不注意就可能調用錯誤內容,或者忘記調用一個基本方法。當你使用custom editors時,通常需要對editors複製繼承層次結構。任何人在擴展某一類時,必須提供自己的editor,或者湊合着使用你提供的editor。

 在調用繼承的情況下,如果你可以避免,不要提供任何Unity信息方法。如果你這樣做,切勿使他們虛擬化。如果需要,你可以定義一個從信息方法調用的空的虛擬函數,子類可以覆蓋此方法來執行其他工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyBaseClass
{
   public sealed void Update()
   {
      CustomUpdate();
      ... // This class's update
   }
 
   //Called before this class does its own update
   //Override to hook in your own update code.
   virtual public void CustomUpdate(){};
}
 
public class Child : MyBaseClass
{
   override public void CustomUpdate()
   {
      //Do custom stuff
   }
}

 這樣可以防止某一類意外地覆蓋你的代碼,但是仍能夠賦予其掛鉤連接Unity信息的功能。我不喜歡這種模式的一個原因是事項次序發生問題。在上述示例中,子類可能想在此類自行更新後直接執行。

 

30. 從遊戲邏輯分離接口。一般來說,接口組件不應該知道任何關於所應用遊戲的任何內容。向它們提供需要可視化的數據,並訂閱事件以查出用戶與它們交互的時間。接口組件不應該創建gamelogic。它們可以篩選輸入,從而確認其有效性,但是主規則處理不應在其他位置發生。在許多拼圖遊戲中,拼圖塊是接口的擴展,同時不應該包含任何規則。

(例如,棋子不應該計算自身的合法移動)。

類似地,輸入應該從作用於此輸入的邏輯分離。使用一個通知你的actor移動意圖的輸入控制器;由actor處理是否實際移動。

 這裏是一個允許用戶從選項列表中選擇武器的UI組件的簡化示例。這些類知曉的唯一遊戲內容是武器類(並且只是因爲武器是這個容器需要顯示數據的有用源)。此外,遊戲也對容器一無所知;它所要做的是註冊OnWeaponSelect事件。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public WeaponSelector : MonoBehaviour
{
   public event Action OnWeaponSelect {add; remove; }
   //the GameManager can register for this event
 
   public void OnInit(List  weapons)
   {
      foreach(var weapon in weapons)
      {
 
          var button = ... //Instantiates a child button and add it to the hierarchy         
  
          buttonOnInit(weapon, () => OnSelect(weapon));
          // child button displays the option,
          // and sends a click-back to this component
      }
   }
   public void OnSelect(Weapon weapon)
  {
      if(OnWepaonSelect != null) OnWeponSelect(weapon);
   }
}
 
public class WeaponButton : MonoBehaviour
{
    private Action<> onClick;
 
    public void OnInit(Weapon weapon, Action onClick)
    {
        ... //set the sprite and text from weapon
 
        this.onClick = onClick;
    }
 
    public void OnClick() //Link this method in as the OnClick of the UI Button component
    {
       Assert.IsTrue(onClick != null);  //Should not happen
 
       onClick();
    }   
}

 

31. 分離配置,狀態和簿記。

          配置變量是指一類被inspector調整從而通過其屬性定義對象的變量。如maxHealth

          狀態變量是指一類可完全確定對象當前狀態的變量,以及如果你的遊戲支持保存操作,你需要保存的一類變量。如currentHealth

          簿記變量是指用於速度、方便或過度狀態。它們總是完全可以通過狀態變量確定。如previousHealth

通過分離這些變量類型,你可以更容易知道哪些是可以更改的,哪些是需要保存的,哪些是需要通過網絡發送/檢索的,並允許你在某種程度上強制執行此類操作。下面給出了一個關於此設置的簡單示例。

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
public class Player
{
   [Serializable]
   public class PlayerConfigurationData
   {
      public float maxHealth;
   }
 
   [Serializable]
   public class PlayerStateData
   {
      public float health;
   }
 
   public PlayerConfigurationData configuration;
   private PlayerState stateData;
 
   //book keeping
   private float previousHealth;
 
   public float Health
   {
      public get { return stateData.health; }
      private set { stateData.health = value; }
   }
}

 

32. 避免使用public索引耦合數組。例如,不要定義任何武器數組,任何子彈數組,以及任何顆粒數組,從而使你的代碼類似於:

1
2
3
4
5
6
7
8
9
10
11
public void SelectWeapon(int index)
{
   currentWeaponIndex = index;
   Player.SwitchWeapon(weapons[currentWeapon]);
}
  
public void Shoot()
{
   Fire(bullets[currentWeapon]);
   FireParticles(particles[currentWeapon]);
}


這類問題不出在代碼中,而是在inspector進行設置時不發出錯誤。相反,定義封裝三個變量的類,並創建下述數組:

1
2
3
4
5
6
7
[Serializable]
public class Weapon
{
   public GameObject prefab;
   public ParticleSystem particles;
   public Bullet bullet;
}

此代碼看起來更整潔,但最重要的一點是,在inspector中設置數據更難以出錯。

 

33. 避免使用除序列以外的結構數組。例如,玩家可能有三種攻擊類型。每種類型使用當前武器,但生成不同的子彈和不通過的行爲。

你可能會嘗試將三個子彈轉儲到某個數組中,然後使用此類邏輯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void FireAttack()
{
   /// behaviour
   Fire(bullets[0]);
}
  
public void IceAttack()
{
   /// behaviour
   Fire(bullets[1]);
}
  
public void WindAttack()
{
   /// behaviour
   Fire(bullets[2]);
}
Enums can make things look better in code…
 
public void WindAttack()
{
   /// behaviour
   Fire(bullets[WeaponType.Wind]);
}


最好使用分離變量以便於名稱輔助顯示將放入的內容。使用一類使其整潔。

1
2
3
4
5
6
7
[Serializable]
public class Bullets
{
   public Bullet fireBullet;
   public Bullet iceBullet;
   public Bullet windBullet;
}

它假設不存在其他火、冰和風的數據。

 

34. 將數據集中在可序列化類中,以使inspector中的事項更整潔。一些實體可能有幾十個可調分。對於在inspector尋找正確的變量,它可能成爲一個噩夢。要使事項更簡便,請遵循以下步驟:

       對於各變量組定義分離類。使它們公開化和可序列化。

       在主類中,對上述每個類型的變量定義爲公開。

     切勿在Awake或Start中初始化這些變量;由於它們是可序列化的,Unity會對它進行處理。你可以通過在定義中分配值來指定先前的默認值;

這將變量集中到inspector中的可摺疊單元,從而更容易進行管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Serializable]
public class MovementProperties //Not a MonoBehaviour!
{
   public float movementSpeed;
   public float turnSpeed = 1; //default provided
}
  
public class HealthProperties //Not a MonoBehaviour!
{
   public float maxHealth;
   public float regenerationRate;
}
  
public class Player : MonoBehaviour
{
   public MovementProperties movementProeprties;
   public HealthPorperties healthProeprties;
}


35. 使非MonoBehaviours的類可序列化,即使它們不用於public字段。當 Inspector處於Debug模式下,它允許你查看inspector中的類字段。這同樣適用於嵌套的類(私密或公開)。

 

36. 避免通過代碼修改那些在Inspector中可編輯的變量。Inspector中可調整的變量即爲配置變量,且不應該視爲運行期間的常量,更不能作爲一個狀態變量。按照這種操作使得將組件狀態重置爲初始狀態的編寫方法更加簡便,同時使變量動作更清楚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Actor : MonoBehaviour
{
   public float initialHealth = 100;
    
   private float currentHealth;
 
   public void Start()
   {
      ResetState();
   }  
 
   private void Respawn()
   {
      ResetState();
   }
 
   private void ResetState()
   {
      currentHealth = initialHealth;
   }
}

 

模式                          

模式是指一種按標準方法解決常見問題的途徑。Bob Nystrom著有的《遊戲編程模式》(免費在線閱讀)爲如何將模式應用於遊戲編程中出現的問題提供了一種有效的觀察資源。Unity本身使用了許多模式:Instantiate是原型模式的一個示例;MonoBehaviours遵循樣板模式的一個版本,UI和動畫使用了觀察者模式,而新的動畫引擎利用了狀態機。

這些技巧均涉及到Unity模式的具體應用。

 

37.爲了方便考慮,使用單例模式。下述類將從其自身繼承的任何類自動轉換爲單例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
   protected static T instance;
  
   //Returns the instance of this singleton.
   public static T Instance
   {
      get
      {
         if(instance == null)
         {
            instance = (T) FindObjectOfType(typeof(T));
  
            if (instance == null)
            {
               Debug.LogError("An instance of " + typeof(T) +
                  " is needed in the scene, but there is none.");
            }
         }
  
         return instance;
      }
   }
}


單例模式對於ParticleManager or AudioManager or GUIManager等管理器很有用。

(許多程序員對模糊命名爲XManager的類報警,這是因爲它指向一個命名不當,或者設計有太多不相關任務的類)。一般來說,我同意這種做法。但是,我們在每個遊戲中只有少量的管理器,並且它們在每個遊戲中都做同樣的事情,因此這些類實際上是常用語。)

 避免對非管理器(如玩家)的Prefabs獨特示例使用單例模式。若不遵守這一原則會使繼承分層複雜化,並使某些變更類別更困難。而是保持引用你的GameManager(或者其他合適的超級類)。針對常在類外部使用的public變量和方法定義靜態屬性和方法。這允許你編寫GameManager.Player,而不是GameManager.Instance.player。

 如其他技巧中所述,單例模式也可用於創建持續在追蹤全局數據的場景加載之間的默認派生點和對象。

 

38.使用狀態機獲取不同狀態下的不同行爲或者執行狀態轉換時的代碼。一個輕量級狀態機具有多種狀態,並且每個狀態允許你指定進入或存在狀態的運行動作,以及更新動作。這可以使代碼更清潔,同時具有較少的錯誤傾向。如果你的Update方法代碼有一個改變其動作或者下面的變量的if-或者switch語句,那麼你將從狀態機受益:

hasShownGameOverMessage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void Update()
{
   if(health <= 0)
   {
      if(!hasShownGameOverMessage)
      {
         ShowGameOverMessage();
         hasShownGameOverMessage = true; //Respawning resets this to false
      }
   }
   else
   {
      HandleInput();
   }  
}


 

若存在更多狀態,這種類型的代碼可能變得非常混亂;狀態機可以使它變得非常清潔。

 

39.使用類型UnityEvent的字段在inspector中設置觀察者模式。UnityEvent類允許你將佔用四個參數的方法鏈接到使用與Buttons上事件相同UI界面的inspector。

 

40.當一個字段值發生變化時,使用觀察者模式以檢測。只有當遊戲中頻繁發生變量變化時纔會發生執行代碼的問題。我們已經在一個通用類中創建一種關於此模式的通用解決方案,這樣允許你無論何時發生值變化時註冊事件。以下是一個health示例。其創建方式爲:

1
2
/*ObservedValue*/ health = new ObservedValue(100);
health.OnValueChanged += () => { if(health.Value <= 0) Die(); };


你現在可以在任何位置更改它,而無需在每個檢查位置執行檢查,例如:

if(hit)health.Value -= 10;

無論何時health值低於0,調用Die方法。更多討論和實施,請參考此發佈

 

41.在prefabs上使用Actor模式。(這不是一個“標準”模式。其基本理念來自於本文所提及的Kieran Lord。)

Actor是Prefab中的主要組件;通常是提供prefabs“標識”的組件,較高級的代碼將與其經常交互。Actor使用同一對象上(有時在子類上)的其他組件—Helpers—執行工作。如果你通過Unity的菜單創建一個Button對象,它將使用Sprite和Button組件創建一個遊戲對象(用Text組件創建一個子類)。在這種情況下,Button是一個actor組件。同樣,除了附連的Camera組件之外,主攝像機一般有多個組件(GUI圖層,Flare圖層,音頻監聽器)。Camera即爲一個actor。

Actor可能需要結合其他組件才能正常工作。你可以通過使用下述在actor組件上屬性使prefab更穩健和有用:

        1)使用RequiredComponent來指示actor對於相同遊戲對象所需的所有組件。(然後你的actor總是安全地調用GetComponent,而無需檢查返回的值是否爲空。)

        2)使用DisallowMultipleComponent防止附加相同組件的多個實例。然後你的actor總是可以調用GetComponent,而無需擔心當有多個組件附加時應產生什麼行爲)。

        3)若你的actor對象有子類時,使用SelectionBase。這會使你在場景試圖更容易選擇。

1
2
3
4
5
6
7
[RequiredComponent(typeof(HelperComponent))]
[DisallowMultipleComponent]
[SelectionBase]
public class Actor : MonoBehaviour
{
   ...//
}

 

42.對隨機和模式化數據流使用Generators(雖然這不是一個標準模式,但我們發現它非常有用。)

Generator類似於隨機生成器:它是一種具有可以被調用獲取特定類型新項目的Next方法的對象。在構建期間可以操縱Generators生成各種模式或不同類型的隨機性。它們很有用,因爲它們保持生成新道具的邏輯與你需要的項目分離,從而使代碼清潔多了。

這裏有幾個實例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var generator = Generator
   .RamdomUniformInt(500)
   .Select(x => 2*x); //Generates random even numbers between 0 and 998
  
var generator = Generator
   .RandomUniformInt(1000)
   .Where(n => n % 2 == 0); //Same as above
  
var generator = Generator
    .Iterate(0, 0, (m, n) => m + n); //Fibonacci numbers
  
var generator = Generator
   .RandomUniformInt(2)
   .Select(n => 2*n - 1)
   .Aggregate((m, n) => m + n); //Random walk using steps of 1 or -1 one randomly
  
var generator = Generator
   .Iterate(0, Generator.RandomUniformInt(4), (m, n) => m + n - 1)
   .Where(n >= 0); //A random sequence that increases on average


 我們使用Generators派生障礙,改變背景色,程序性音樂,生成可能在文字遊戲中生成字母的字母序列,等等。此外,Generators在控制以非恆定間隔重複的協程方面也有效,其構造如下:

1
2
3
4
5
6
while (true)
{
   //Do stuff
    
   yield return new WaitForSeconds(timeIntervalGenerator.Next());
}

更多關於Generators的討論,請參考此發佈。

 

Prefabs和Scriptable object

43. 對任何事物使用prefabs你的場景中唯一的遊戲對象不應該是prefabs(或者prefabs的一部分),而應該是目錄。即使僅使用一次的唯一對象應該是prefabs。這使得更容易進行無需場景變換的變更。

 

44. 對prefabs之間互相鏈接;而不要對實例對象互相鏈接。當prefab放置到某個場景中時,維護prefabs鏈接;對於實例鏈接則無需保持。儘可能的使用Prefab之間的鏈接可以減少場景創建的操作,並且減少場景的修改。

如有可能,在實例對象之間自動創建鏈接。如果你需要在實例之間鏈接,則在程序代碼中創建鏈接。例如,玩家prefab在啓動時需要把自己註冊到GameManager,或者GameManager可以在啓動時去查找玩家prefab。

 

45. 若需要添加其他腳本,不要將Mesh放置在prefabs的根節點上。當你需要從Mesh創建一個prefab時,首先創建一個空的GameObject作爲父對象,並用來做根節點。把腳本放到根節點上,而不要放到Mesh節點上。通過採用這種方法,更容易替換Mesh,而不會丟失所有你在Inspector中設置的值。

 

46. 對共享配置數據,而不是prefabs使用Scriptableobject

若是如此:

        1)場景較小

      2)你不能錯誤地對單個場景(prefab實例上)進行更改。

      

47. 對level數據使用scriptableobjects。關卡數據常存儲在XML或JSON中,但使用scriptable objects具有一些優點:

      1)它可以在Editor中編輯。這樣更容易驗證數據,並且對非技術領域的設計師更友好。此外,你可以使用自定義編輯器使編輯更容易。

        2)你不必操心讀取/編寫和解析數據。

      3)它更容易分拆和嵌套,同時管理生成的assets,因此是從構建塊,而非大型配置組成關卡。

 

48. 使用scriptable objects配置inspector中的行爲。Scriptableobjects通常與數據配置相關,但它們也支持將“方法”用作數據。

考慮一個場景,其中你有一個Enemy類型,並且每個敵人有一堆SuperPowers。如果它們在Enemy類中,你可以創建這些常規類,並生成一個列表……若沒有自定義編輯器,你便無法在inspector中設置一個包含不同superpowers的列表(每個具有自身屬性)。但如果你創建這些super powers assets(將它們實現爲ScriptableObjects),你就可以進行上述設置!

其構造爲:

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
public class Enemy : MonoBehaviour
{
   public SuperPower superPowers;
 
   public UseRandomPower()
   {
       superPowers.RandomItem().UsePower(this);
   }
}
 
public class BasePower : ScriptableObject
{
   virtual void UsePower(Enemy self)
   {
   }
}
 
[CreateAssetMenu("BlowFire", "Blow Fire")
public class BlowFire : SuperPower
{
   public strength;
   override public void UsePower(Enemy self)
   {
      ///program blowing fire here
   }
}

 

當遵循這一模式時,需注意以下幾點:

        1)無法可靠地使Scriptable objects抽象化。相反,需要使用具體的基類,並使用抽象方法拋出NotImplementedExceptions。此外,你也可以定義Abstract屬性,並標記應爲抽象的類和方法。

        2)Scriptableobjects是指無法序列化的通用對象。然而,你可以使用通用基類,並且只對指定所有通用對象的子類抽象化。

 

49. 使用scriptable objects對prefabs特殊化。若兩個對象的配置僅在某些屬性上不同,則通常在場景中放置兩個實例,並調整這些實例上的屬性。通常較好的做法是創建一個單獨的屬性類,它可以區別兩種類型爲一個單獨的scriptableobject類。

這可以提供更多的靈活性:

        1)你可以利用從特殊類的繼承,向不同對象類型提供更具體的特定屬性。

        2)場景設置更安全(你只要選擇正確的scriptable object,而無需調整所有屬性,便可以創建所需類型的對象)。

 3)運行期間,通過代碼更容易操縱這些對象。

 4)如果你有這兩種類型的多個實例,你就會知道當進行更改時,它們的屬性將總是保持一致。

 5)你可以將配置變量集分拆爲可以混合和匹配的集合。

下面舉出了一個關於此設置的簡要示例:

1
2
3
4
5
6
7
8
9
10
11
[CreateAssetMenu("HealthProperties.asset", "Health Properties")]
public class HealthProperties : ScriptableObject
{
   public float maxHealth;
   public float resotrationRate;
}
 
public class Actor : MonoBehaviour
{
   public HealthProperties healthProperties;
}

 

 如果特殊化類的數量較大,你可能要將特殊化類定義爲普通類,並使用鏈接到一個包含某些特殊化類的列表,這些特殊化類是鏈接到你可以獲取的適當位置的scriptable object中。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public enum ActorType
{
   Vampire, Wherewolf
}
 
[Serializable]
public class HealthProperties
{
   public ActorType type;
   public float maxHealth;
   public float resotrationRate;
}
 
[CreateAssetMenu("ActorSpecialization.asset", "Actor Specialization")]
public class ActorSpecialization : ScriptableObject
{
   public List healthProperties;
 
   public this[ActorType]
   {
       get { return healthProperties.First(p => p.type == type); } //Unsafe version!
   }
}
 
public class GameManager : Singleton 
{
   public ActorSpecialization actorSpecialization;
 
   ...
}
 
public class Actor : MonoBehaviour
{
   public ActorType type;
   public float health;
    
   //Example usage
   public Regenerate()
   {
      health
         += GameManager.Instance.actorSpecialization[type].resotrationRate;
   }
}


 

50. 使用CreateAssetMenu屬性自動向Asset/Create菜單添加ScriptableObject創建。

 

調試

51. 學習如何有效地使用Unity的調試工具。

        1)Debug.Log語句添加上下文對象以查看它們的生成位置。

        2)在編輯器中使用Debug.Break暫停遊戲(例如,當你想產生錯誤條件,並且在該幀上檢查部件屬性時,它很有用)。

        3)針對可視化調試使用Debug.DrawRayDebug.DrawLine功能(例如,當調試爲什麼沒有光影投射時,DrawRay非常有效)。

        4)針對可視化調試使用Gizmos。此外,你可以通過使用DrawGizmo屬性提供mono behaviours外部的gizmo渲染器。

        5)使用debug inspector試圖(使用inspector查看運行中的私密字段的值)。

 

52. 學習如何有效地使用調試器。詳見Visual Studio中的“調試Unity遊戲示例”。

 

53. 使用一個隨着時間的推移繪製數值圖形的可視化調試器。這對於調試物理,動畫和其他動態進程,尤其是偶然性錯誤非常有用。你將能夠從圖中找出錯誤,並能夠同時有哪些其他變量發生了變化。另外,可視化檢查也使某些異常行爲變得更明顯,比如說數值變化太頻繁,或者不具明顯原因地發生偏移。我們使用的是Monitor Components,但也有幾種可用的方案。

 

54. 使用改進的控制檯記錄。使用一個可以根據類別進行顏色編碼輸出,同時可以根據這些類別篩選輸出的編輯器擴展。我們使用的是Editor Console Pro但也有幾種可用的方案。

 

55. 使用Unity的測試工具,特別是測試算法和數學代碼。詳見Unity測試工具教程,或者使用Unity測試工具以光速進行事後單元測試。

 

56. 使用Unity的測試工具以運行“scratchpad”測試。

Unity的測試工具不僅適合正式測試,而且還可以便於進行可以在編輯器中運行,同時無需場景運行的scratch-pad測試。

 

57. 實現截屏快捷鍵。當你截屏拍照時,許多錯誤是可見的,並且更容易報告。理想化的系統應該在PlayerPrefs保持一個計數器,從而使連續截屏不會被覆蓋。截屏應保存在項目文件夾外,以避免人員將它們誤提交到存儲庫。

 

58. 實現打印重要變量快照的快捷方式。當你可以檢查的遊戲期間發生未知錯誤,這樣更容易記錄一些信息。當然,記錄哪些變量是取決於你的遊戲。實例是玩家和敵人的位置,或者AI演員的“思維狀態”(例如嘗試行走的路徑)。

 

59. 實現一些方便測試的調試選項。下面舉出了一些示例:

         解鎖所有道具。

         禁用敵人。

         禁用GUI

         讓玩家無敵。

         禁用所有遊戲邏輯。

要注意,切勿不慎提交調試選項;更改調試選項可能會迷惑團隊中的其他開發人員。

 

60. 定義一些Debug快捷鍵常量,並將它們保存到同一個位置。通常(爲方便起見)在一個位置處理Debug鍵,如同其它的遊戲輸入一樣。爲了避免快捷鍵衝突,在一箇中心位置定義所有常量。另一種方法是在某個位置處理所有按鍵輸入,無論它是否是Debug鍵。(其負面效果在於,此類可能需要引用更多的其它對象)。

 

61. 在程序網格生成時,在頂點繪製或派生小球體。這將幫助你在使用三角形和UVs以顯示網格之前,確定頂點處在期預期的位置,並且網格是正確的尺寸。

 

性能

62. 請注意關於效能原因設計和構造的通用建議。

          1)這些建議通常是基於虛構的,而不是由測試支持的。

          2)即便有時建議是由測試支持的,但測試存在錯誤。

       3)有時建議是由正確的測試支持,但它們處在不真實的或不同的環境之中。(例如,很容易展現如何比通用列表更快地使用數組。然而,在真實遊戲環境中,這種差異幾乎總是可以忽略不計。同樣,若測試適用於除目標設備以外的不同硬件時,它們的結果可能對你無意義。)

       4)有時建議是良好的,但卻過時。

       5)有時,建議是適用的。然而,存在權衡關係。航運慢速遊戲有時要好於非航運快速遊戲。而高度優化的遊戲更可能包含可以延遲航運的複雜代碼。

        效能建議可能有助於記憶,幫助你通過下述進程更快地追蹤實際問題源。

 

63. 從早期階段對目標設備進行定期測試。

不同的設備可能具有顯著不同的效能特性;不要對它們感到吃驚。越早知道問題,你就能越有效地解決問題。

 

64. 知道如何更有效地使用效能評測器以追蹤導致效能問題的原因。

          如果你剛接觸效能分析,請參閱效能評測器簡介。

        學習如何針對精細度分析來定義你自己的框架(使用Profiler.BeginFrame Profiler.EndFrame)。

        學習如何使用平臺特定的效能分析,如iOS系統的內置效能分析器。

        學習分析內置玩家中的文件,並顯示效能分析器中的數據。

 

65. 在必要時,使用自定義分析器進行更準確的分析。有時,Unity的效能分析器無法清楚地展示發生的事物;它可能消耗完分析框架,否則深度分析可能減慢遊戲速度,以致於測試沒有意義。我們對此使用自有的內部分析器,但應該可以在Asset Store中找到其他替代工具。

 

66. 衡量效能增強的影響。

當你作出更改提升效能時,衡量它確保該更改着實有效。如果這個更改是不可衡量或凌亂的,請撤銷更改。

 

67. 不要編寫可讀度減低的代碼,以保證更佳的效能。除非有下述任一情況:

        你碰到了一個問題,使用效能分析器識別出問題源,同時相較於可維護性損失,獲得的增益足夠高。或者你清楚自己在做什麼。

 

命名規範和目錄結構

68.遵循一個命名規範和目錄結構。保持命名和目錄結構的一致性可以方便查找,並明確指出具體內容。

你很有可能想要創建自己的命名規範和目錄結構。下面舉出了一個例子。

命名的一般原則

1.按事物本身命名。例如,鳥應該稱爲Bird

2. 選擇可以發音,方便記憶的名字。如果你在製作一個與瑪雅文化相關的遊戲,不要把關卡命名爲QuetzalcoatisReturn

3. 保持一致性。如果你選擇了一個名字,就堅持用它。不要在一處命名buttonHolder,而在其它位置命名buttonContainer。

4. 使用Pascal風格的大小寫,例如ComplicatedVerySpecificObject。

不要使用空格,下劃線,或者連字符,但有一個例外

(詳見爲同一事物的不同方面命名一節)。

5. 不要使用版本數字,或者表示其進度的名詞(WIP,final)。

6. 不要使用縮寫:DVamp@W應該寫成DarkVampire@Walk。

7. 使用設計文檔中的術語:如果文檔中將一個動畫命名爲Die,則使用DarkVampire@Die,而不要用DarkVampire@Death。

8. 保持細節修飾詞在左側:DarkVampire,而不是VampireDark;PauseButton,而不是ButtonPaused。舉個例子,在Inspector中查找PauseButton,這要比所有按鈕都以Button開頭更加方便。(很多人傾向於相反次序,認爲這樣可以使名稱自然分組。然而,名稱不是用來分組的,目錄纔是。名稱是用於在同一類對象中快速辨識的。)

9.某些名稱形成一個序列。在這些名稱中使用數字。例如PathNode0, PathNode1。永遠從0開始,而不是1。

10. 對於非序列的情況,不要使用數字。例如 Bird0, Bird1, Bird2,本應該是Flamingo, Eagle,Swallow。

11.爲臨時對象添加雙下劃線前綴,例如__Player_Backup

 

命名同一事物的不同方面

在覈心名稱與描述“對象”的事物之間添加下劃線。例如:

            GUIbuttons states EnterButton_Active,EnterButton_Inactive

            Textures DarkVampire_Diffuse,DarkVampire_Normalmap

            Skybox JungleSky_Top,JungleSky_North

            LODGroups DarkVampire_LOD0, DarkVampire_LOD1

不要只是爲了區分不同類型的項目而使用此類規範,例如Rock_Small, Rock_Large,本應該是SmallRock,LargeRock。

 

結構

場景,項目目錄和腳本目錄的結構應遵循一個類似的模式。下面列舉了一些精簡示例。

目錄結構

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
MyGame
   Helper
      Design
      Scratchpad
   Materials
   Meshes
      Actors
         DarkVampire
         LightVampire
         ...
      Structures
         Buildings
         ...
      Props
         Plants
         ...
      ...
   Resources
      Actors
      Items
      ...
   Prefabs
      Actors
      Items
      ...
   Scenes
      Menus
      Levels
   Scripts
   Tests
   Textures
      UI
      Effects
      ...
   UI
MyLibray
   ...
Plugins
SomeOtherAsset1
SomeOtherAsset2
...


場景結構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Main
Debug
Managers
Cameras
Lights
UI
   Canvas
      HUD
      PauseMenu
      ...
World
   Ground
   Props
   Structures
   ...
Gameplay
   Actors
   Items
   ...
Dynamic Objects


腳本目錄結構

1
2
3
4
5
6
7
8
9
Debug
Gameplay
   Actors
   Items
   ...
Framework
Graphics
UI
...


轉自:http://gad.qq.com/program/translateview/7167991
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章