3

我正在尝试在 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。该例程再次激活并继续执行,直到下一个 yield 返回。这在大多数情况下都可以正常工作,但令人讨厌的是子程序不能像下面那样拆分:

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 关键字在这种情况下可能会很好地工作。我现在想弄清楚如何更改代码以使用等待。是否可以在调用方法中嵌套等待,就像我想对收益返回做的那样?另外我将如何使用等待实现计数器?例程的调用者需要控制减少为每个等待例程留下的等待帧,因为它将是每帧调用一次的帧。我将如何实施呢?

4

1 回答 1

3

我不确定 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);
    }
}

现在我们可以进入实现部分。当然你可以实现一个自定义的等待对象,但我只依赖Taskand 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<>

于 2013-06-05T02:27:15.590 回答