C# 使用wait关键字实现游戏脚本的共同例程

C# 使用wait关键字实现游戏脚本的共同例程,c#,xna,async-await,coroutine,C#,Xna,Async Await,Coroutine,我试图在C#中实现协同例程,以便在游戏中编写敌人行为脚本时更轻松。游戏是固定帧率的。编写脚本的理想语法是: wait(60) while(true) { shootAtPlayer(); wait(30); } 也就是说等待60帧,向玩家射击,等待30帧,向玩家射击,等待30帧。。。e、 t.c 我用C#中的收益率实现了这个解决方案: 调用者保留一堆活动例程(IEnumerable计数器上的0)和休眠例程(>0)。每帧调用者将每个睡眠例程的睡眠帧计数器减少1,直到达到0。此例程再次激活

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

wait(60)
while(true)
{
  shootAtPlayer();
  wait(30);
}
也就是说等待60帧,向玩家射击,等待30帧,向玩家射击,等待30帧。。。e、 t.c

我用C#中的收益率实现了这个解决方案:

调用者保留一堆活动例程(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#实现所需的行为,有人提到wait关键字在这种情况下可能会很好地工作。我现在正试图弄清楚如何将代码更改为使用wait。有没有可能像我希望的那样,在被调用的方法中嵌套等待呢?另外,如何使用waitis实现计数器?例程的调用者需要控制减少每个等待例程剩余的等待帧,因为它将是每帧调用一次的帧。我该如何实现这一点呢?

我不确定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
(不幸的是,没有非通用版本,所以我只使用
TaskCompletionSource
):

类游戏引擎:IGameEngine
{
专用整数帧计数器;
专用词典(scheduledActions);;
公共游戏引擎()
{
_scheduledActions=新字典();
}
public void NextFrame()
{
if(_frameCounter==int.MaxValue)
{
_帧计数器=0;
}
其他的
{
++_帧计数器;
}
TaskCompletionSource completionSource;
if(_scheduledActions.TryGetValue(_frameCounter,out completionSource))
{
Console.WriteLine(“{0}:当前帧:{1}”,
Thread.CurrentThread.ManagedThreadId,_frameCounter);
_scheduledActions.Remove(_frameCounter);
completionSource.SetResult(空);
}
其他的
{
WriteLine(“{0}:当前帧:{1},无事件。”,
Thread.CurrentThread.ManagedThreadId,_frameCounter);
}
}
公共任务等待(int framesToWait)
{
if(framesToWait<0)
{
抛出新的ArgumentOutOfRangeException(“framesToWait”,“应该是非负的”);
}
if(framesToWait==0)
{
返回Task.FromResult(空);
}
long scheduledFrame=(long)_frameCounter+(long)framesToWait;
if(scheduledFrame>int.MaxValue)
{
scheduledFrame-=int.MaxValue;
}
TaskCompletionSource completionSource;
if(!_scheduledActions.TryGetValue((int)scheduledFrame,out completionSource))
{
completionSource=新任务completionSource();
_添加((int)scheduledFrame,completionSource);
}
返回completionSource.Task;
}
}
主要思想:

  • Wait
    方法创建一个任务,该任务在到达指定帧时完成
  • 我保留了一个预定任务的字典,并在达到要求的框架后尽快完成它们

更新:通过删除不必要的
列表来简化代码

具有协同程序的有效实现,尽管它不是基于
async/await
(在C#5到来之前就已经存在了)。你可能想看看这个。你可能有很多工作要做,但肯定有可能用任务和
wait
操作符构建这种系统。我很想知道你是否能让某些东西工作。在你的代码中,单个帧的
任务
s将并行执行。我认为这是一个应该解决的问题。此外,您不需要每个帧的TCS列表,如果某个帧的TCS已经存在,您可以重用它。实际上,这里的一切都在单个线程中执行-您可以通过在所有
控制台.WriteLine()
中编写
thread.CurrentThread.ManagedThreadId
来测试它<代码>tcs.SetResult(空)会一直阻塞直到任务继续执行(您可以通过向
Script
方法添加
Thread.Sleep()
并测量
SetResult()
执行时间来测试它)。至于使用
TaskCompletionSource
而不是list,您完全正确,我将更新代码。这很有趣,你是对的,它确实在同一个线程上执行,但我不确定你能在多大程度上依赖它。我不知道关于这种内联的任何文档,但我确实认为这只是一种优化。
static void Main(string[] args)
{
    IGameEngine engine = new GameEngine();
    Scripts.AttackPlayer(engine);
    while(true)
    {
        engine.NextFrame();
        Thread.Sleep(100);
    }
}
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;
    }
}