Friday, August 21, 2009

The FSM isn't just a spaghetti monster

I just pushed 19 changes to the Jetblade code repository. And yet the only significant difference you'll see is that there's a splash screen when you start the game up:



Now, given how long startup can take, this is an important change: you'll have something to contemplate while you wait for Cython modules to be compiled and a map to be generated; you'll be able to wonder just why our armored hero is cowering at the approach of a red four-winged flying narwhal. But it doesn't warrant 19 changes. No, most of what's changed lately is behind the scenes. I've fired the Refactoring Cannon at the code.

If you'd looked at the TerrestrialObject class before, you would have seen that it was a mess. It had a whole tangle of booleans to control what it was doing and how it could move: isGrounded, wasGrounded, justJumped, isHanging, justStoppedHanging, justStartedClimbing, shouldCrawl, isCrawling, wasCrawling, haveChangedAction. And of course, dealing with these inputs required a large tangle of if-then-else statements. You don't need Goto to make spaghetti code, and TerrestrialObject is the proof.

Fortunately, the Flying Spaghetti Monster handed me a solution to my spaghetti code in the form of a Finite State Machine. A FSM describes the behavior of some device, or creature, or program, as a set of states and transitions between states. It's a bit like a flowchart. For example, the player starts out in the Grounded state, which means that they're standing on the ground. While in this state, they can run around, fall off of ledges, jump into the air, and crawl around. Each of these is a transition to a new state. In Jetblade's case, each state is its own class, implementing the interface defined in the ObjectState class, and the states between them describe all the behaviors of a TerrestrialObject.

Now, strictly speaking, TerrestrialObject was already a FSM before the refactoring. All programs are FSMs, after all. However, formalizing the code into an explicit FSM greatly improves its cleanliness. All of those booleans I mentioned before? Gone, or else hidden within a specific state, so that the others need not worry about it. TerrestrialObject used to be 455 lines long; it's now 184 lines. And half of that is documentation, whitespace, and default values.

As a side-effect, now that transitions between states are explicit, several bits of logic that had been causing a lot of weird glitches (specifically, forcing crawls when there's no room to stand, or forcing standing when there's no room to crawl) are now much simpler and less error-prone. I won't claim that they're bug free; after all, there's always one more bug. However, debugging them just got a heck of a lot easier.

If you find yourself dreading working with a piece of code, it's probably time to refactor it. TerrestrialObject is now refactored, and quite frankly it's a joy to work with.

No comments:

Post a Comment