unity優化 —腳本優化2

     接着前面腳本優化講,我們還是深入的講解腳本該如何優化。在其它的一些情景中,我們希望組件或對象被禁用後,他們能夠離玩家足夠遠,這樣他們可能是勉強可見的,但是又離對象特別遠。一個貼切的例子是AI生物的巡邏,我們可以在一定的距離內看見,但是我們不需要它們處理任何事情。下面的代碼是一個簡單的程序,用來定期檢測到目標的總距離並且如果靠的太遠就禁用自身。

     [SerializeField] GameObject _target;
     [SerializeField] float _maxDistance;
    [SerializeField] int _coroutineFrequency;
    void Start() {
       StartCoroutine(DisableAtADistance());
   }
    IEnumerator DisableAtADistance() {

    while(true) {
             float distSqrd = (Transform.position -
           _target.transform.position).sqrMagnitude;
         if (distSqrd < _maxDistance * _maxDistance) 

         {
              enabled = true;
         } 

       else 

       {
            enabled = false;
       }

     for (int i = 0; i < _coroutineFrequency; ++i) {
       yield return new WaitForEndOfFrame();
       }
    }
}

我們應該在Inspector面版分配玩家對象(或任何我們想比較對象)給_target 字段,在_maxdistance定義最大距離,通過_coroutineFrequency屬性在每次協程被調用時修改頻率。任何時候對象和指定對象間的距離超出最大距離,它將變得不可見。在最大距離內就重新啓用。一個小的優化是用距離的平方而不是用距離本身,這會方便我們的下一個提示。可以說,CPU比較擅長浮點數乘法,但是計算平方根就不是很好了。每次我們調用Vector3的magnitude屬性去計算距離或者用Distance()方法的時候,我們要求它進行一個平方根運算(按照畢達哥拉斯定理)。與許多其他類型的矢量數學計算相比,它可以花費大量的處理器開銷。

不過,Vector3還提供了一個sqrMagnitude 屬性,是用來計算距離的平方的。這讓我們執行了基本相同的比較而不用花費高昂的平方根代價,而如果我們直接寫距離的平方會顯得太過冗長;或者可以這麼描述這個數學問題,A的模長小於B的模長,那麼A的平方同樣小於B的平方。
舉個例子,代碼如下:
float distance = (transform.position –other.transform.position).Distance();
if (distance < targetDistance) {
// do stuff
}

這可以被下面的替代得到相同的結果:
float distanceSqrd = (transform.position –other.transform.position).sqrMagnitude;
if (distanceSqrd < targetDistance * targetDistance) {
// do stuff
}

因爲浮點精度(影響不大)其結果幾乎是相同的。我們很可能會失去一些我們本來可以使用的平方根值的精度,因爲該值被調到一個不同精度表示的數的範圍;它可能更準確更接近的表示該數,也可能減少一定的精度。因此,比較的結果並不完全相同,但是在大多數情況下,它是足夠接近而不會出錯的,並且在這種情況下獲得的效率提升是足夠大的。如果這個小的精度損失對你不重要,這個性能的技巧是應該被考慮的。然而,如果精度是非常重要的(如運行一個準確的大型星系空間模擬),你就應該尋找其他地方的性能改進。請注意,該技術可用於任何平方根的計算,而不僅僅是距離。這是你可能遇到的最常見的例子,來明確Vector3中sqrMagnitude 屬性的重要性-------Unity技術故意暴露給我們的一個屬性。前面講到了一點分發的原理,這裏我還是強調一下吧。優化更新的另一種方法是不使用update(),更準確的說是隻使用一次。當Unity調用 Update() ,它涉及到橋接遊戲對象本身和管理對象的表現,這可能是一個代價高昂的任務。因此,我們可以通過限制它的橋接最大程度的限制它的開銷。在我們所有的自定義組件中,我們可以用一個最高的管理類來調用我們自己定義的執行Update功能的函數。事實上,許多Unity的開發人員更喜歡這種方法作爲他們項目的開始,以便更精細準確的對整個系統進行控
制管理,比如菜單暫停和冷卻時間操作的效果。所有想要集成這樣一個系統的對象必須有一個共同的入口點。我們可以通過一個接口類實現這個。接口基本上建立了一個契約,任何繼承該接口的類必須實現一系列的方法。換句話說,如果我們知道對象實現了接口,然後我們可以確定什麼方法是可用的。在C#中,每個類只能有一個基類,但是它們可以繼承多接口(這避免了C++程序員可能會遇到的“deadly diamond of death”問題)。
下面的接口定義就足夠了,只需要派生類實現一個單一的方法:
public interface IUpdateable {
void OnUpdate(float dt);
}

接着,我們要定義MonoBehaviour類,讓它繼承該接口:
public class UpdateableMonoBehaviour : MonoBehaviour, IUpdateable
{
public virtual void OnUpdate(float dt) {}
}

UpdateableMonoBehaviour 的OnUpdate()方法需要檢索當前的時間增量dt,節省我們一些不必要的Time.deltaTime的調用。我們還聲明瞭虛方法,允許派生類來定義它。然而,正如你所知道的,Unity會自動調用名字爲Update()的方法,但是我們自定義的update 用了一個不同的名字,我們需要在合適的時間對這些方法進行調用,比如在一些GameLogic的管理類。在這個組件的初始化過程中,我們應該做些什麼來通知我們的遊戲邏輯的它存在和它的銷燬,以便於知道何時啓用和停止該onupdate()方法。在下面的例子,我們假設我們的遊戲邏輯類是單例組件,正如在題爲“單例”組件的章節中定義的那樣,定義適當的靜態函數進行註冊和註銷(雖然它可以很容易地使用我們的信息系統!)。將MonoBehaviours與這個系統掛鉤,最合適的地方是在start()和ondestroy():
void Start() {
GameLogic.Instance.RegisterUpdateableObject(this);
}
void OnDestroy() {
GameLogic.Instance.DeregisterUpdateableObject(this);
}

最好使用start()方法完成這個任務,使用Start()意味着我們可以確定所有需要提前存在的對象已經在這一刻在Awake()方法的調用中存在了。通過這種方式,任何關鍵的初始化工作都將在我們開始調用它的更新之前就已經完成了。注意,因爲我們在MonoBehaviour基類中使用Start()方法,如果我們在派生類定義Start()方法,它將有效的重寫了基類的定義,並且Unity會自動調用派生類的start()方法。聰明的辦法是實現一個Initialize() 的虛方法,派生類可以重寫它定製的初始化行爲,而不會干擾我們組件的GameLogic 對象基類的任務通知。

void Start() {
GameLogic.Instance.RegisterUpdateableObject(this);
Initialize();
}
protected virtual void Initialize() {
// derived classes should override this method for initialization code
}

我們應該儘可能的讓這個過程自動完成,爲了讓我們不必再爲我們定義的每一個新的組件重新執行這些任務。當一個類繼承自updateablemonobehaviour類,在合適的時候調用OnUpdate()方法就將是安全的。最後,我們需要實現遊戲邏輯類。無論是單例組件還是獨立組件,該實現都是相同的,也不管它是不是使用信息系統。無論哪種方式,作
爲iupdateableobject對象我們updateablemonobehaviour類必須註冊和註銷,遊戲邏輯類必須使用自己的update()回調,來迭代每個註冊的對象調用OnUpdate()函數。

public class GameLogic : SingletonAsComponent<GameLogic> 

{
                  public static GameLogic Instance 

                  {
                            get { return ((GameLogic)_Instance); }
                            set { _Instance = value; }
                  }
               List<IUpdateableObject> _updateableObjects = new List<IUpdateableObject>();
              public void RegisterUpdateableObject(IUpdateableObject obj) 

               {
                     if (!_Instance._updateableObjects.Contains(obj)) {
                          _Instance._updateableObjects.Add(obj);
                }

             public void DeregisterUpdateableObject(IUpdateableObject obj) {
                     if (_Instance._updateableObjects.Contains(obj)) {
                         _Instance._updateableObjects.Remove(obj);
                    }


             }
               void Update() {
                        float dt = Time.deltaTime;
                         for(int i = 0; i < _Instance._updateableObjects.Count; ++i) {
                      _Instance._updateableObjects[i].OnUpdate(dt);
             }
       }
}

如果我們確保所有我們自定義的MonoBehaviours 類繼承自UpdateableMonoBehaviour 類,我們就有效的用了一個 Update()的調用替代了N個Update()的調用,以及N個虛方法的調用。這可以節省大量的性能開銷,因爲即使我們調用虛擬函數,我們仍然需要保持絕大多數的更新行爲在託管代碼,儘可能避免本地託管橋。取決於你進行你項目的深度,這樣的變化是非常艱鉅的,耗時的,當子系統更新的時候會引入大量的bug,從而使用了一個完全不同的依賴集。但是如果你有時間,它的好處也要超過你的風險。在場景中用用一組對象進行測試是比較明智的,這樣可以通過場景文件驗證你的
好處是否超過你的風險。

   

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