2017年7月21日 星期五

[效能調教] 十個提升遊戲執行效率的 Script 撰寫建議 by Playdead (Part 1)


偶然翻到 INSIDELIMBO 開發商 Playdead Unite 2016 大會中發表的演講影片簡報檔
發現 28:34 開始的講題【Scripting Performance】非常有意思,特此分享給大家。

在這段影片中,Playdead 的工作人員用開發 Inside 時的經驗,
分享了他們 10個可以讓 C# Script 執行效率更佳的建議。

Part 2. 連結

那就開始囉 =====================

1、Reduce Vector operations (減少 Vector 的運算)

1
2
3
4
5
6
7
8
    void before()
    {
        var lastPos = transform.position;
        transform.position = lastPos
                           + wantedVelocity * speed * speedfactor
                           * Mathf.Sin(someOtherFactor)
                           * drag * friction * Time.deltaTime;
    }

1
2
3
4
5
6
7
8
    void after()
    {
        var lastPos = transform.position;
        transform.position = lastPos
                           + wantedVelocity * (speed * speedfactor
                           * Mathf.Sin(someOtherFactor)
                           * drag * friction * Time.deltaTime);
    }

在 after() 與 before() 最大的差異,在於他將 speed、speedfactor、someOtherFactor、drag、friction及Time.deltatime 這些 float 變數 都先計算完後再跟 lastPos及 wantedVeloctiy 這兩個Vector3 變數做運算。 這樣的目的是 「減少在程式中進行向量值的計算次數 」。




  • 第一次 :wantedVelocity *= speed
  • 第二次 :wantedVelocity *= speedfactor 
  • 第三次 :wantedVelocity *= Mathf.Sin(someOtherFactor) 
  • 第四次 :wantedVelocity *= drag
  • 第五次 :wantedVelocity *= friction 
  • 第六次 :wantedVelocity *= Time.deltaTime 
  • 第七次 :wantedVelocity += lastPos

  • 舉例來說,在 before() 中總共進行了七次的向量計算:
    
    同樣結果,在 after() 僅進行了兩次的向量計算:

    • 第一次 :wantedVelocity *= speed*speedfactor* ...Time.deltaTime
    • 第二次 :wantedVelocity += lastPos

    2、Use cached Transforms (製作並使用 Transforms 快取)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
        private Transform _transform;
        void Awake()
        {
            _transform = GetComponent<Transform>();
        }
    
        public void after()
        {
            var lastPos = _transform.position; //cached in “void Start()”
            _transform.position = lastPos
                                  + wantedVelocity * (speed * speedfactor
                                  * Mathf.Sin(someOtherFactor)
                                  * drag * friction * Time.deltaTime);
        }
    
        void before()
        {
            var lastPos = transform.position;
            transform.position = lastPos
                               + wantedVelocity * (speed * speedfactor
                               * Mathf.Sin(someOtherFactor)
                               * drag * friction * Time.deltaTime);
        }
    

    在上面的程式碼中,在 Start()階段將 Transform 這個 Class 存到 _transform 之中。
    之後當有需要用到 transforms 時都直接從取_transforms變數。

    按照 Playdead 在影片中口述解釋,直接向Unity調用 transform 時(如下程式碼)
    
    
     var lastPos = transform.position;

    Unity 為了安全起見並不單純的回傳 transform資料而已,
    所以為了提升效能我們可以在 start() 階段先儲存一個快取用的 transform ,
    減少反覆呼叫時多餘的浪費。

    P.S 這部份我原先看不懂怎麼製做快取,後來直到 Google 到 ...【這篇


    3、Use localPosition (if possible) (盡量使用 localPosition )

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
        private Transform _transform;
        void Awake()
        {
            _transform = GetComponent<Transform>();
        }
        void after()
        {
            var lastPos = _transform.localPosition;
            _transform.localPosition = lastPos
                                 + wantedVelocity * (speed * speedfactor
                                 * Mathf.Sin(someOtherFactor)
                                 * drag * friction * Time.deltaTime);
        }
    
    
        void before()
        {
            var lastPos = _transform.position; //cached in “void Start()”
            _transform.position = lastPos
                                  + wantedVelocity * (speed * speedfactor
                                  * Mathf.Sin(someOtherFactor)
                                  * drag * friction * Time.deltaTime);
        }

    應該不難想像用 localPosition 會比起時用 position 來的省效能,因為 Unity 平時是儲存LocalPosition的Data,然後有需要使用世界座標時再轉換依照子母層級作轉換。
    因此直接使用LocalPosition  時可以省去子母階層的相關計算。

    P.S01 雖然是這麼講...但我自己的實驗沒有差多少效能耶@@
    P.S02 但是在 Playdead 的簡報中,效能的提升非常顯著 (如下圖)
    P.S03 也許發佈成執行檔版本才會有所差異


    4、Reduce engine calls (減少向 Unity 查找資料的頻率 )

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
        public Vector3 cachedLocalPosition = Vector3.zero;
        void Awake()
        {
            _transform = GetComponent<Transform>();
        }
        public void after()
        {
            cachedLocalPosition += wantedVelocity * (speed * speedfactor
                                 * Mathf.Sin(someOtherFactor)
                                 * drag * friction * Time.deltaTime);
            _transform.localPosition = cachedLocalPosition;
        }
        public void before()
        {
            var lastPos = _transform.localPosition;
            _transform.localPosition = lastPos
                               + wantedVelocity * (speed * speedfactor
                               * Mathf.Sin(someOtherFactor)
                               * drag * friction * Time.deltaTime);
        }
    

    Playdead 提醒大家盡量減少向引擎查找資料。
    例如在上面的程式碼中將 before() 中的
    var lastPos = _transform.localPosition;

    在 after() 中的改為額外透過下面這個全域變數作儲存
    public Vector3 cachedLocalPosition = Vector3.zero;

    這樣的好處是當我們要索引物件的座標時,
    不用多一層透過 Transform 這個 Class 來索取座標。(ㄜ 這樣講應該沒問題吧?)


    5、Hmmm… Are getters and setters slow? ( 使用 get、set 會有額外耗能)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class ScriptPerformanceTest : MonoBehaviour
    {
        public float speed { get; set; }
        public float speedfactor { get; set; }
        public float someOtherFactor { get; set; }
        public float drag { get; set; }
        public float friction { get; set; }
    
    }
    

    1
    2
    3
    4
    5
    6
    7
    8
    9

    public class ScriptPerformanceTest : MonoBehaviour
    {
        public float speed;
        public float speedfactor;
        public float someOtherFactor;
        public float drag;
        public float friction;
    
    }
    

    C# 中提供了非常實用的 getset ,讓我們可以更自由的去設定變數的可存取範圍。
    但是如果非必要的情況下可以減少這工具的使用,可以再省下些效能唷!



    ==============================

    Part 2. 連結

    1 則留言: