2

My son is writing a simple RPG game that has a number of non-player characters (aka NPC's). Each NPC has an associated "script" that controls its behaviour. We were going to use a mini custom script language to write these behaviours but I'm now wondering if this would be better done in C#5/Async.

Taking a really simple example, suppose one of the NPC's just walks between two points I'm thinking it would be nice to write something like this:

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

The WalkTo method would be an async method that handles everything to do with walking between the two points and does this over a number of frames from the game loop. It's not a blocking method that can be off-loaded to a background thread.

And this is where I'm stuck... I haven't been able to find any examples using async/await in this manner, but it seems it would be perfect for it.

Ideas?


Here's some very rough pseudo code for what I'd like to do:

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;

}
Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
Brad Robinson
  • 44,114
  • 19
  • 59
  • 88

2 Answers2

4

Okay, it sounds like one option would be to have a TaskCompletionSource for each frame of the game. You can then await the Task from WalkTo, and set the result in 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
    }
}

I'm not sure how efficient this will be, admittedly... but it should work.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Jon, thanks for that. I'll read up on TaskCompletionSource now, but in the meantime I've come up with a working solution - see my posted answer. – Brad Robinson Feb 19 '13 at 13:02
  • Yep, TaskCompletionSource is what I was after, but using it differently to this example (see other answer). Basically I'm happy to write the state management without async... it was just in the "scripts" that I was hoping to use async. – Brad Robinson Feb 19 '13 at 13:21
0

I think I've figured it out in a simple test program

Firstly, I've got a base class for the NPC's like this:

EDIT: Updated NpcBase to use 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;
        }
    }

}

For reference, here's the old version of NpcBase with custom IAsyncResult implementation instead of 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;
    }
}

Next, I've got a simple "NPC" that moves in one dimension. When a moveTo operation starts it calls BeginTask in the NpcBase. When arrived at the destination, it calls 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;
}

And finally, a simple WinForms app to drive it. It consists of a button and two labels. Clicking the button starts both NPC and their position is displayed on the labels.

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();
    }

}

And it works, all seems to be running on the main UI thread... which is what I wanted.

Of course it needs to be fixed to handle cancelling of operations, exceptions etc... but the basic idea is there.

Brad Robinson
  • 44,114
  • 19
  • 59
  • 88
  • This looks like an *awful* lot of work compared with using TaskCompletionSource... – Jon Skeet Feb 19 '13 at 13:21
  • Yes, but... 1. I've updated it to use TaskCompletionSource (though differently to your example) 2. It's really just proof of concept and what I've shown fits better with the rest of the game's design and 3. The bulk of it is in a base class to be used by all NPC's... so it's actually fairly simple. – Brad Robinson Feb 19 '13 at 22:56