使用C#5异步来等待在多个游戏帧上执行的东西

本文关键字:执行 游戏 异步 C#5 等待 使用 | 更新日期: 2023-09-27 17:59:57

我儿子正在写一款简单的RPG游戏,游戏中有许多非玩家角色(又名NPC)。每个NPC都有一个相关的"脚本"来控制其行为。我们本来打算使用一种小型自定义脚本语言来编写这些行为,但我现在想知道在C#5/Async中这样做是否更好。

举一个非常简单的例子,假设一个NPC只是在两点之间徘徊,我想写这样的东西会很好:

while (true)
{
    await WalkTo(100,100);
    await WalkTo(200,200);
}

WalkTo方法将是一种异步方法,它处理与在两点之间行走有关的一切,并在游戏循环中的多个帧上执行此操作。它不是一个可以卸载到后台线程的阻塞方法。

这就是我被困的地方。。。我还没有找到任何以这种方式使用async/await的例子,但它似乎非常适合它

想法?


以下是我想做的一些非常粗略的伪代码:

class NpcBase
{
    // Called from game loop
    public void onUpdate(double elapsedTime)
    {
        // Move the NPC
        .
        .
        .

        // Arrived at destination?
        if (Arrived)
        {
            // How do I trigger that the task is finished?
            _currentTask.MarkComplete();        
        }
    }

    // Async method called by NPC "script"
    public async Task WalkTo(int x, int y)
    {
        // Store new target location

        // return a task object that will be "triggered" when the walk is finished
        _currentTask = <something??>
        return _currentTask;
    }
    Task _currentTask;
}

使用C#5异步来等待在多个游戏帧上执行的东西

好吧,听起来有一个选项是为游戏的每一帧设置一个TaskCompletionSource。然后,您可以等待来自WalkToTask,并在OnUpdate:中设置结果

private TaskCompletionSource<double> currentFrameSource;
// Called from game loop
public void OnUpdate(double elapsedTime)
{
    ...
    var previousFrameSource = currentFrameSource;
    currentFrameSource = new TaskCompletionSource<double>();
    // This will trigger all the continuations...
    previousFrameSource.SetResult(elapsedTime);
}
// Async method called by NPC "script"
public async Task WalkTo(int x, int y)
{
    // Store new target location
    while (/* we're not there yet */)
    {
        double currentTime = await currentFrameSource.Task;
        // Move
    }
}

无可否认,我不确定这会有多有效。。。但它应该起作用。

我想我已经在一个简单的测试程序中找到了它

首先,我有一个NPC的基类:

编辑:更新NpcBase以使用TaskCompletionSource:

public class NpcBase
{
    // Derived classes to call this when starting an async operation
    public Task BeginTask()
    {
        // Task already running?
        if (_tcs!= null)
        {
            throw new InvalidOperationException("busy");
        }
        _tcs = new TaskCompletionSource<int>();
        return _tcs.Task;
    }
    TaskCompletionSource<int> _tcs;
    // Derived class calls this when async operation complete
    public void EndTask()
    {
        if (_tcs != null)
        {
            var temp = _tcs;
            _tcs = null;
            temp.SetResult(0);
        }
    }
    // Is this NPC currently busy?
    public bool IsBusy
    {
        get
        {
            return _tcs != null;
        }
    }
}

作为参考,这里是NpcBase的旧版本,带有自定义IAsyncResult实现,而不是TaskCompletionSource:

// DONT USE THIS, OLD VERSION FOR REFERENCE ONLY
public class NpcBase
{
    // Derived classes to call this when starting an async operation
    public Task BeginTask()
    {
        // Task already running?
        if (_result != null)
        {
            throw new InvalidOperationException("busy");
        }
        // Create the async Task
        return Task.Factory.FromAsync(
            // begin method
            (ac, o) =>
            {
                return _result = new Result(ac, o);
            },
            // End method
            (r) =>
            {
            },
            // State object
            null
            );
    }
    // Derived class calls this when async operation complete
    public void EndTask()
    {
        if (_result != null)
        {
            var temp = _result;
            _result = null;
            temp.Finish();
        }
    }
    // Is this NPC currently busy?
    public bool IsBusy
    {
        get
        {
            return _result != null;
        }
    }
    // Result object for the current task
    private Result _result;
    // Simple AsyncResult class that stores the callback and the state object
    class Result : IAsyncResult
    {
        public Result(AsyncCallback callback, object AsyncState)
        {
            _callback = callback;
            _state = AsyncState;
        }

        private AsyncCallback _callback;
        private object _state;
        public object AsyncState
        {
            get { return _state; ; }
        }
        public System.Threading.WaitHandle AsyncWaitHandle
        {
            get { throw new NotImplementedException(); }
        }
        public bool CompletedSynchronously
        {
            get { return false; }
        }
        public bool IsCompleted
        {
            get { return _finished; }
        }
        public void Finish()
        {
            _finished = true;
            if (_callback != null)
                _callback(this);
        }
        bool _finished;
    }
}

接下来,我有一个简单的"NPC",它在一个维度上移动。当moveTo操作启动时,它会调用NpcBase中的BeginTask。当到达目的地时,它调用EndTask()。

public class NpcTest : NpcBase
{
    public NpcTest()
    {
        _position = 0;
        _target = 0;
    }
    // Async operation to count
    public Task MoveTo(int newPosition)
    {
        // Store new target
        _target = newPosition;
        return BeginTask();
    }
    public int Position
    {
        get
        {
            return _position;
        }
    }
    public void onFrame()
    {
        if (_position == _target)
        {
            EndTask();
        }
        else if (_position < _target)
        {
            _position++;
        }
        else
        {
            _position--;
        }
    }
    private int _position;
    private int _target;
}

最后,一个简单的WinForms应用程序来驱动它。它由一个按钮和两个标签组成。点击按钮启动两个NPC,它们的位置显示在标签上。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
    private void onButtonClick(object sender, EventArgs e)
    {
        RunNpc1();
        RunNpc2();
    }
    public async void RunNpc1()
    {
        while (true)
        {
            await _npc1.MoveTo(20);
            await _npc1.MoveTo(10);
        }
    }
    public async void RunNpc2()
    {
        while (true)
        {
            await _npc2.MoveTo(80);
            await _npc2.MoveTo(70);
        }
    }

    NpcTest _npc1 = new NpcTest();
    NpcTest _npc2 = new NpcTest();
    private void timer1_Tick(object sender, EventArgs e)
    {
        _npc1.onFrame();
        _npc2.onFrame();
        label1.Text = _npc1.Position.ToString();
        label2.Text = _npc2.Position.ToString();
    }
}

它工作起来了,似乎都在主UI线程上运行。。。这正是我想要的。

当然,它需要修复以处理操作的取消、异常等……但基本思想是存在的。