如何在Unity中用F#寫Coroutine
March 25, 2017
一直到Unity 5.6的版本,Unity都是一個單執行緒(主緒)的引擎,雖然從物理到繪圖的部份已經改成了多緒,但執行遊戲邏輯時一直都是以單執行緒的方式進行。但在某些邏輯面的處理,若沒有特別的方式規避,則會卡住那唯一的主緒,造成整個遊戲的頓感。
用現有的Update其實開發者是可以自行撰寫狀態加上計時器解決此問題,但Unity很早就知道這樣太麻煩開發者,而且沒有統一的寫法很容易出現錯誤。因此,Unity特地將Coroutine的機制加入,強調它可以非同步(Async)的執行,讓唯一的主緒不會被卡住。利用了yield return回傳可以被轉型成IEnumerator型別的寫法,讓開發者可以良好的設計不會卡住主緒的邏輯撰寫,藉此完成遊戲規則、表現面的展現。
這個機制已出現了七、八年以上的時間了,從最初多數的開發者不清楚這是什麼樣的機制到現在儼然已成為Unity開發者最基本的概念,可以說這個年代沒有開發者不會寫、不會用Coroutine。但,當Coroutine的語法是以F#的方式撰寫時,它會是怎麼表現呢?
在Google上查詢後會發現相關的資料不多,唯一的詢問和解答是在一篇2013年的Stackoverflow討論中,那時抱著嘗試F#心態已經有試著將它記錄在Gist中。但那時候的嘗試是點對點的嘗試,抱持著C#的語法轉換到F#時會長成什麼樣子的心態,但沒有記錄下來,時過這麼多年後,正式想將開發的語法轉換至F#時,又碰到了這樣的問題,因此 趁著這次維持部落格撰寫習慣的當下,特地用此篇記錄如何用F#語法表現出Coroutine的撰寫。
Coroutine在Unity的設計下就是一個以IEnumerator型別做趨動的機,不討論它內部的狀態處理,純粹看IEnumerator的函意,它就是代表多個的集合並和IEnumerable有著關聯性。在F#中,可以利用seq computation expressions獲取集合(IEnumerable)。seq實際上就是.Net中的IEnumerable,只是在F#有個不一樣的名稱而以。也因為是IEnumerable的關係,可以塞入無限多的元素到此集合中(雖然是說無限,但還是受限於實體的記憶體容量等),而它具有惰性求值(Lazy Evaluation)的特性,不會讓無限的元素在使用當下展開,而是可以延遲去處理。
從以下這段程式碼片段可以看到若是用Coroutine的方式讓物件進行基本移動時是如何撰寫的。
整個seq computation expressions回傳的型別要強制轉換成IEnumerator,此處沒有額外處理異常(除了列印出錯誤和回傳空集合外,也沒有好的處理方式)。這就是基本的寫法,而在呼叫端,則是直接將此method置於最後一行。這裡的概念是利用Start本身是可以回傳IEnumerator的型別,用此回傳值強制編譯時將Start轉成自身為Corotuine的函式。
而物件的移動利用while true的寫法產生了無限多的元素,但配合Lazy Evaluation的Coroutine機制,可以一個個取出進行處理。
多數的Coroutine寫法是屬於這個範圍的,但有些寫法下,只會有有限的步驟。這樣的寫法在網路上沒有明確的範例,因此在自行模索後,修改了無限元素的集合,寫成了以下表現出數個步驟的Cut Scene邏輯寫法。
產生數個只有一個元素的集合後,再利用一個seq將每個集合中的元素取出合併,展現出一段段的步驟。看樣來有些怪怪的,但在運行上完全符合一步步的展現方式。在沒有任何範例可遵循的情況下,會先用此方式進行有限步驟Coroutine的撰寫,直到有找到或是有建議怎麼寫更適合時,再做調整。
利用F#的語法要試著囊括所有Unity在C#的機制理應是可行的,但實際上在撰寫時才會發現一對一轉換(one-to-one mapping)在多數的情況下雖可行,但不是最佳的選擇,而在少數情況下,根本就沒有辦法進行。因此,嘗試找尋就是唯一的方法,但頗花時間的,所以將之記錄下來,不要浪費已投入的時間。
Coroutine的寫法只是其中一環,另一個也在找尋的是如何將一些已是以事件或是Coroutine呈現的功能用F#轉換成UniRx用法的寫法。但目前的進展有點卡住,有些外掛雖然是可以用Coroutine的方式撰寫,但包的方式很奇特,需要花很多時間才能理解。有任何進展再行記錄。