Godot - 通过C#实现类似Unity协程

参考博客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,运行顺序逻辑是满足预期的,但执行细节需要调整一下

posted @ 2019-12-18 14:51  lunoctis  阅读(1699)  评论(0编辑  收藏  举报