Useful state machines for XNA development

As I continue to implement various mechanics of the game, I keep finding common patterns for state management in game entities. For example, lots of entities sometimes need to flash on and off at regular intervals. Others need to slowly fade away over time. These needs keep popping up. So how do I share the code to make it happen?

Inheritance in this setting, even when it’s logical and possible, turns into an absolute mess pretty quickly, as every useful function gets crammed into the base class with lots of special cases in subclasses. Static class methods will work sometimes, especially when the thing you’re trying to do is essentially functional. But what about when there’s actual per-instance state you need to track?

To solve this, I’ve created several XNA-specific state machines to do simple things on behalf of other classes. They get fed a GameTime every Update() cycle, and update their state accordingly. The concept is most easily illustrated with an example. Here’s how I implement a simple Timer:

/// 
/// A timer that will tell you when the time has elapsed.
/// 
public class Timer {

    private double _timeTillAlarmMs;

    public Timer(double timeTillAlarmMs) {
        _timeTillAlarmMs = timeTillAlarmMs;
    }

    public void Update(GameTime gameTime) {
        _timeTillAlarmMs -= gameTime.ElapsedGameTime.TotalMilliseconds;
    }

    public double TimeLeft { get { return _timeTillAlarmMs; } }

    public bool IsTimeUp() {
        return _timeTillAlarmMs <= 0;
    }
}

Lots of game entities have a timeToLive member that's implemented in this fashion, but I get quite a bit of traction out of it in surprising places.

Next up is an oscillator, which cycles on or off in a period of your choosing:

/// 
/// Simple oscillator between states at a time interval of your choosing
/// 
public class Oscillator {

    private readonly double _periodMs;
    private double _timer;

    public Oscillator(double periodMs, bool initialState) {
        _periodMs = periodMs;
        IsActiveState = initialState;
        _timer = periodMs;
    }

    public bool IsActiveState { get; private set; }

    public void Update(GameTime gameTime) {
        _timer += gameTime.ElapsedGameTime.TotalMilliseconds;
        if ( _timer >= _periodMs ) {
            IsActiveState = !IsActiveState;
            _timer %= _periodMs;
        }
    }
}

This is great for a "flash on and off" effect, also very common. But it's really just the 2-state case of more general state machine that counts up to a certain total over and over:

/// 
/// Counter state machine. Counts up to a given value, then back to zero.
/// 
public class Counter {
        
    private readonly double _periodMs;
    private readonly int _numStates;
    private double _timer;

    public Counter(double periodMs, int numStates) {
        _periodMs = periodMs;
        StateNumber = 0;
        _timer = periodMs;
        _numStates = numStates;
    }

    public int StateNumber { get; private set; }

    public void Update(GameTime gameTime) {
        _timer += gameTime.ElapsedGameTime.TotalMilliseconds;
        if ( _timer >= _periodMs ) {
            _timer %= _periodMs;
            StateNumber = (StateNumber + 1) % _numStates;
        }
    }
}

These counters are super handy for keeping track of which frame of animation a game entity is on.

What's great about all these little devices is that they're totally reusable in any context, rather than wired into a hierarchy of classes. These are the ones you'll probably find most useful, but there are many other, more complicated ones I've built. For example, this one helps game entities determine the question of whether they are standing on solid ground. This should be an easy question to answer, except that Box2D (or maybe just Farseer) doesn't always handle collisions and contacts correctly when Body objects are created and disposed.

/// 
/// Class to help track whether an entity is in contact with the ground or ceiling
/// 
public class StandingMonitor {
        
    public bool IsStanding { get; set; }
    public bool IsTouchingCeiling { get; set; }
    public int IgnoreStandingUpdatesNextNumFrames { get; set; }

    /// 
    /// Updates bookkeeping counters
    /// 
    public void UpdateCounters() {
        if ( IgnoreStandingUpdatesNextNumFrames > 0 ) {
            IgnoreStandingUpdatesNextNumFrames--;
        }
    }

    /// 
    /// Updates the standing and ceiling status using the body's current contacts, 
    /// given the location of its lowest point.
    /// 
    public void UpdateStanding(Body body, World world, Vector2 standingLocation, float width) {

        bool isStanding = false;
        bool isTouchingCeiling = false;

        var contactEdge = body.ContactList;
        while ( contactEdge != null ) {
            if ( !contactEdge.Contact.FixtureA.IsSensor && !contactEdge.Contact.FixtureB.IsSensor &&
                    (contactEdge.Contact.IsTouching() &&
                    (contactEdge.Other.GetUserData().IsTerrain || contactEdge.Other.GetUserData().IsDoor)) ) {
                Vector2 normal = contactEdge.Contact.GetPlayerNormal(body);
                if ( normal.Y < -.8 ) {
                    isStanding = true;
                } else if ( normal.Y > .8 ) {
                    isTouchingCeiling = true;
                }
            }
            contactEdge = contactEdge.Next;
        }

        /*
            * If we didn't find any contact points, it could mean that it's because Box2d isn't playing nicely with
            * a newly created body (as when a tile reappears).  In that case, try to find the ground under our feet
            * with a ray cast.
            */
        float delta = .1f;
        if ( !isStanding ) {
            foreach ( Vector2 start in new Vector2[] {
                standingLocation + new Vector2(-width / 2, -delta),
                standingLocation + new Vector2(width / 2, -delta),
            } ) {
                world.RayCast((fixture, point, normal, fraction) => {
                    if ( fixture.GetUserData().IsTerrain ) {
                        isStanding = true;
                        return 0;
                    }
                    return -1;
                }, start, start + new Vector2(0, 2 * delta));
            }
        }

        if ( IgnoreStandingUpdatesNextNumFrames <= 0 ) {
            IsStanding = isStanding;
        }

        IsTouchingCeiling = isTouchingCeiling;
    }
}

As for the IgnoreStandingUpdatesNextNumFrames field, I'll let you imagine for yourself the horrors that necessitated that fearsome kludge. Did I mention Box2D doesn't handle resizing Body's very well either?

Leave a Reply