🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

State Changes

posted in The Berg for project The Berg
Published October 14, 2018
Advertisement

Games usually (if not always) require some way to manage state changes... and I'm sure most of you (if not all of you) know far more about State Machines than I do.  And I'm certain that I could learn a heck of lot from reading up about the subject to build a state machine that works beautifully and makes my code look amazing etc etc.

Pfft.. never mind all that... I'm building this game 'off the cuff' as it were, making it up as I go along and following the principle of 'I build what I need when I need it and only insofar that it adequately fulfils the requirements at that time'.  I don't try to plan ahead (not in any granular sense anyway), I'm not building a reusable one-size-fits-all game engine, I'm not trying to make the code beautfiul, or win any awards or even make any money from the darn thing.  It just needs to perform well enough for what I want it to do.

So my immediate requirement is that I have a way to manage the player switching from walking to running to whatever.  If I can use it elsewhere for other things then great... and I'll be honest, I do like reusable code so I tend to naturally sway toward that.  What I'm trying to avoid is getting myself stuck in a rut, spending weeks/months deliberating over the smallest details because it's got to be 'perfect' and then realising I've still got 99.5% of the game to build!  Quick and dirty is OK in my world.

I often approach things from a top-down perspective.  This boils down to:

'How do I want to instruct the computer to do x, y or z?'

So for this particular requirement, how do I want to instruct the game that the player can change from walking to running and running to walking, or walking/running to falling (assuming I make that a player state - which I do), but not from sleeping to running for example?  Hell, I don't even know all the states that I want yet, but these are the ones I have a feel for so far:

  • Walking
  • Running
  • Skiiing
  • Driving
  • Falling
  • Drowning
  • Sleeping
  • Eating

Introducing 'When'

I thought it might be nice to be able to write something like this in my player setup:


// Configure valid player state transitions
When( this.playerState ).changes().from( PLAYER_STATES.WALKING ).to( PLAYER_STATES.RUNNING ).then( function () { } );
When( this.playerState ).changes().from( PLAYER_STATES.RUNNING ).to( PLAYER_STATES.WALKING ).then( function () { } );
When( this.playerState ).changes().from( PLAYER_STATES.WALKING ).to( PLAYER_STATES.SKIING ).then( function () { } );
When( this.playerState ).changes().from( PLAYER_STATES.SKIING ).to( PLAYER_STATES.WALKING ).then( function () { } );
When( this.playerState ).changes().from( PLAYER_STATES.WALKING, PLAYER_STATES.RUNNING, PLAYER_STATES.SKIING ).to( PLAYER_STATES.FALLING ).then( function () { } );

There's probably a library for something like this out there, but heck, where's the fun in that?!

 

So I create a new 'Stateful' object that represents a state (in this case the playerState) and it's allowed transitions and a 'When' function so I can write the code exactly as above:


const Stateful = function () { }
Stateful.isStateful = function ( obj ) {
    return obj.constructor && obj.constructor.name === Stateful.name;
}
Stateful.areEqual = function ( v1, v2 ) {
    return v1.equals ? v1.equals( v2 ) : v1 == v2;
}
Stateful.prototype = {
    constructor: Stateful,
    set: function ( v ) {
        let newState = typeof ( v ) === "function" ? new v() : v;

        for ( let i = 0; i < this.transitions.length; i++ ) {
            let transition = this.transitions[i];
            if ( transition && typeof ( transition.callback ) === "function" ) {
                let fromMatch = Stateful.areEqual( transition.vFrom, this );
                let toMatch = Stateful.areEqual( transition.vTo, newState );

                if ( fromMatch && toMatch ) {
                    // We can only change to the new state if a valid transition exists.
                    this.previousState = Object.assign( Object.create( {} ), this );
                    Object.assign( this, newState );

                    transition.callback( this.previousState, this );
                }
            }
        }
    },
    transitions: Object.create( Object.assign( Array.prototype, {
        from: function ( vFrom ) {
            this.vFrom = typeof ( vFrom ) === "function" ? new vFrom() : vFrom;
            return this;
        },
        to: function ( vTo ) {
            this.vTo = typeof ( vTo ) === "function" ? new vTo() : vTo;
            return this;
        },
        remove: function ( fn ) {
            this.vFrom = this.vFrom === undefined ? { equals: function () { return true; } } : this.vFrom;
            this.vTo = this.vTo === undefined ? { equals: function () { return true; } } : this.vTo;

            for ( let i = 0; i < this.length; i++ ) {
                let transition = this[i];
                let fromMatch = Stateful.areEqual( this.vFrom, transition.vFrom );
                let toMatch = Stateful.areEqual( this.vTo, transition.vTo );
                let fnMatch = fn === undefined ? true : transition.callback == fn;
                if ( fromMatch && toMatch & fnMatch ) {
                    delete this[i];
                }
            }
        }
    } ) )
}

function When( statefulObj ) {
    if ( !Stateful.isStateful( statefulObj ) ) {
        throw "Argument must be a Stateful object";
    }

    return {
        changes: function () {
            return {
                from: function ( ...vFrom ) {
                    this.vFrom = vFrom;
                    return this;
                },
                to: function ( ...vTo ) {
                    this.vTo = vTo;
                    return this;
                },
                then: function ( fn ) {
                    if ( typeof ( fn ) === "function" ) {
                        this.vFrom = this.vFrom === undefined ? [true] : this.vFrom;
                        this.vTo = this.vTo === undefined ? [true] : this.vTo;

                        for ( let i = 0; i < this.vFrom.length; i++ ) {
                            for ( let j = 0; j < this.vTo.length; j++ ) {
                                statefulObj.transitions.push( {
                                    vFrom: typeof ( this.vFrom[i] ) === "function" ? new this.vFrom[i]() : this.vFrom[i],
                                    vTo: typeof ( this.vTo[j] ) === "function" ? new this.vTo[j]() : this.vTo[j],
                                    callback: fn
                                } );
                            }
                        }
                    } else {
                        throw "Supplied argument must be a function";
                    }
                }
            };
        }
    }
}

I drop the aforementioned 'When' statements into my Player setup and remove the old 'If' statements that were previously controlling changes between walking and running and insert the new playerState.set() calls where appropriate.

e.g.


"run": ( pc, keyup ) => {
  if ( keyup ) {
    _this.player.playerState.set( PLAYER_STATES.WALKING );
  } else {
    _this.player.playerState.set( PLAYER_STATES.RUNNING );
  }
}

And it seems to work!  (Yes I was actually surprised by that) ?

TheBerg-StateChanges.mp4

p.s. I've switched to using Bandicam for screen capture as it seems far superior to what I was using previously.

0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement
Advertisement