2017年6月9日 星期五

[Unity 範例] Unity 的 Coroutine 應用範例 (以物件移動為例)


本範例檔以【滑鼠點擊控制物件移動】提供兩種不同的寫法作為範例比較,供大家參考 Coroutine (協同程序)的應用。本文的移動方式是使用SmoothDamp(平滑移動)

線上玩
範例檔下載 (使用update)
範例檔下載 (使用Coroutine)


延伸閱讀: Unity3D製作計數器「StartCoroutine應用」協程Coroutine簡介

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

使用 Update() 的作法... (專案下載)

main.cs

  1. 抓取 unity 場景上有個名叫 sphere (1) 的物件上的 sphere.cs (main.cs:第13行)
  2. 當我們 按下滑鼠鍵 的時候 (main.cs:第20行),會取得 滑鼠座標  (main.cs:第22行)
  3. 並執行 sphere (1)物件的 sphere,cs 這個script的方法 movto(pos) (main.cs:第23行)

 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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class main : MonoBehaviour
{
    sphere selected;
    GameObject click;

    // Use this for initialization
    void Start()
    {
        selected = GameObject.Find("sphere (1)").GetComponent<sphere>();
        click = GameObject.Find("click");
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            Vector3 pos = Input.mousePosition;
            selected.moveto(Camera.main.ScreenToWorldPoint(pos));
            click.transform.position = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        }
    }

}

sphere.cs

  1. 物件內的 update() 方法裡面,放入moving()方法。(sphere.cs:第20行)
  2. 在 moving() 中判斷全域變數 isMove 是否為 true。 (sphere.cs:第32行)
  3. 如果 isMove 是 true 的狀態下,才繼續執行後續程式碼。
    如果 isMove 是 false 的狀態下,直接跳出。
  4. 所以說在遊戲進行中會不斷檢查 isMove 是否為 ture/false。 
  5. 物件的全域方法  movto(vector3) 被執行時會將 isMove 改為 ture。 (sphere.cs:第27行)
  6. 並且設定該物件欲移動的目標 goalPos  (sphere.cs:第26行)
  7. 這時因為 isMove 變成 true 了,所以 sphere.cs:32行 以後的程式碼將會被執行。
  8.  sphere.cs:35行 計算了目前與目標距離的差距
  9.  sphere.cs:38行 檢查差距是否高於0.025f,如果高於的話則執行「移動物件的代碼」
  10.  sphere.cs:41行 就是「移動物件的代碼」,當他被執行就會讓物件朝目標前進。
  11.  sphere.cs:42行 這裡加入了 return ,還沒到達目標時不會執行 sphere.cs:46~47行 
  12.  sphere.cs:46行 當物件離目的地距離非常近的時候,就直接把他送到目標位置。
  13.  sphere.cs:47行 都已經到目標位置了,把isMove 改為 false,關閉移動。
 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
44
45
46
47
48
49
50
51
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class sphere : MonoBehaviour
{
    public float smoothTime = 0.25F;
    public Vector3 velocity = Vector3.zero;
    public bool isMove;

    Vector3 goalPos;

    void Start()
    {
        isMove = false;
    }

    void Update()
    {
        moving();
    }

    public void moveto(Vector3 pos)
    {
        GetComponent<Renderer>().material.color = Random.ColorHSV(0f, 1f, 1f, 1f, 1f, 1f, 0.5f, 0.5f);
        goalPos = pos;
        isMove = true;
    }

    void moving()
    {
        if (isMove == false) return;

        //計算與目標之間的距離差並儲存到dist
        float dist = Vector3.Distance(transform.position, goalPos);

        // 當「物件位置」與「目的位置」距離差距超過0.1f距離單位以上時,if內的程式買將重複執行
        if (dist > 0.025f)
        {
            //Vector3.SmoothDamp (起始位置、目標位置、當前速度、抵達時間)
            transform.position = Vector3.SmoothDamp(transform.position, goalPos, ref velocity, smoothTime);
            return;
        }

        // 當「物件位置」與「目的位置」距離差距低於0.1f距離單位時...
        transform.position = goalPos;
        isMove = false;
    }
}

心得:

原則上 Unity 官方API文件都是用Update作範例的 (例如這篇Vector3.SmoothDamp),用起來簡單直覺。但這樣的寫法有個缺點...就是必須額外做個「開關變數」,來控制「移動程序」執行與否。因此 Unity 在執行時將「不斷檢查開關變數」 狀態,似乎是個沒效率的一種作法。

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

使用 Coroutine 的作法... (專案下載)

main.cs 

  1. unity 場景上有個名叫 sphere (1) 的物件並該物件掛載 sphere.cs (main.cs:第13行)
  2. 當我們 按下滑鼠鍵 的時候,會取得 滑鼠座標  (main.cs:第13行)
  3. 並執行 sphere (1)物件的 sphere,cs 這個script的方法 movto(pos) (main.cs:第13行)

sphere.cs

  1. 在全域變數中宣告一個迭代器(IEnumerator) 名叫 moveCoroutine  (sphere.cs:第9行)
  2. 並在物件的start()階段,指定該迭代器對應的方法 moving() (sphere.cs:第9行)
  3. 物件的全域方法 moveto(vector3) 被 main.cs 呼叫執行時... (sphere.cs:第17~28行)
  4. 停止正在執行的 moveCoroutine (sphere.cs:第20行)
  5. 指定 moveCoroutine 等於moving方法,並給予滑鼠座標 (sphere.cs:第21行)
  6. 開始執行 moveCoroutine,也就是執行moving() (sphere.cs:第22行)

sphere.cs:moving(vector3)

  1. 當moving 被執行時(透過startCoroutine())  (sphere.cs:第22行)
  2. 首先會檢查物件與目標位置之間的距離差距 (sphere.cs:第33行)
  3. 若差距低於0.025f個單位,進入while迴圈 (sphere.cs:第37行)
  4. 在while迴圈中在更新一次與目標的距離差距 (sphere.cs:第40行)
  5.  sphere.cs:第42行 被執行就會讓物件朝目標前進。
  6. 當程式執行到 sphere.cs:第43行 時,會拋出這個【while迴圈】及【moving】。
  7. 直到要再執行update()時,會再次進入這個while迴圈 sphere.cs:36行
  8. 因為是回到這個迴圈之中,所以 sphere.cs:第32、33行不會再次執行。
  9. 所以是透過 yield return null;這段程式碼,讓while可以跳出後再update階段再進入。
  10. 當物件與目標位置距離差距低於0.025f時,會跳出while迴圈(sphere.cs:第37行)
  11. 並執行 sphere.cs:第47行 直接把物件送到目標位置。
  12. moving這個方法執行完畢。
 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
44
45
46
47
48
49
50
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class sphere : MonoBehaviour
{
    public float smoothTime = 0.25F;
    public Vector3 velocity = Vector3.zero;
    IEnumerator moveCoroutine;

    void Start()
    {
        //指定 moveCoroutine 使用的迭代器與參數
        moveCoroutine = moving(Vector3.zero);
    }

    public void moveto(Vector3 pos)
    {
        //停止正在進行的 moveCoroutine ,如果沒有預先指定(即14行)會無法通過編譯
        StopCoroutine(moveCoroutine);

        //指定 moveCoroutine 使用的迭代器與參數 --- IEnumberator moving(vector3,GameObject)
        moveCoroutine = moving(pos);

        //執行 moveCoroutine
        StartCoroutine(moveCoroutine);
    }

    IEnumerator moving(Vector3 pos)
    {
        //這段程式碼只執行一次
        float dist = Mathf.Infinity;
        GetComponent<Renderer>().material.color = Random.ColorHSV(0f, 1f, 1f, 1f, 1f, 1f, 0.5f, 0.5f);

        // 當「物件位置」與「目的位置」距離差距超過0.1f距離單位以上時,while內的程式買將重複執行
        while (dist > 0.025f)
        {
            //計算與目標之間的距離差並儲存到dist
            dist = Vector3.Distance(transform.position, pos);
            //Vector3.SmoothDamp (起始位置、目標位置、當前速度、抵達時間)
            transform.position = Vector3.SmoothDamp(transform.position, pos, ref velocity, smoothTime);
            yield return null;
        }

        // 當「物件位置」與「目的位置」距離差距低於0.1f距離單位以上時,下面程式碼會執行後跳出 moving 迭代器
        transform.position = pos;
    }

}

說明&心得:

改用 Coroutine (協程) 的版本中,我們是透過 StartCoroutine(moveCoroutine); 讓moving()立即被執行,並且在執行 yield return null; (sphere.cs:第42行) 時拋出迴圈。在被拋出後會在update階段再次進入迴圈。如果在執行moving()時沒有被拋出的話,moving就結束執行了。

我個人認為在這個功能範例中,改用 Coroutine (協程) 的版本使用起來更為自由、更少一些步驟。(開啟關閉容易且不需要開關變數來控制...)

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

最後提供簡化版寫法 

  • 不須額外宣告 moveCoroutine ,直接透過參數使用。
  • 缺點是這種寫法只能傳送一個參數給moving()。
  • 並且這種寫法在VSCODE中,無法顯示moving被誰引用。

 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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class sphere : MonoBehaviour
{
    public float smoothTime = 0.25F;
    public Vector3 velocity = Vector3.zero;

    public void moveto(Vector3 pos)
    {

        StopCoroutine("moving");
        StartCoroutine("moving", pos);

    }

    IEnumerator moving(Vector3 pos)
    {
        //這段程式碼只執行一次
        float dist = Mathf.Infinity;
        GetComponent<Renderer>().material.color = Random.ColorHSV(0f, 1f, 1f, 1f, 1f, 1f, 0.5f, 0.5f);

        // 當「物件位置」與「目的位置」距離差距超過0.1f距離單位以上時,while內的程式買將重複執行
        while (dist > 0.025f)
        {
            //計算與目標之間的距離差並儲存到dist
            dist = Vector3.Distance(transform.position, pos);
            //Vector3.SmoothDamp (起始位置、目標位置、當前速度、抵達時間)
            transform.position = Vector3.SmoothDamp(transform.position, pos, ref velocity, smoothTime);
            yield return null;
        }

        // 當「物件位置」與「目的位置」距離差距低於0.1f距離單位以上時,下面程式碼會執行後跳出 moving 迭代器
        transform.position = pos;
    }

}



最後在補充...
我自己測試了同時兩千五百個物件在執行下,用update還是Coroutine哪個省效能。
結果圖先奉上,晚點在細說明及提供我測試的專案。

看起來跟這篇說的一樣..Is Using Coroutines Actually Faster Than Update?

何時該用 Coroutine 何時該用Update,大家覺得呢 ^^


Update()


Coroutine()





3 則留言: