Sunday 11 December 2011

GameTime in another Thread

I run all the AI in Diabolical: The Shooter in a separate thread.  On the Xbox this is a hardware thread.  While working on the Behaviour Tree that I am currently in the process of implementing I found I needed to keep track of time.

The time is used to avoid tasks taking too long and making the Bot look daft.  It did not take me long to realise that getting the main thread GameTime in to another thread is not that easy.  In fact the following post from the XNA forums convinced me that I should not try: http://xboxforums.create.msdn.com/forums/t/10587.aspx

In the post Shawn Hargreaves suggests using a Stopwatch.  As Shawn wrote a large chunk of the XNA framework I have a lot of respect for his suggestions and taking that advice usually saves me a lot of work.  So without any hesitation that is what I did. 

I need to pass the time round to lots of behaviours.  The Stopwatch is a class so I could pass that round easily.  It's results are TimeSpan's which are structures so not as fast for passing as a parameter.  I would have liked some of the convenience of the GameTime class however it was unclear from the documentation if it included its own timer so I decided to write my own wrapper for a Stopwatch.

As it turned out all I needed was the following:


// Use a stopwatch to keep track of 
// the time for other threads
public class ThreadTimer
{
    private Stopwatch threadTime;

    // Create and start the timer.
    public ThreadTimer()
    {
        threadTime = Stopwatch.StartNew();
    }
    // The time since the timer was started
    public TimeSpan CurrentTime
    {
        get { return threadTime.Elapsed; }
    }
    // Calculate the future time after a number of 
    // seconds has been added to the current time.
    public TimeSpan EndTimeAfter(float howManySeconds)
    {
        return CurrentTime + 
               TimeSpan.FromSeconds(howManySeconds);
    }
}


I start that class whenever the new AI thread starts:


private void ProcessBehaviourUpdatesThread()
{
#if XBOX
    // First move on to the desired 
    // hardware thread
    int[] cpu = new int[1];
    cpu[0] = 5;
    Thread.CurrentThread.SetProcessorAffinity(cpu);
#endif
    // Each thread needs it's own random
    random = new Random();
    // Use the time to prevent run away behaviour
    threadTime = new ThreadTimer();
    // Keep the thread processing requests
    while (!KillThreads)
    {
        UpdateBehaviour();
        // Allow for anything else that runs 
        // on this hardware thread to get a chance.
        Thread.Sleep(
               GameSettings.botLoopSleepMilliSeconds);
    }
    IsBehaviourThreadActive = false;
}


That 'threadTime' is then passed to the Update() method of each behaviour. 

While mentioning threads I also pass the Random class as well.  That is another thing that is awkward with threads.  For some reason each thread must have its own Random.  I pass the reference to that in the Update() as well.



I'm please with how my Behaviour Tree is coming along.  At the moment it is untested but already it is more flexible and neater code than the State Machine I was using before.  The main code is done and now I am adding the individual behaviour classes.  I'll probably go in to more detail when I have it tested.

3 comments:

Anonymous said...

Why not just assign game time to a static member?

I just do this in my screenManager ...

Global3DGame.GameTime = gameTime;


My character animation is in another thread and i pass game time that way. I also use it to avoid having to pass the same darn thing around to everything.

John C Brown said...

I have learnt when dealing with thread safety to err on the side of caution!

My understanding from the forum post I link to above is that GameTime is not thread safe!

See the second paragraph of Shawn Hargreaves second comment:
'If you are for some reason running game logic outside of Update (for instance in an async multithreaded design) I don't know why you would use GameTime at all. A Stopwatch would be more appropriate for that scenario.'

John C Brown said...

On your first point about using static members. I find this causes me problems using the wrong instance for the thread I am running in. Especially if the same method can be called from multiple threads.

I agree I end up passing GameTime and Random as parameters to lots of methods but as I said above, with thread safety I take no chances!

Threading problems come up and bite me unexpectedly, especially on the Xbox if I have been testing it first on Windows.