The Curtain Problem
The thoughts and opinions expressed are those of the writer and not Gamasutra or its parent company.
I have a coding example that I think generalizes to a case I see a lot in games. It seems to be a knee-jerk reaction for a lot of coders (including myself, once) to cache state when we don't have to. Even before I was interested in functional programming I knew that duplicating data was a ripe opportunity for bugs, but it was playing around with Haskell that made me realize just how far you can go to avoid pushing state.
I think one of the reasons we tend to push state and cache calculations is because often it's a little less work to write the code in the first place. Adding a new flag and setting it is easier than adding a new function. A more complicated example - came across some code recently where the coder set up an stl map so he could quickly look up a few elements from a larger hierarchy of stuff. Probably less work than writing the recursive search; but now we have to maintain the map and the original data. And then there's performance. Nobody wants somebody else to look at their code and say, "Hey, why'd you do it this slow way?"
Back on Spidey 2, the designers were in charge of writing the scripts to handle fades, and because they're not programmers by trade, it got a bit funky, though I wouldn't necessarily have done better at the time, because it was one of those problems that seems simple but turns out to have some issues. Bugs kept cropping up relating to the fade curtain - it would fade to black and stay faded, or it would fade in too soon, or the like. And we'd go in and find the script that was doing things incorrectly and fix it, and then another bug would crop up a few days later.
The curtain was an example of how state can screw one up. You might initially set this up ("always use the simplest possible design") as two functions BeginFadingOut and BeginFadingIn which set a boolean isFadingOut. But then you get two chunks of code (not necessarily even on different threads) that both think they need to use the curtains and you get a mess - one'll start it fading in before the other one's ready.
One thing we tried, then, was incrementing and decrementing a counter. Better, and when a script failed it was because the script was clearly not using the system correctly, so it became easier to blame the scripts.
What I should have realized when we were doing these spot fixes of offending scripts was that the curtain mechanism itself was to blame. We were effectively pushing state...duplicating data. Whether the curtain was fading in or out depended on the state of the game. The curtain should have been pulling state. It probably should have looked something like this:
And then, what translucency to draw the curtain (or how high to draw it, if it's a more literal curtain) could have been determined simply by measuring how long ShouldIBeDown had been true. The functions that respawn Spidey or transition to different areas would wait on IsCurtainDown() before doing their work.
Now, you say, "That breaks encapsulation!" "That creates circular dependencies!" because the curtain is dependent on transitions and Spidey's respawn system and the cutscene system and they're all dependent on the curtain. Before, the curtain was a low level service they could all just layer on top of. I'd say - hey, I'd rather be conflated than buggy, and for this case it kind of makes sense for all these things to be conflated - but what about a more general situation, where some more encapsulation or layering would make sense?
One possibility that's C++ friendly is the curtain's ShouldIBeDown() function could be virtual. Another possibility, not so C++ friendly, would be to use some kind of lambdas - function pointers or functors or delegates or something like that. You could add a list of curtain-closing conditions this way.
Speaking of delegates, with Schizoid I was using delegates like candy, and one particularly bone-headed thing I did that I later stopped doing once I realized how bug-prone it was: changing delegates mid-stream. I had some enemies that would change their minds, and tried to implement that, at first, by changing their whatDoIWantToDo delegate. This is pushing state - the curtain problem all over again. Instead of having one function tell the entity to do one thing by pushing one delegate and having another function tell the entity to do the other thing by pushing another delegate, I needed to put that all inside a single function if( condition() ) doOneThing() else doTheOther(). If I had a rule of thumb for delegates or function pointers now, it would be - you can pass them in constructors but never change them once their object is live. So that's what I did, and I lived happily ever after.
The value of avoiding state is something that the how-to-program books I've read never seem to discuss. The "Don't Repeat Yourself" chapter of The Pragmatic Programmer touches on it at the same time as it tells you not to copy and paste code, but that's the only one I can think of off the top of my head. Whole engines are built around stateful paradigms: for example, the animations that execute scripts when they get to certain frames in Flash, and the tween-happy Coco2D. And that's crazy to me. Unlike QA, unit tests, pair programming and code reviews, avoiding state keeps bugs out of your code in the first place. I personally think it's more important than avoiding magic numbers or properly declaring things 'private' or 'const'. What do you think?
(Originally printed here. If you found this article interesting, please help me out on Greenlight.)