dasasdhba 发表于 2024-4-27 23:33:04

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

本帖最后由 dasasdhba 于 2024-4-27 23:35 编辑

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

方便起见本帖的示例代码不严格写了,懂我意思就 ok

dasasdhba 发表于 2024-4-27 23:43:40

本帖最后由 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 里边,这个之后再说具体实现。

dasasdhba 发表于 2024-4-27 23:49:26

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-27 23:56:20

本帖最后由 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() 的方式与之前的做法混搭

dasasdhba 发表于 2024-4-27 23:59:58

4. 简单总结

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

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

dasasdhba 发表于 2024-4-28 00:02:17

本帖最后由 dasasdhba 于 2024-4-28 00:08 编辑

5. 其他问题

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

囿里有条小咸鱼 发表于 2024-4-28 11:25:41

gdscript的await:
await signal
# 或者
await async_func()

func async_func():
    await signal #或者await other_async_funcs()
需要注意的是:被异步调用的函数必须也是个协程才行,不然调了等于白异步
页: [1]
查看完整版本: 【Godot C#】通过异步编程提高开发效率