Things That Always Suck
By Kyle Wilson
Saturday, February 07, 2004
There are a lot of hard problems in game development. The ones I listed in The List are really just the tip of the iceberg. But most of the problems we game developers deal with are reasonably well understood, and have known solutions that aren't altogether terrible. If you want to implement a terrain system, or a pack-file system, or a water simulation, there are resources available and there's sample code you can study. You have to invest some time in research, design and coding, but in the end you'll probably end up with something that works pretty well.
Then there's the other kind of problem. These are what Peter DeGrace and Leslie Stahl refer to as "wicked problems." Wicked problems have no clear good solutions. The solutions that are implemented tend to produce unforeseeable side effects over time. Those solutions are spread throughout the code base so as to be difficult to change or remove. And having solved a problem once, in one environment, is little help in solving the problem again elsewhere.
They call them wicked problems. I call them things that always suck.
Localization is easy, right? You blew it on the first game, because no one on the team had ever had to deal with localization issues before. It didn't occur to you when you were trying to create a game for the first time that people would also want to play it in German, French and Japanese.
But it doesn't seem like it ought to be that hard. You identify every bit of text that's going to be displayed to the player. You keep that text in data, not code, and you make sure to store it in wchar_t rather than char strings. And you identify any textures that have text in them--button labels and the like--and put them somewhere where you can switch between them when language changes.
If you're making a game with no spoken dialogue, then you're done. But most games these days have characters who talk to each other and to the player. For voiceovers, the job is still relatively straightforward: you get all the scripts translated just like you do with your displayed text and you hire voice actors in all the countries you're localizing for to re-record them in their own languages.
But for cinematic dialogue, things are more complicated still. If you have your animators hand-animate mouth movement for speaking characters, then the characters' lip movements will be wrong when foreign dialogue is substituted in. You're better off licensing the TalkBack library from whichever scion of LIPSinc inherited it. With a tool based on TalkBack, you can automatically generate animations from your speech sound files. But that means you have large quantities of animation data to generate and tweak in multiple versions, and makes it more imperative than ever that you get your scripts finalized, translated and recorded as early in development as possible. So your data build process and your whole production schedule are affected.
And consider the size of the text! Everybody knows that a sentence in German will be longer than the same sentence in English. You need to budget for that when you're allocating screen real-estate for text. But you also need to account for it in your cinematics. You have two choices:
- Make your foreign voice actors hold their lines to the exact time of the original English recordings. The localized dialogue will sound odd because it doesn't fit the natural flow of speech.
- Trigger camera switches and character actions in cinematics based on when characters stop speaking. Cinematic tools become more complicated and non-lip-sync animations won't be properly matched with speech.
So, in sum, localization sucks. It looks like one problem, but it's really a whole host of problems. The problems involve tools, data management, interface design, sound and so forth. The solutions to those problems are often unsatisfactory. And those solutions typically have far-reaching consequences that extend throughout the development process.
"Oh, but that'll break in split screen," is something I hear at least once a month. Which is strange, because MechAssault played just fine in split-screen mode, and it shipped a year and a half ago. That functionality should be done, right?
Supporting split-screen play on consoles is another thing that seems like it should be easy--just set the camera, set the viewport, re-render the scene--but somehow just isn't.
You set the camera to player two's location. But there's a lot of stuff that depends on the camera. Up at game-object level, you may want to reduce execution time by not processing entities that are out past far-plane distance. But now you have to check every visible camera to determine whether an object is close enough to any of them to be updated. Do you have a culture system generating foliage in some radius around the camera? That's another system that'll have to be smart enough to merge and fade shrubbery based on its distance from any camera, and do it quickly and efficiently. If you have a sky dome, you need to snap it to the new camera location. If you have multiple levels of detail for models, you need to pick the LOD to display before each camera renders.
You set the viewport. But there's a lot of stuff that depends on the viewport. If you're doing full-screen passes for stencil shadow rendering or light bloom effects or depth-of-field, those all need to be done for each viewport separately. And you need to take the viewport size into account when creating lower-resolution copies of the frame buffer for blurring in the latter cases. You also need to take the current camera and viewport into account when dealing with HUD placement and crosshair alignment.
Any one of these problems in isolation is trivial. Together, they help turn clean engine designs into a tangled mess. And like localization, they're not something you can ever afford to check off and forget about.
I wrote a bit about serialization a long while ago. I'm afraid that I haven't figured out how to do effortless serialization since.
Any approach to serialization is, almost by definition, going to weave itself inextricably throughout your code. After all, a great deal of data needs to be preserved, and that data is distributed throughout your code.
There are two exceptions to that rule. One approach which lends itself to a tight and tidy encapsulation is to simply capture the entire memory state of your executing process and save it out intact. This is likely to produce extremely large save files, and there are presumably numerous hardware headaches to overcome, though I can't say for sure since I've never implemented such a scheme. And such an approach is clearly unsuitable for replicating object state over a network.
Another option which is poorly suited to handling save games and not at all suited to handling network communication is journaling. That is, if you have mini-levels or checkpoints that you can quickly reproduce, you can "load" intermediate saves by snapping back to the previous checkpoint and playing an accelerated input log to the point of save. On the bright side, saving a game should be extremely quick. Unfortunately, even if you turn rendering off, "loading" is likely to take almost as long as just playing the game again up to the point of save.
So we're left with various permutations on adding Read and Write calls to every class in your engine with state to be preserved. If you're lucky, you can get away with just having one Serialize call that identifies data requiring network replication, saving, exporting, or UI editability. Then you only have to add one function to every class in your engine, rather than the four we added at Cyan or the five we currently add at Day 1. Regardless, adding this functionality to an engine that doesn't have it is a tremendous amount of work. I spent the better part of six months retrofitting support for saving, loading, exporting and importing into our engine at Cyan, and while I think I could do it more wisely a second time around, it's still a vast amount of typing, to say nothing of all the minor problems of preserving object relationships, ordering, and so forth.
But if you aren't satisfied with the Brobdingnagian save files that copying memory state gives you or the glacial loading that journaling gives you, then no matter what you do--whether it's writing a new preprocessor to implement reflection or adding serialize calls or building templatized variable lists--you're going to have to go over every game-state variable in your engine and decide whether it needs to be saved to disk, transmitted over the network, or exported from your exporter.
And after you've done all that work, you'll discover that some of those decisions were wrong. And they'll keep going wrong, as new code gets written by people who aren't familiar with how your save code or network system works. So even if you implement the coolest, cleanest serialization system I can imagine, you're looking forward to an endless stream of bugs that say, "Hey, when I push box A and then save, it still hasn't moved when I load the game again" and "When I kick button B on this machine in multiplayer, it doesn't move on that machine over there."
So enjoy. You're in a world of suck.
Animation synthesis is different from the other problems I've mentioned, in that it doesn't have far-reaching consequences that spread throughout a game engine. In fact, the problem flow runs the other way. Localization, split-screen support and serialization are all easy-to-understand problems that you could completely define is a sentence. But what makes a good animation system is hard to define. What makes animation synthesis a hard problem isn't the way it affects your engine design, but the demands that different parts of your engine place on your animation system.
In its minimal state, an animation system starts by allowing a keyframed skeletal animation to be played on a model. Generally there are discontinuities between animations, so the system will allow one to be blended out while another is blended in. In some cases, however, transition animations from one primary animation to another may look better than just blending weights, so there should be some way to specify this. And some animations may only have specific points in other animations with which they sync well, so there should be some way to specify, "Hey, blend out the walk cycle and blend in the run cycle, but start doing the blend a second into the walk so we know the legs are lined up."
And for that matter, maybe you don't want a walk animation and a run animation. Maybe you're better off doing a slow blend from one to the other, but unfortunately they're different lengths since runners step faster than walkers. So in that case, you'd want to scale the play rates of both to align them and then do the blend, and support playing both animations at the same time. (See the 3-13-03 entry of Charles Bloom's rambles for more on this idea.)
But wait, there's more! You really probably want to be able to play more than one animation on a model at a time anyway, so you can do "running and shooting" or "running while damaged". But different animations should affect different parts of the body, so you want to be able to set weights per bone, per animation, so that your shooting animation doesn't make the legs move less while the character's running too.
And then you want the character's feet not to go through the ground, so you need to somehow support IK fix-ups to your post-animation position. And you need to lip-sync to sounds, so you also want to support blends between multiple static bone poses (think of a weighted blend between fifteen one-frame keyframe animations), all synchronized to a different clock than your main animations. After all, lip-sync animations should be kept in sync with the voice sound file being played. If you have any kind of real physics system, then you'll be expected to limit animated motions to prevent characters from moving their arms through furniture, and the interaction between keyframed animations and physics is a whole wicked problem of its own.
The other problems I mentioned all have simple problem statements, but unbounded solutions. Animation synthesis is just an unbounded problem. That may mean that, technically, it's not a wicked problem. But it's still something that always sucks.
Any opinions expressed herein are in no way representative of those of my employers.