【Godot C#】通过异步编程提高开发效率
本帖最后由 dasasdhba 于 2024-4-27 23:35 编辑之前一直觉得异步和多线程主要是用于提高程序运行效率(并行计算 be like)
这段时间越来越发现其实异步可以省不少事情,故在此分享。
方便起见本帖的示例代码不严格写了,懂我意思就 ok 本帖最后由 dasasdhba 于 2024-4-28 11:38 编辑
1. 以食人花三连发子弹作为简单例子,直观感受传统 Process 模式和异步模式的区别:
public class Piranha
{
public int FireballCount { get; set; } = 3;
public double FireballInterval { get; set; } = 0.1d;
protected int FireballCounter { get; set; } = 0;
protected double FireballTimer { get; set; } = 0d;
// 传统写法
public void Process(double delta)
{
if (FireballCounter < FireballCount)
{
FireballTimer += delta;
if (FireballTimer >= FireballInterval)
{
FireballTimer = 0d;
FireballCounter++;
CreateFireball();
}
}
}
// 异步写法
public async Task CreateFireballAsync()
{
for (int i = 0; i < FireballCount; i++)
{
CreateFireball();
await Async.Wait(this, FireballInterval);
}
}
public void CreateFireball() { /*...*/ }
}
我个人习惯是把异步相关的基本功能封装到一个静态类 Async 里边,这个之后再说具体实现。
2. 在 Godot 中封装实用的异步工具函数
我目前最常用的有这些:
using System;
using Godot;
using System.Threading.Tasks;
public static partial class Async
{
// 基于 Godot Timer 的异步等待方法
public static async Task Wait(Node node, double time, bool physics = false)
{
Timer timer = new()
{
Autostart = true,
WaitTime = time,
ProcessCallback = physics ? Timer.TimerProcessCallback.Physics : Timer.TimerProcessCallback.Idle
};
timer.Timeout += timer.QueueFree;
node.AddChild(timer, false, Node.InternalMode.Front);
await timer.ToSignal(timer, Timer.SignalName.Timeout);
}
// 用于 WaitProcess 方法的辅助节点
public partial class AsyncProcessTimer : Timer
{
public Action<double> Process { get; set; }
public override void _Process(double delta)
{
if (ProcessCallback == TimerProcessCallback.Idle) Process?.Invoke(delta);
}
public override void _PhysicsProcess(double delta)
{
if (ProcessCallback == TimerProcessCallback.Physics) Process?.Invoke(delta);
}
}
// 在异步等待 Timer 的同时进行 Process
public static async Task WaitProcess(Node node, double time, Action<double> process, bool physics = false)
{
AsyncProcessTimer timer = new()
{
Autostart = true,
WaitTime = time,
ProcessCallback = physics ? Timer.TimerProcessCallback.Physics : Timer.TimerProcessCallback.Idle,
Process = process
};
timer.Timeout += timer.QueueFree;
node.AddChild(timer, false, Node.InternalMode.Front);
await timer.ToSignal(timer, Timer.SignalName.Timeout);
}
// 用于 Delegate 方法的辅助节点
public partial class AsyncDelegateNode : Node
{
public Func<double, bool> Action { get; set; }
public bool Physics { get; set; } = false;
public delegate void FinishedEventHandler();
public void Act(double delta)
{
if (Action.Invoke(delta))
{
EmitSignal(SignalName.Finished);
QueueFree();
}
}
public override void _Process(double delta)
{
if (!Physics) Act(delta);
}
public override void _PhysicsProcess(double delta)
{
if (Physics) Act(delta);
}
}
// 异步等待直到给定的 action 返回 true
public static async Task Delegate(Node node, Func<bool> action, bool physics = false)
{
AsyncDelegateNode delegateNode = new()
{
Action = (double delta) => action.Invoke(),
Physics = physics
};
node.AddChild(delegateNode, false, Node.InternalMode.Front);
await delegateNode.ToSignal(delegateNode, AsyncDelegateNode.SignalName.Finished);
}
// 在异步等待 Delegate 的同时进行 Process
public static async Task DelegateProcess(Node node, Func<double, bool> action, bool physics = false)
{
AsyncDelegateNode delegateNode = new()
{
Action = action,
Physics = physics
};
node.AddChild(delegateNode, false, Node.InternalMode.Front);
await delegateNode.ToSignal(delegateNode, AsyncDelegateNode.SignalName.Finished);
}
}
原理和作用查看源代码和相关注释即可,不再赘述
直接拿去用也行,我无所谓 本帖最后由 dasasdhba 于 2024-4-28 00:00 编辑
3. 使用 Tween
Godot 的 Tween 其实跟异步没啥区别,使用 Tween 也能实现食人花子弹三连的例子,如下:
public void CreateFireballTween()
{
var tween = CreateTween();
for (int i = 0; i < FireballCount; i++)
{
tween.TweenInterval(FireballInterval);
tween.TweenCallback(Callable.From(CreateFireball));
}
tween.TweenCallback(Callable.From(tween.Kill));
}
更一般的,完全可以把 Tween 也作为一个异步工具箱使用,采用 await ToSignal() 的方式与之前的做法混搭 4. 简单总结
传统写法需要把所有东西都糅合在一个 Process 中,其实非常不舒服;
另一方面,总是需要声明成员变量用于计时和计数也是真的很烦人。
个人认为异步写很多逻辑有时候会更加自然,因为游戏逻辑很多时候都是一个连续的过程,计时和计数的操作极为常见;
而且异步开发的效率也确实高不少,至少我这段时间是这么个感觉。
本帖最后由 dasasdhba 于 2024-4-28 00:08 编辑
5. 其他问题
关于 gdscript:我不熟 gdscript,据说也有 await,若读者熟悉的话欢迎指出。
关于性能和稳定性:我懒得深究,不过至少我没遇到什么问题。
gdscript的await:
await signal
# 或者
await async_func()
func async_func():
await signal #或者await other_async_funcs()
需要注意的是:被异步调用的函数必须也是个协程才行,不然调了等于白异步
页:
[1]