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,运行顺序逻辑是满足预期的,但执行细节需要调整一下
Godot - 通过C#实现类似Unity协程的更多相关文章
- 用Lua的协程实现类似Unity协程的语句块
local co_time_tbl = {} setmetatable(co_time_tbl, { __len = function(o) for k, v in pairs(o) do count ...
- unity协程coroutine浅析
转载请标明出处:http://www.cnblogs.com/zblade/ 一.序言 在unity的游戏开发中,对于异步操作,有一个避免不了的操作: 协程,以前一直理解的懵懵懂懂,最近认真充电了一下 ...
- Unity协程(Coroutine)管理类——TaskManager工具分享
博客分类: Unity3D插件学习,工具分享 源码分析 Unity协程(Coroutine)管理类——TaskManager工具分享 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处 ...
- Unity协程(Coroutine)原理深入剖析
Unity协程(Coroutine)原理深入剖析 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 其实协程并没有那么复杂,网上很多地方都说是多 ...
- Unity协程(Coroutine)原理深入剖析(转载)
记得去年6月份刚开始实习的时候,当时要我写网络层的结构,用到了协程,当时有点懵,完全不知道Unity协程的执行机制是怎么样的,只是知道函数的返回值是IEnumerator类型,函数中使用yield r ...
- Unity协程Coroutine使用总结和一些坑
原文摘自 Unity协程Coroutine使用总结和一些坑 MonoBehavior关于协程提供了下面几个接口: 可以使用函数或者函数名字符串来启动一个协程,同时可以用函数,函数名字符串,和Corou ...
- 深入浅出!从语义角度分析隐藏在Unity协程背后的原理
Unity的协程使用起来比较方便,但是由于其封装和隐藏了太多细节,使其看起来比较神秘.比如协程是否是真正的异步执行?协程与线程到底是什么关系?本文将从语义角度来分析隐藏在协程背后的原理,并使用C++来 ...
- Unity 协程使用指南
0x00 前言 在使用Unity的过程中,对协程仅仅知道怎样使用,但并不知道协程的内部机理,对于自己不清楚的部分就像一块大石压力心里.让自己感觉到担忧和不适. 这篇文章一探到底,彻底揭开协程的面纱,让 ...
- Unity协程(Coroutine)原理深入剖析再续
Unity协程(Coroutine)原理深入剖析再续 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 前面已经介绍过对协程(Coroutine ...
- Unity协程使用经验
[Unity协程使用经验] 1.协程的好处是,异步操作发起的地方和结束的地方可以统一在一个方法,这样就不用引入额外的成员变量来进行状态同步. 2.在一个协程中,StartCoroutine()和 yi ...
随机推荐
- kali下对压缩包的压缩与解压(转)
kali linux 压缩文件解压缩命令(包含7z) tar tar 解包:tar xvf FileName.tar 打包:tar cvf FileName.tar DirName (注:tar是打包 ...
- linux 字符集与编码格式相关
字符集:多个字符的集合. # 书写系统字母与符号的集合. 字符编码:把 字符集 中的字符 编码为(映射)指定集合中的某一对象. # 以便文本在计算机中存储和通过通信网络的传递 查看文件的的编码格式 ...
- 为什么大于 $3$ 的素数可以表示为 $6n\pm1$?
我们有一个素数 \(p\),都能表示为 \(6n \pm 1\). 为什么呢? 我们设 \(p = 6n \pm k\),\(k\) 可以是 \(0, 1, 2, 3, 4, 5\). 如果 \(k\ ...
- C#.NET 国密SM3 HASH 哈希 与JAVA互通 ver:20230803
C#.NET 国密SM3 HASH 哈希 与JAVA互通 ver:20230803 .NET 环境:.NET6 控制台程序(.net core). JAVA 环境:JAVA8,带maven 的JAVA ...
- 6、Mybatis之高级查询
6.1.创建接口.映射文件和测试类 ++++++++++++++++++++++++++分割线++++++++++++++++++++++++++ 注意namespace属性值为对应接口的全限定类名 ...
- Windows 虚拟地址 到底是如何映射到 物理地址 的?
一:背景 1. 讲故事 我发现有很多的 .NET程序员 写了很多年的代码都没弄清楚什么是 虚拟地址,更不用谈什么是 物理地址 以及Windows是如何实现地址映射的了?这一篇我们就来聊一聊这两者之间的 ...
- Badusb制作,远程别人电脑
Badusb制作 插一下U盘黑一台电脑,插了我的U盘你可就是我的脑了,(▽) 理论准备 我们要用它就应该知道他的工作原理是怎么样的,方便我们去发散思维去使用它. Badusb的原理是利用HID(Hum ...
- 通过API接口获取到数据后的使用方法以及储存方法
API接口是许多应用程序和服务所必需的,可以将多个应用程序连接起来,允许不同应用程序之间的数据共享.在本文中,我们将探讨如何使用API接口获取数据,以及如何储存这些数据. 1.使用API接口获取数据 ...
- 《Python魔法大冒险》007 被困的精灵:数据类型的解救
小鱼和魔法师深入魔法森林,树木之间流淌着神秘的光芒,每一片叶子都似乎在低语着古老的咒语.不久,他们来到了一个小湖旁,湖中央有一个小岛,岛上困着一个透明的泡泡,里面有一个悲伤的精灵. 小鱼看着那个精灵, ...
- PYQT5学习(13):QMidArea同时显示多个窗口,创建多个独立的窗口
QMidArea 参考文章:https://blog.csdn.net/jia666666/article/details/81670569 一种同时显示多个窗口的方法,创建多个独立的窗口,这些独立 ...