While prototyping a new game for Uken, I found myself wanting a nice, simple, flexible state machine for JavaScript. Encapsulating game entity behaviours and state into their own components and being able to trust that a given component will just behave properly when one of its methods is called makes developement a lot faster and simpler. Something that might have been an unmanagable nest of if/else statements or an enormous switch statement can be reduced to a single function call that just works depending on the entity's state.
I was a little disappointed by the state of JS state machines. I'm sure that the libraries that exist that do this work are top-notch but (for my taste) they're a little rigid. Many of them are just fairly simple abstractions over that big, nasty switch statement. I didn't want to have to define all the relationships between all my states ahead of time. In general I wanted to avoid having to have a big configuration block that dictates something that should be a natural and evolving thing: application state.
Thankfully, there's a better way.
In UE3, UnrealScript introduced a feature to support states at the language level. This enabled you to 'write functions and code that exist in a particular state'. States were a particular data structure that was associated with a class and defined a behaviour or set of behaviours that were only accessible while the instance of that class was executing code in that state. This let you bundle all the relevant logic together. It also decouple states from each other in that all their logic didn't need to exist together in a single expression.
This is a nice solution if you have complete control over the language like Unreal does but that's not a luxury that's afforded to most of us. Thankfully, it's not really necessary if you're working in an interpreted language like JavaScript or... Lua. My first exposure to this style of state machine wasn't actually UE3, it was a Lua library (or mixin, more aptly) called Stateful.lua. The way it works is by mixing into an existing class and exposing an addState
function on a class and which in turn created a gotoState
function on any instance.
local class = require 'middleclass'
local Stateful = require 'stateful'
local Enemy = class('Enemy')
Enemy:include(Stateful)
function Enemy:initialize(health)
self.health = health
end
function Enemy:speak()
return 'My health is' .. tostring(self.health)
end
local Immortal = Enemy:addState('Immortal')
-- overriden function
function Immortal:speak()
return 'I am UNBREAKABLE!!'
end
-- added function
function Immortal:die()
return 'I can not die now!'
end
local peter = Enemy:new(10)
peter:speak() -- My health is 10
peter:gotoState('Immortal')
peter:speak() -- I am UNBREAKABLE!!
peter:die() -- I can not die now!
peter:gotoState(nil)
peter:speak() -- My health is 10
I've always found this a really nice way to think about entity behaviours and state. Unfortunately, a library like this didn't exist for JavaScript. Fortunately, it was pretty easy to make something similar. Enter: make-stateful.
make-stateful exposes a very similar API to Stateful.lua. A call to Stateful.extend
with a JS class as an argument will extend that class to include the addState
function to register new states and the class's prototype to include a gotoState
function so that instances can transition between states. It's simple, requires minimal configuration and just works. You never need to check what state you're in - you just structure your entities to behave properly given any state.
The one downside of using this approach in JavaScript is that you need to modify the 'base' entity whenever you transition states. In Lua, transitioning to a new state is as simple as updating a reference to what should be currently used as the entity's state. The object's properties don't need to be modified: the correct property is fetched from the current state whenever it's requested. JavaScript doesn't have a language feature that allows us to reasonably accomplish this, though. Yet.
At the time of this writing, browser support for Proxy and Reflect is peeking above 50%. With those facilities, we can start doing something similar to what Lua does with metatables and just swap out the state reference when we transition states.
const extend = function(klass) {
var states = {undefined: klass.prototype};
klass.addState = function(stateName, state) {
states[stateName] = state;
}
return new Proxy(klass, {
construct: function(target, args) {
var currentState = target.prototype;
var instance = Reflect.construct(target, args);
instance.gotoState = function(stateName, ...args) {
currentState = states[stateName];
}
return new Proxy(instance, {
get: function(target, name, receiver) {
if (currentState[name]) {
return currentState[name];
} else {
return target[name];
}
}
});
}
});
}
The places that this code will run is still somewhat limited for browser use but it's something to look forward to. In the meantime, the current implementation of make-stateful works just great!
Links: