The following blog post, unless otherwise noted, was written by a member of Gamasutraís community.
The thoughts and opinions expressed are those of the writer and not Gamasutra or its parent company.
I have been a professional game programmer for over 12 years... but in the first weeks of working at my most recent job, I’ve come to realize just how much I’ve internalized at my previous job from sitting next to a certain ex-coworker for the last three years. It is enough to make me want to write about it and specifically credit him by name.
I no longer work with Joe Houston. It is unlikely that I will ever work with him again... I have quit that job we had together in order to pursue an opportunity in a completely different city, and he has recently quit that job to find his calling as an indie game developer. Assuming both of us succeed in our life goals, we will probably never work together again.
His worthy Kickstarter campaign ends on February 13th, 2013:
I backed it.
Lesson #1: Defer Your State Transitions
To calibrate our vocabulary, here is a gross simplification of what I mean when talking state machines...A finite state machine is used to keep mutually exclusive bodies of code decoupled from each other and bookended by predictable “Enter()” and “Exit()” functions for each state. Technically, a switch statement inside an Update() function can be a state machine, but when your states become more complex, you will want to encapsulate that code within “class State” and make sure it is managed by “class FiniteStateMachine”.
In a State Machine, you have two very different choices on how state transitions work... or in other words, what happens inside the scope of “FiniteStateMachine::RequestStateChange( stateID )”
|Instant Transition ||Deferred Transition
- State changes happen instantly at the time of the change request.
- A call to GetCurrentState() on the next line of code would return the value of the new state.
- State changes happen at a consistent spot within the update loop (PostUpdate) no matter where in the loop that change request was made.
- A call to GetCurrentState() on the next line of code would return the value of the old state, and so you may feel the urge to implement “GetCurrentOrPendingState()”.
|Pros for Instant ||Pros for Deferred
- Easy call stack.
- Intuitive mental model.
- Initially faster to implement
- Easier to trust the state snapshot as an atomic indivisible frame of execution
|Cons for Instant ||Cons for Deferred
- Current state is ambiguous if you request state change while inside a state's update function.
- Unpredictable state snapshot because transition can occur at any time while that snapshot is being affected by other systems and entities within the game world. In other words, the order within a for or while loop can yield very different gameplay results.
- Bugs that come from this may not be 100% reproducible due to the many factors that govern order of processing within an iteration loop of the different objects and systems within the simulation.
- Call stacks resultant from state transitions will not include the actual request that instigated the transition since that happened sometime in the last frame
- Must deal with all issues stemming from mismatch of expectations between pending state and current state, such as requesting a state change when one is already pending.
- Dealing with the issues above require cooperation and buy-in to the deferred mentality from all programmers interacting with the state machine.
Joe and I had a debate about this early in the project. I was in a hurry and wanted a simple call stack. He insisted that we’d pay for that simple callstack down the line with very difficult and subtle bugs. His explanations detailed those listed above. After that, I was convinced that deferred was the way to go. There were other types of bugs coming from adopting deferred transitions (listed in Deferred Cons above), but in the end, it was the right decision.
A bug in which the state is wrong is much easier to fix than a bug in which a state is sometimes partially wrong depending on the order that objects get handled within a loop.
If you happen to find this explanation a bit too simple, you can read deeper thoughts on this subject from the source:
I also wrote a somewhat robust Deferred FSM here:
You are welcome to download, pillage, and modify that code (and its comments) in whatever way suits your needs.
Lesson #2: Favor Blending Forces Over Logic Branching When Driving A Vector
Have you ever had a direction vector that was influenced by many things? Maybe when X happens, the vector is in one direction and then when Y happens, the number is smaller and/or in another direction?
Instead of making a state machine or mess of if-else statements to determine the value of that floating point number, consider visualizing a force that represents X and a force that represents Y. Instead of turning those forces on and off at the right moments, make X and Y get stronger with proximity to relevance and gradually zero out when they get far enough to become irrelevant. X and Y can add up to reach an equilibrium.
Examples of Blending being better than all-or-none branching logic:
- Steering Influences, such as pathfinding mixing with combat formations
- Camera Influences
- Animation influences
Blending force model strategies are not the panacea for all situations. Sometimes, you honestly do need priorities to gate contributions from certain influences, but this is definitely a good place to start when designing any code architecture that must convert input from many systems into a single output.
If you are given a spreadsheet of situations by a designer that dictate the value of certain resultant numbers by row and column, then maybe it is time to consider a mathematical model that can be continuous from parameters 0.0 to 1.0 instead of an approximation governed by a table. A cooperating systems designer may resist at first, but eventually thank you for this.
This mental transition to start with force blending instead of turning switches on and off is now a natural instinct for me, but it initially required jolts of reasoning from various sessions in which I “borrowed Joe’s brain”.
Lesson #3: Step Mode
- Press a certain key and the game pauses
- Press that same key again and the game advances ONLY one frame
- Press that key as many times as you want to advance the game one frame at a time
- Press another key to resume the game
- Make sure the framerate is capped to 1/30 seconds per step delta or whatever you think normal simulation should be
- For a nice twist: Allow other debug tools such as “freeflymode” to work in combination with this in order to be able to teleport the player anywhere within a single frame.
This is an indispensable debug tool that I and other programmers ended up using many times to investigate aberrations in systems that drove state changes as well as anything else with very intricate events happenings between frames A and B. Joe is the one who introduced the value of such a utility to me after implementing it within our codebase.
Lesson #4: Seize Opportunities to use Square() Instead of Sqrt()
It’s the little things.... This habit finally sunk in after numerous code reviews.
Square root is a more expensive CPU operation than multiplication.
- SLOW:if ( myVector.Length() < myValue )
- FASTER:if ( myVector.LengthSquared() < Square(myValue) )
The difference in cost between Square and Sqrt (hidden within myVector.Length) is meaningful enough to show up on profilers running benchmarks on 2012 hardware.
Those uses of Length instead of LengthSquared can add up, and so I now favor this pattern when doing distance-related comparisons as well as interpolations that allow quadratic ratios instead of linear ratios.
Lesson #5: Supporting Data Inheritance is Critical
One way in which programmers use class inheritance is to avoid the need to copy and paste code when sharing functionality between derived class types.
Systems designers have the same need to do this with the complex balls of data they create. They make a ball of data that represents a base object, and then one or more other balls of data might represent derived versions of their original ball that inherit properties from that ball or one of its ancestors.
Programmers have a choice of using inheritance or composition to share functionality between objects. Designers are sometimes only given the choice of using composition to manage their data.
A lack of support for data inheritance will result in designers copy-and-pasting large complex balls of data when they need to make a variant... and this will likely result in that odd bug that only happens in one level because they missed a spot or a spot changed in an ancestor somewhere up the tree that exists only in their head or wiki page at best.
The importance of data inheritance did not become a prevalent concept for me until Joe implemented it and others used it, including myself.
Being THAT Guy
I learned more from Joe than the five lessons listed here, but those other lessons are either too specific to the game we were working on or too esoteric for consumption by the general public.
Writing this memorializes my time working with a person that I consider the best game programmer that I have ever worked with. It crystallizes the lessons I’ve taken from him and reminds me that I am constantly evolving with the people around me.