參考博客Unity 協程原理探究與實現
Godot 3.1.2版本尚不支持C#版本的協程,仿照Unity的形式進行一個協程的嘗試
但因為Godot的輪詢函數為逐幀的_Process(float delta)
和固定時間的_PhysicsProcess(float delta)
, 不像untiy可以在同一函數中同時取得邏輯時間和物理時間,一些時間誤差還是可能有的。
基本協程執行
協程原理見上面的參考博客,即通過在游戲輪詢函數中進行迭代,通過迭代器的yield語句將邏輯進行分段執行。
首先把游戲引擎的輪詢函數接入
// GDMain.cs
// 把這個腳本掛到一個節點上啟動即可
using Godot;
public class GDMain : Node
{
public override void _Process(float delta)
{
CoroutineCore.Update(delta);
}
public override void _PhysicsProcess(float delta)
{
CoroutineCore.FixedUpdate(delta);
}
}
// CoroutineCore.cs
using Godot;
using System.Collections;
public static class CoroutineCore
{
private static s_It;
public static void StartCoroutine(IEnumerator e)
{
//這里就產生了一個問題,第一次在下一幀時執行,可以做相關邏輯調整
s_It = e;
}
public static void Update(float delta)
{
InnderDo(delta, false);
}
public static void FixedUpdate(float delta)
{
InnderDo(delta, true);
}
private static void InnderDo(float delta, bool isFixedTime)
{
if (s_It == null) return;
IEnumerator it = s_It;
object current = it.Current;
bool isNotOver = true;
if (current is WaitForFixedUpdate)
{
if (isFixedTime)
{
isNotOver = it.MoveNext();
}
}
else if (current is WaitForSeconds wait)
{
if (!isFixedTime && wait.IsOver(delta))
{
isNotOver = it.MoveNext();
}
}
else if (!isFixedTime)
{
isNotOver = it.MoveNext();
}
if (!isNotOver)
{
GD.Print("one cor over!");
s_It = null;
}
}
}
// WaitForFixedUpdate.cs
public struct WaitForFixedUpdate
{
}
// WaitForSeconds.cs
public class WaitForSeconds
{
private float m_Limit;
private float m_PassedTime;
public WaitForSeconds(float limit)
{
m_Limit = limit;
m_PassedTime = 0;
}
public bool IsOver(float delta)
{
m_PassedTime += delta;
return m_PassedTime >= m_Limit;
}
}
這樣就可以在一個IEnumerator中通過yield return null;
等待下一幀,yield return null WaitForFixedUpdate();
等待下一個物理更新,yield return new WaitForSeconds(1);
等待一秒。WaitWhile()
和WaitUtil()
實現同理
協程嵌套
協程的實用情景主要是資源加載之類耗時較久的地方,Unity中通過協程將異步操作以同步形式表現,如果這里的“協程”不能實現嵌套,那么也就沒有多少價值了。
在嘗試實現的過程中遇到的一個主要問題是子協程結束后如何呼叫父協程的下一個迭代...之后用層級計數的方法暫時處理。
僅實現了一種可行的方案,如果要投入實用,還需要做相關優化、bug修復、異常處理。
// CoroutineCore.cs
// 考慮協程嵌套的情況,單一IEnumerator變量就不能滿足需求了,從直覺上,首先想到使用Stack結構
public static class CoroutineCore
{
private static Stack<IEnumerator> s_Its = new Stack<IEnumerator>();
private static int s_SubCount = 0;
public static void StartCoroutine(IEnumerator e);
{
s_Its.Push(e);
}
public static void Update(float delta)
{
InnderDo(delta, false);
}
public static void FixedUpdate(float delta)
{
InnderDo(delta, true);
}
private static void InnderDo(float delta, bool isFixedTime)
{
if (s_Its.Count == 0) return;
IEnumerator it = s_It.Peek();
object current = it.Current;
bool isNotOver = true;
if (current is WaitForFixedUpdate)
{
if (isFixedTime)
{
isNotOver = it.MoveNext();
}
}
else if (current is WaitForSeconds wait)
{
if (!isFixedTime && wait.IsOver(delta))
{
isNotOver = it.MoveNext();
}
}
else if (current is IEnumerator nextIt)
{
s_Its.Push(nextIt);
s_SubCount++;
}
else if (!isFixedTime)
{
isNotOver = it.MoveNext();
}
if (!isNotOver)
{
GD.Print("one cor over!");
s_Its.Pop();
if (s_SubCount > 0)
{
it = s_Its.Peek();
it.MoveNext();
s_SubCount--;
}
}
}
}
測試代碼如下
private void TestBtn_pressed()
{
CoroutineCore.StartCoroutine(TestA);
}
IEnumerator TestA()
{
DateTimeOffset now;
now = DateTimeOffset.Now;
GD.Print(string.Format("{0}, {1}", now.Second, now.Millisecond));
yield return null;
now = DateTimeOffset.Now;
GD.Print(string.Format("{0}, {1}", now.Second, now.Millisecond));
yield return new WaitForSeconds(2);
now = DateTimeOffset.Now;
GD.Print(string.Format("{0}, {1}", now.Second, now.Millisecond));
yield return new WaitForFixedUpdate();
now = DateTimeOffset.Now;
GD.Print(string.Format("{0}, {1}", now.Second, now.Millisecond));
yield return TestB();
now = DateTimeOffset.Now;
GD.Print(string.Format("{0}, {1}", now.Second, now.Millisecond));
yield return null;
now = DateTimeOffset.Now;
GD.Print(string.Format("{0}, {1}", now.Second, now.Millisecond));
yield return null;
}
IEnumerator TestB()
{
DateTimeOffset now;
now = DateTimeOffset.Now;
GD.Print(string.Format("this is B!, {0}, {1}", now.Second, now.Millisecond));
yield return null;
now = DateTimeOffset.Now;
GD.Print(string.Format("this is B!, {0}, {1}", now.Second, now.Millisecond));
yield return new WaitForSeconds(1);
yield return TestC();
now = DateTimeOffset.Now;
GD.Print(string.Format("this is B!, {0}, {1}", now.Second, now.Millisecond));
yield return new WaitForSeconds(1);
}
IEnumerator TestC()
{
DateTimeOffset now;
now = DateTimeOffset.Now;
GD.Print(string.Format("this is C!, {0}, {1}", now.Second, now.Millisecond));
yield return null;
now = DateTimeOffset.Now;
GD.Print(string.Format("this is C!, {0}, {1}", now.Second, now.Millisecond));
}
執行結果
18, 130
18, 158
20, 158
20, 175
this is B!, 20, 192
this is B!, 20, 208
this is C!, 21, 242 *這里只執行了WaitForSeconds(1), 和預期值差了大概兩幀的時間
this is C!, 21, 258
one cor over!
this is B!, 21, 262
one cor over!
22, 260
22, 275
one cor over!
運行幀率是60FPS,即每次更新delta == 0.0167,運行順序邏輯是滿足預期的,但執行細節需要調整一下