I’m still not convinced in the rightness of the thou shalt have no single GameObject school of thought. Having the objects present an uniform interface towards all objects present in the game world decouples many object-handling tasks from the internal representation of the object, the two chief examples being the editor and the script interface. We had to add very little code to the editor, for example, when we added new, different types of objects - grass, particle systems and terrain decals - for them the entire sets of operations such as move/scale/rotate, undo/redo, edit properties and save/load to file worked automagically just because they were simply GameObjects. We really have three classes of GameObjects - very lightweight, simple and numerous (think grass), lightweight-dumb-static (think trees and rocks) and full-blown (think units and buildings) - but 90% of the engine code and 99% of the game code doesn’t know the distinction between them. (And we use normal Lua objects, not game-engine living-in-the-world GameObjects for abstract gameplay entities like the economy simulation of the city, so that would count maybe as a fourth one.) So far the system seems to work OK, with significant advantages in code simplicity and memory footprint compared to the previous two systems we used. Two years ago we built a game around a single type of GameObjects, even for abstract gameplay entities, and there was an invisible “economy” object placed somewhere on the map. And seven years ago we had a full-blown OOP-textbook-madness, seven layers deep hierarchy of GameObjects, complete with a mess of virtual functions and semi-complete implementations jumping back and forth between the layers of the inheritance tree.
Archive for August, 2007
Seriously, who came up with the brilliant idea of storing the mip levels of a texture bigger-to-smaller after the header in a DDS file? This way, when you want to load just, say, mip level 256×256 and all the smaller ones, you need to do two disjoint reads from the file - one to parse the DDS header, and another for the mip subchain. Nothing would be simpler than storing them smaller-to-bigger - this way you’d be able to read with a single read operation, or, at worst, with two adjacent ones. When you’re trying to read asynchronously textures, the bigger-to-smaller order forces you to either keep a preloaded table of all the DDS headers in your game (which is a bad idea in many ways - it consumes memory proportionally to the entire data set of the game, instead of the currently needed data set, introduces additional asset build steps, and messes with the ability to let artists change texture formats and size on the fly while the game is running), or to two two asynchronous reads, doubling the latency for textures appearing on the screen.
I try hard not to be one of those not-invented-here guys who insist on having their own data processing tools and file formats for everything, but it’s not easy…
I’ve been thinking lately howВ a game programmer should have not the (hopefully) standard two, but four monitors on his PC. Besides the usual two for the game itself and his favorite IDE, he must have one for the generated machine code, because you can’t really trust a compiler even with the most basic of tasks. And one for his profiler of choice, to always keep an eye of what the game really is doing.
Real-world example:В a misunderstood feature for dynamic remapping of terrain layer textures leads to a per-frame resolving of texture filenames to device texture objects. Textures are looked up by filename, which must be handledВ without regard for case; and stored in a hash map. The unfortunate combination of three seemingly simple, convenient tools - dynamic reloading of the terrain table, a case-insensitive std::string wrapper, and a hash-map - lead to 40 000 calls to tolower() per frame. Not a big deal - nothing you’d even think about without a profiler. But here goes 1% of the total CPU time of a completely CPU-bound game - a percent which you’d have a hard time to shave off somewhere else.
Another one: an innocent, smallish 12-byte structure is augmented with a “dirty” flag, to keep track of yet another type of dynamically modifiable data. The structure is put in a three-dimensional array - which have to be banned outright, by the way. Then the array is “sliced” - iterated over (the wrong) two of the dimensions, and the dirty flag is checked.В This particular access pattern and the particular arrayВ and structureВ sizesВ lead to about of 4000 successive cache misses per-frame. Which take up to 2.5% of the frame time on the target platform, if you’re going for 30 fps. See it in a profiler, and spend 5 minutes to move the dirty flags to a separate array (or keep a “modified data” queue aside if you have 30 minutes), and shave off these 2.5%. Or don’t, andВ shipВ a seemingly bottleneck-free, but severely underpeforming game, full of such pearls of wisdom.