3
votes

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?

1
Caliburn.Micro has a working implementation of CoRoutines, though it's not based in async/await (it was there way before C# 5 arrived). You may want to look at that.Federico Berasategui
You would have quite a bit of work to do but it is certainly possible to build this sort of system out of tasks and the await operator. I would be curious to know if you manage to get something working.Eric Lippert

1 Answers

3
votes

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<>.