查看: 176|回复: 7

[讨论] 【Godot C#】通过异步编程提高开发效率

[复制链接]

37

主题

727

回帖

13

精华

版主

经验
7418
硬币
1171 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

发表于 2024-4-27 23:33:04 | 显示全部楼层 |阅读模式
本帖最后由 dasasdhba 于 2024-4-27 23:35 编辑

之前一直觉得异步和多线程主要是用于提高程序运行效率(并行计算 be like)
这段时间越来越发现其实异步可以省不少事情,故在此分享。

方便起见本帖的示例代码不严格写了,懂我意思就 ok
Moonstruck Blossom
个人网站:dasasdhba.github.io

37

主题

727

回帖

13

精华

版主

经验
7418
硬币
1171 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2024-4-27 23:43:40 | 显示全部楼层
本帖最后由 dasasdhba 于 2024-4-28 11:38 编辑

1. 以食人花三连发子弹作为简单例子,直观感受传统 Process 模式和异步模式的区别:

  1. public class Piranha
  2. {
  3.         public int FireballCount { get; set; } = 3;
  4.         public double FireballInterval { get; set; } = 0.1d;

  5.         protected int FireballCounter { get; set; } = 0;
  6.         protected double FireballTimer { get; set; } = 0d;

  7.         // 传统写法
  8.         public void Process(double delta)
  9.         {
  10.                 if (FireballCounter < FireballCount)
  11.                 {
  12.                         FireballTimer += delta;
  13.                         if (FireballTimer >= FireballInterval)
  14.                         {
  15.                                 FireballTimer = 0d;
  16.                                 FireballCounter++;
  17.                                 CreateFireball();
  18.                         }
  19.                 }
  20.         }

  21.         // 异步写法
  22.         public async Task CreateFireballAsync()
  23.         {
  24.                 for (int i = 0; i < FireballCount; i++)
  25.                 {
  26.                         CreateFireball();
  27.                         await Async.Wait(this, FireballInterval);
  28.                 }
  29.         }

  30.         public void CreateFireball() { /*...*/ }
  31. }
复制代码


我个人习惯是把异步相关的基本功能封装到一个静态类 Async 里边,这个之后再说具体实现。
Moonstruck Blossom
个人网站:dasasdhba.github.io

37

主题

727

回帖

13

精华

版主

经验
7418
硬币
1171 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2024-4-27 23:49:26 | 显示全部楼层
2. 在 Godot 中封装实用的异步工具函数

我目前最常用的有这些:

  1. using System;
  2. using Godot;
  3. using System.Threading.Tasks;

  4. public static partial class Async
  5. {

  6.         // 基于 Godot Timer 的异步等待方法
  7.     public static async Task Wait(Node node, double time, bool physics = false)
  8.     {
  9.         Timer timer = new()
  10.         {
  11.             Autostart = true,
  12.             WaitTime = time,
  13.             ProcessCallback = physics ? Timer.TimerProcessCallback.Physics : Timer.TimerProcessCallback.Idle
  14.         };
  15.         timer.Timeout += timer.QueueFree;
  16.         node.AddChild(timer, false, Node.InternalMode.Front);
  17.         await timer.ToSignal(timer, Timer.SignalName.Timeout);
  18.     }

  19.         // 用于 WaitProcess 方法的辅助节点
  20.     public partial class AsyncProcessTimer : Timer
  21.     {
  22.         public Action<double> Process { get; set; }

  23.         public override void _Process(double delta)
  24.         {
  25.             if (ProcessCallback == TimerProcessCallback.Idle) Process?.Invoke(delta);
  26.         }

  27.         public override void _PhysicsProcess(double delta)
  28.         {
  29.             if (ProcessCallback == TimerProcessCallback.Physics) Process?.Invoke(delta);
  30.         }
  31.     }

  32.         // 在异步等待 Timer 的同时进行 Process
  33.     public static async Task WaitProcess(Node node, double time, Action<double> process, bool physics = false)
  34.     {
  35.         AsyncProcessTimer timer = new()
  36.         {
  37.             Autostart = true,
  38.             WaitTime = time,
  39.             ProcessCallback = physics ? Timer.TimerProcessCallback.Physics : Timer.TimerProcessCallback.Idle,
  40.             Process = process
  41.         };
  42.         timer.Timeout += timer.QueueFree;
  43.         node.AddChild(timer, false, Node.InternalMode.Front);
  44.         await timer.ToSignal(timer, Timer.SignalName.Timeout);
  45.     }

  46.         // 用于 Delegate 方法的辅助节点
  47.     public partial class AsyncDelegateNode : Node
  48.     {
  49.         public Func<double, bool> Action { get; set; }
  50.         public bool Physics { get; set; } = false;

  51.         [Signal]
  52.         public delegate void FinishedEventHandler();

  53.         public void Act(double delta)
  54.         {
  55.             if (Action.Invoke(delta))
  56.             {
  57.                 EmitSignal(SignalName.Finished);
  58.                 QueueFree();
  59.             }
  60.         }

  61.         public override void _Process(double delta)
  62.         {
  63.             if (!Physics) Act(delta);
  64.         }

  65.         public override void _PhysicsProcess(double delta)
  66.         {
  67.             if (Physics) Act(delta);
  68.         }
  69.     }

  70.         // 异步等待直到给定的 action 返回 true
  71.     public static async Task Delegate(Node node, Func<bool> action, bool physics = false)
  72.     {
  73.         AsyncDelegateNode delegateNode = new()
  74.         {
  75.             Action = (double delta) => action.Invoke(),
  76.             Physics = physics
  77.         };

  78.         node.AddChild(delegateNode, false, Node.InternalMode.Front);
  79.         await delegateNode.ToSignal(delegateNode, AsyncDelegateNode.SignalName.Finished);
  80.     }

  81.         // 在异步等待 Delegate 的同时进行 Process
  82.     public static async Task DelegateProcess(Node node, Func<double, bool> action, bool physics = false)
  83.     {
  84.         AsyncDelegateNode delegateNode = new()
  85.         {
  86.             Action = action,
  87.             Physics = physics
  88.         };

  89.         node.AddChild(delegateNode, false, Node.InternalMode.Front);
  90.         await delegateNode.ToSignal(delegateNode, AsyncDelegateNode.SignalName.Finished);
  91.     }
  92. }
复制代码


原理和作用查看源代码和相关注释即可,不再赘述
直接拿去用也行,我无所谓
Moonstruck Blossom
个人网站:dasasdhba.github.io

37

主题

727

回帖

13

精华

版主

经验
7418
硬币
1171 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2024-4-27 23:56:20 | 显示全部楼层
本帖最后由 dasasdhba 于 2024-4-28 00:00 编辑

3. 使用 Tween
Godot 的 Tween 其实跟异步没啥区别,使用 Tween 也能实现食人花子弹三连的例子,如下:

  1. public void CreateFireballTween()
  2.         {
  3.                 var tween = CreateTween();
  4.                 for (int i = 0; i < FireballCount; i++)
  5.                 {
  6.                         tween.TweenInterval(FireballInterval);
  7.                         tween.TweenCallback(Callable.From(CreateFireball));
  8.                 }
  9.                 tween.TweenCallback(Callable.From(tween.Kill));
  10.         }
复制代码


更一般的,完全可以把 Tween 也作为一个异步工具箱使用,采用 await ToSignal() 的方式与之前的做法混搭

点评

其实如果不涉及每次异步调用都有异步函数参数变化的话可以考虑直接CreateTween().SetLoop(n)  发表于 2024-4-29 13:15
Moonstruck Blossom
个人网站:dasasdhba.github.io

37

主题

727

回帖

13

精华

版主

经验
7418
硬币
1171 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2024-4-27 23:59:58 | 显示全部楼层
4. 简单总结

传统写法需要把所有东西都糅合在一个 Process 中,其实非常不舒服;
另一方面,总是需要声明成员变量用于计时和计数也是真的很烦人。

个人认为异步写很多逻辑有时候会更加自然,因为游戏逻辑很多时候都是一个连续的过程,计时和计数的操作极为常见;
而且异步开发的效率也确实高不少,至少我这段时间是这么个感觉。
Moonstruck Blossom
个人网站:dasasdhba.github.io

37

主题

727

回帖

13

精华

版主

经验
7418
硬币
1171 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军对不起,小姐盲猜大王数字君X68数字君X68数字君X78

 楼主| 发表于 2024-4-28 00:02:17 | 显示全部楼层
本帖最后由 dasasdhba 于 2024-4-28 00:08 编辑

5. 其他问题

关于 gdscript:我不熟 gdscript,据说也有 await,若读者熟悉的话欢迎指出。
关于性能和稳定性:我懒得深究,不过至少我没遇到什么问题。
Moonstruck Blossom
个人网站:dasasdhba.github.io

56

主题

409

回帖

8

精华

版主

☯ 博 丽 不 是 灵 梦 ☯

经验
6140
硬币
1038 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

发表于 2024-4-28 11:25:41 | 显示全部楼层
gdscript的await:
  1. await signal
  2. # 或者
  3. await async_func()

  4. func async_func():
  5.     await signal #或者await other_async_funcs()
复制代码

需要注意的是:被异步调用的函数必须也是个协程才行,不然调了等于白异步
>❀ To the Best You ❀<
您需要登录后才可以回帖 登录 | 创建账户

本版积分规则