使用await关键字实现游戏脚本的协同例程(Using await keyword to implement co

编程入门 行业动态 更新时间:2024-10-22 10:58:19
使用await关键字实现游戏脚本的协同例程(Using await keyword to implement co-routines for game scripts)

我试图在C#中实现协同例程,以便在我的游戏中编写敌人行为脚本时让我的生活变得更轻松。 游戏是固定的帧率。 编写脚本的理想语法是:

wait(60) while(true) { shootAtPlayer(); wait(30); }

这意味着等待60帧,向玩家射击,等待30,向玩家射击,等待30 ......等等

我在C#中使用yield return实现了这个解决方案:

public IEnumerable update() { yield return 60; while(true) { shootAtPlayer(); yield return 30; } }

调用者保持一堆活动例程(IEnumerable计数器上为0)和休眠例程(> 0)。 每个帧调用者将每个睡眠例程的睡眠帧计数器减1,直到达到0.此例程再次生效并继续执行直到下一个收益返回。 这在大多数情况下都可以正常工作,但是子程序不能像下面这样拆分很烦人:

public IEnumerable update() { yield return 60; while(true) { doSomeLogic() yield return 30; } } public IEnumerable doSomeLogic() { somethingHappening(); yield return 100; somethingElseHappens(); }

上述语法不正确,因为yield return不能嵌套在另一个方法中,这意味着只能在单个方法中维护执行状态(在本例中为update())。 这是yield return语句的限制,我无法真正找到解决它的方法。

我之前问过如何使用C#实现所需的行为,并且提到await关键字在这种情况下可能运行良好。 我现在想弄清楚如何更改代码以使用await。 是否有可能在调用方法中嵌套等待,就像我想要收益率回报一样? 我如何使用等待实现计数器? 例程的调用者需要控制递减为每个等待例程留下的等待帧,因为它将是每帧调用一次的等待帧。 我将如何实现这一目标?

I am attempting to implement co-routines in C# in order to make my life easier when it comes to scripting enemy behavior in my game. The game is fixed framerate. What would be an ideal syntax for writing the scripts would be:

wait(60) while(true) { shootAtPlayer(); wait(30); }

Which means wait 60 frames, shoot at player, wait 30, shoot at player, wait 30... e.t.c.

I have sort of implemented this solution using yield return in C#:

public IEnumerable update() { yield return 60; while(true) { shootAtPlayer(); yield return 30; } }

The caller keeps a stack of alive routines (0 on the IEnumerable counter) and sleeping routines ( > 0). Every frame the caller reduces the sleeping frame counter of each sleeping routine by 1 until one reaches 0. This routine is made alive again and continues execution until the next yield return. This works fine in most cases but it is annoying that the subroutine cannot be split up like below:

public IEnumerable update() { yield return 60; while(true) { doSomeLogic() yield return 30; } } public IEnumerable doSomeLogic() { somethingHappening(); yield return 100; somethingElseHappens(); }

The above syntax is incorrect as yield return cannot be nested within another method meaning the execution state can only ever be maintained in a single method (the update() in this case). This is a limitation of the yield return statement and I couldn't really find a way to work around it.

I asked earlier how might it be possible to implement the desired behaviour using C# and it was mentioned that the await keyword might work well in this situation. I am trying to figure out now how to change the code to use await. Is it possible to nest awaits in called methods like I wanted to do with yield return? also how would I implement a counter using awaits? The caller of the routines would need to be in control of decrementing the waiting frames left for each waiting routine as it will be the one called once per frame. How would I go about implementing this?

最满意答案

我不确定async / await是否对此有好处,但它绝对可能。 我已经创建了一个小的(好吧,至少我试图让它尽可能小)测试环境来说明可能的方法。 让我们从一个概念开始:

/// <summary>A simple frame-based game engine.</summary> interface IGameEngine { /// <summary>Proceed to next frame.</summary> void NextFrame(); /// <summary>Await this to schedule action.</summary> /// <param name="framesToWait">Number of frames to wait.</param> /// <returns>Awaitable task.</returns> Task Wait(int framesToWait); }

这应该允许我们以这种方式编写复杂的游戏脚本:

static class Scripts { public static async void AttackPlayer(IGameEngine g) { await g.Wait(60); while(true) { await DoSomeLogic(g); await g.Wait(30); } } private static async Task DoSomeLogic(IGameEngine g) { SomethingHappening(); await g.Wait(10); SomethingElseHappens(); } private static void ShootAtPlayer() { Console.WriteLine("Pew Pew!"); } private static void SomethingHappening() { Console.WriteLine("Something happening!"); } private static void SomethingElseHappens() { Console.WriteLine("SomethingElseHappens!"); } }

我将以这种方式使用引擎:

static void Main(string[] args) { IGameEngine engine = new GameEngine(); Scripts.AttackPlayer(engine); while(true) { engine.NextFrame(); Thread.Sleep(100); } }

现在我们可以了解实现部分。 当然你可以实现一个自定义的等待对象,但我只依赖于Task和TaskCompletionSource<T> (遗憾的是,没有非泛型版本,所以我只使用TaskCompletionSource<object> ):

class GameEngine : IGameEngine { private int _frameCounter; private Dictionary<int, TaskCompletionSource<object>> _scheduledActions; public GameEngine() { _scheduledActions = new Dictionary<int, TaskCompletionSource<object>>(); } public void NextFrame() { if(_frameCounter == int.MaxValue) { _frameCounter = 0; } else { ++_frameCounter; } TaskCompletionSource<object> completionSource; if(_scheduledActions.TryGetValue(_frameCounter, out completionSource)) { Console.WriteLine("{0}: Current frame: {1}", Thread.CurrentThread.ManagedThreadId, _frameCounter); _scheduledActions.Remove(_frameCounter); completionSource.SetResult(null); } else { Console.WriteLine("{0}: Current frame: {1}, no events.", Thread.CurrentThread.ManagedThreadId, _frameCounter); } } public Task Wait(int framesToWait) { if(framesToWait < 0) { throw new ArgumentOutOfRangeException("framesToWait", "Should be non-negative."); } if(framesToWait == 0) { return Task.FromResult<object>(null); } long scheduledFrame = (long)_frameCounter + (long)framesToWait; if(scheduledFrame > int.MaxValue) { scheduledFrame -= int.MaxValue; } TaskCompletionSource<object> completionSource; if(!_scheduledActions.TryGetValue((int)scheduledFrame, out completionSource)) { completionSource = new TaskCompletionSource<object>(); _scheduledActions.Add((int)scheduledFrame, completionSource); } return completionSource.Task; } }

主要观点:

Wait方法创建一个任务,该任务在到达指定的帧时完成。 我保留了计划任务的字典,并在达到所需的框架后立即完成。

更新:删除不必要的List<>简化代码。

I'm not sure if async/await are good for this, but it is definitely possible. I've created a small (well, at least I've tried to make it as small as possible) testing environment to illustrate possible approach. Let's start with a concept:

/// <summary>A simple frame-based game engine.</summary> interface IGameEngine { /// <summary>Proceed to next frame.</summary> void NextFrame(); /// <summary>Await this to schedule action.</summary> /// <param name="framesToWait">Number of frames to wait.</param> /// <returns>Awaitable task.</returns> Task Wait(int framesToWait); }

This should allow us to write complex game scripts this way:

static class Scripts { public static async void AttackPlayer(IGameEngine g) { await g.Wait(60); while(true) { await DoSomeLogic(g); await g.Wait(30); } } private static async Task DoSomeLogic(IGameEngine g) { SomethingHappening(); await g.Wait(10); SomethingElseHappens(); } private static void ShootAtPlayer() { Console.WriteLine("Pew Pew!"); } private static void SomethingHappening() { Console.WriteLine("Something happening!"); } private static void SomethingElseHappens() { Console.WriteLine("SomethingElseHappens!"); } }

I'm going to use engine this way:

static void Main(string[] args) { IGameEngine engine = new GameEngine(); Scripts.AttackPlayer(engine); while(true) { engine.NextFrame(); Thread.Sleep(100); } }

Now we can get to implementation part. Of course you can implement a custom awaitable object, but I'll just rely on Task and TaskCompletionSource<T> (unfortunately, no non-generic version, so I'll just use TaskCompletionSource<object>):

class GameEngine : IGameEngine { private int _frameCounter; private Dictionary<int, TaskCompletionSource<object>> _scheduledActions; public GameEngine() { _scheduledActions = new Dictionary<int, TaskCompletionSource<object>>(); } public void NextFrame() { if(_frameCounter == int.MaxValue) { _frameCounter = 0; } else { ++_frameCounter; } TaskCompletionSource<object> completionSource; if(_scheduledActions.TryGetValue(_frameCounter, out completionSource)) { Console.WriteLine("{0}: Current frame: {1}", Thread.CurrentThread.ManagedThreadId, _frameCounter); _scheduledActions.Remove(_frameCounter); completionSource.SetResult(null); } else { Console.WriteLine("{0}: Current frame: {1}, no events.", Thread.CurrentThread.ManagedThreadId, _frameCounter); } } public Task Wait(int framesToWait) { if(framesToWait < 0) { throw new ArgumentOutOfRangeException("framesToWait", "Should be non-negative."); } if(framesToWait == 0) { return Task.FromResult<object>(null); } long scheduledFrame = (long)_frameCounter + (long)framesToWait; if(scheduledFrame > int.MaxValue) { scheduledFrame -= int.MaxValue; } TaskCompletionSource<object> completionSource; if(!_scheduledActions.TryGetValue((int)scheduledFrame, out completionSource)) { completionSource = new TaskCompletionSource<object>(); _scheduledActions.Add((int)scheduledFrame, completionSource); } return completionSource.Task; } }

Main ideas:

Wait method creates a task, which completes when the specified frame is reached. I keep a dictionary of scheduled task and complete them as soon as required frame is reached.

Update: simplified code by removing unnecessary List<>.

更多推荐

本文发布于:2023-08-01 03:37:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1353984.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:脚本   例程   关键字   游戏   await

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!