The Zombie Engine DeconstructedThe BasicsCovering math library, utility classes, system classes, debugging tools, object system, resource management.When it comes to game development, most of the initial enthusiasm is usually focused on the visual part. Admittedly, graphics are the most attractive part of games, and usually the one that actually sells the product to publishers and customers alike. The results of graphics development are visible, fancy, and can be shown with pride to strangers.
This is probably why graphics renderers are the piece of game technology you'll find most usually covered in forums, projects and articles over the Internet. I know because I wanted to get into games development mainly because of this area. And this excess of enthusiasm led me to neglect a more fundamental building block of game development, the ones you may not even know about, unless you're seriously working in actual games.
Possibly, the reason is that these basics are usually assumed. Most development tools and environments include some libraries covering file management, math, strings, containers and other such classes and utilities. Standard libraries like the STL (for C++) provide you with a readily available toolbox of types, definitions and classes, covering a wide range of programming needs. This is why many times you can just jump into programming concepts, demos or simple games without having to give any of these much of a thought.
But are these really all you need?
(Example) Say you're creating a simple puzzle game, one where an image is split into square pieces that are shuffled randomly, and the point of the game is to move them back to their place by moving the pieces in a specific way.
This is a fairly simple exercise for you to make, and it is, in fact, one of the first attempts at game programming I would advise an unexperienced developer to try: No 3D, no complex interaction, no multiple processes, no internal logic. A simple problem and a simple set of rules and victory conditions.
Game complexity aside, just by creating such a simple example you would start noticing the convenience of having more than just a few standard classes to help you with. Surely, you would have to write the game logic, and some way to load and process the image, display it on the screen, and receive and process the point-and-click input from your mouse.
In order to program such a game, you could just allocate memory dynamically using standard memory methods, read the image from a file using standard file access functions, use a standard system timer, and create all required custom classes for the state of the game, including custom methods to save it to file.
There are a lot of different reasons why you don't want to use directly the standard classes and libraries, and I'm sure you could find even more of these.
- Code sanity: game code that uses classes, macros or functions with meaningful names and well-defined behavior is easier to write, read, test and maintain. WAY easier.
- Reusability: creating classes and functions specific for use in games makes it very convenient for repeated use, in the same or different projects, reducing redundancy.
- Encapsulation: you really don't want to think about memory leaks, resource referencing or empty pointers, when you are writing a game, so hiding these inside classes hides the complexity and exposes cleaner interfaces.
- Platform independence: this means your time, file, device and other such classes are abstracted from the actual, system-dependent classes, making code both portable and clearer.
If you're a programmer, I'm sure you could keep filling this list yourself, but the point I'm trying to make is: no matter how simple your project, make sure your basic stuff is in place before you even start creating a concept. There are things you will need eventually when writing your game, and having to think about them when you're already developing actual game systems or features is going to be painful.
And now, I'm going to summarize what these basics are, so that you can check if you have them in your code weaponry, before you even start thinking about triangles.
MathNumeric Types, Vectors, Matrices, Quaternions, Points, Lines, Planes, Angles, Transforms, etc.A complete and efficient math library is a must for any game engine worth that name. Practically every text book and demo framework will provide you with one that you can extend and complete as you need, and there are many such libraries available for free at different sites.
One such library should provide, at the bare minimum, structures for 2D and 3D math: and all required operations, conversions and tests. Also, it is highly desirable that it provides structures to represent standard 3D objects, such as boxes, spheres, etc. including their transformations and interaction tests. Collision detection is a complex module to build, but having a readily usable set of shapes and tests is a useful thing to have.
Another important feature, specially to support portability, multiplatform, and compatibility with third-party libraries, is some encapsulation of numeric types and operations for special data architectures (eg. SIMD), optimized implementations (fixed point, double precision), etc.
It is highly tempting to skip adopting a math library, using instead the one provided by another module. For example, many graphics and physics libraries come with their own math library included. My advice is to choose a math library that suits your needs, and stick to it. If you need to interface with an external module, prefer always converting data rather than creating dependencies with a library that is not under your control. The internal representation of numeric data in a math-intensive system (eg. physics) is probably optimized for some internal reasons.
Also, relying on the math library provided by an external API (eg. DirectX) will make your code immediately dependent on the use of that API, which is a bad thing in general. Specific APIs and middleware may come and go, but your maths should be as stable and reliable as possible.
UtilsStrings, Containers, Streams, Packing, User dataI've always found it funny that every engine I've seen (and Nebula was no exception) includes its own class definition for strings, arrays, dictionaries, and all such basic structures. Because the point of these articles is to avoid reinventing the wheel (unless you want to do it with another shape) I would try to use whatever library you can find that gets the job done and forget about it. You won't probably come up with a more optimized version of a hash table, so don't bother writing one yourself, unless you really need some very specific and custom feature.
Aside of these basic data types and containers, you should start thinking about defining your abstract stream interface, that you can later on use along with your file, network or memory system to read and write streams of arbitrary data types. This way, you'll be able to define the interfaces for resource loading and object serialization (see below) to use generic streams instead of files or memory, which will simplify your methods and make your life easier.
Think of packing, compressing and encoding as part of this basic utility library. The reason why is that you want to have a way of packing and compressing resources as soon as possible, so that you don't have to optimize for disk space, bandwidth or access speed later on. The Zombie Engine failed to provide at an early stage a proper resource packing system, instead relying on resource files (levels, models, textures, sounds, etc) being loaded from raw files on the hard drive. Don't make the same mistake, and work with packaged resources from the very beginning.
Finally, I'd create some simple way of providing text-based user data to your game in some way, in a standard, or user-defined form. In the Zombie Engine, and other projects where I've worked, such a user-editable data format was used to describe such diverse things as materials, render pipelines, character states, game actions settings, user-defined data templates, GUI definitions or localized texts. XML is a good choice because of its hierarchical nature, human readability and wide tool support. But it has some other problems, so you can choose other ways of doing the same thing, such as C-like structured files, INI files a-la windows, or whatever makes you happy. I would make sure the same format is used consistently across the whole project, specially if these data files are going to be processed into optimized binary data, which will simplify the process of data conversion and packing, and your personal sanity.
SystemMemory, Timer, Devices, Files, IOThese are several low-level systems that depend heavily on the specific operating system, and there are many subtle changes from one platform to another.
You need to account for memory management from the very beginning, or you'll be in trouble later. If there are going to be different memory areas, or you want to have a budget to allocate resources, you need that information available as soon as possible in the project. Games use to manage memory in an intensively dynamic way, which easily leads to fragmentation, and potential memory leaks if you don't pay attention. Having a proper memory manager in place before you create a single game object will make your life easier, will let you know where exactly your memory is being allocated, and will prevent errors later on.
Using time, access to external devices, managing files, and the all-important user input are some of the utilities that depend on how each specific system handles it, and you shouldn't have to worry whether a click came from a mouse button, or a multitouch screen. To allow proper portability, the internal working of these needs to be not only completely hidden from the client application, but also be able to provide access with consistent data format and ranges across all systems. So for example, proper file management would encapsulate file paths using some virtual file system that provides a common namespace for all possible implementation, and this includes hiding completely things like whether the system uses slashes or backslashes to separate path components.
When I talk about input here I mean raw input, that is, collecting input events from any input device your platform supports, and enqueuing them for later processing. Input handling, such as converting mouse clicks or key presses into actual events for the application, is a higher-level functionality. If you're able to collect and store the state of all keys, buttons and screen interactions, using a consistent representation (eg. a normalized coordinate system, to abstract the actual resolution) you'll have everything you need to start defining input handlers for your game.
I'm skipping here two important parts of modern games that are just too complex to handle as part of your basic utility library, although they arguably belong in this category: networking and multithreading. I'll talk about these in another post, for they deserve some individual attention each.
DebuggingAsserts, Logging, Profiling, Memory, ConsoleAlthough debugging, just like testing, is usually seen as something that needs to be taken care of only close to the end of a project iteration, having a functional and useful debugging system in place from the very first moment can make the difference between a succesful project and a project that may not fail but will painful, if not impossible, to maintain.
These utilities fall in the category of Risk Management, and you should look at them as the fire extinguishers of your engine. If everything goes as expected, these remain in place untouched, but the moment awkward things happen, or when you start having a lot of data loaded, you definitely need these.
Asserts and Logging are well known mechanisms to monitor the behavior of a program, and to catch unexpected, or unsupported, conditions. But here I need to make an important note though: although they are related in the sense that they provide useful information about what's happenning, and the state of the game, they are not to be mistaken, or used interchangeably.
One of the lessons you can learn from the Zombie Engine is how not to use Asserts as a way to complain about input data. Asserts are supposed to check for internal system conditions: allocated memory, array indices in range, stuff like that. They are not supposed to check for the validity of data, which is something you need to check anyway, and probably Log it if it's not valid. But you can't Assert something like whether a given file exists, which is not a condition, but a data dependency. In the Zombie Engine, there are lots of different asserts that check for data consistency, such as:
n_assert(this->parent->IsA(kernelServer->FindClass("ngeometrynode")));Profiling and Memory inspection are your other best new friends when it comes to performance and data size. As important as it is to log what's happening at any moment during an execution, it's just as important to know where is the most time spent, and how much memory is being used, and where. This kind of information will be critical to identify bottlenecks, and to track where all your memory is being sucked up. Remember that a game project usually is the result of conflicting parts trying to get more and more space and time out of what a frame allows: artists want bigger textures, while sound designers want better quality, but both require space. Budgeting frame time and memory space is an integral part of the design of a game. And Profiling and Memory managemtn will be your main tools in sticking to these budgets. You can even Log when these budgets are exceeded on either area, and you certainly should.
There are many ways in which you can define these, but one particularly effective is to define “watchers” which are arbitrary variables that you can use from different modules to track any kind of profiling you'd like to do, and then some ways to keep them in memory. The Zombie Engine uses these from nebula2, and also adds a visual inspector of such “watched” variables so that you can have an onscreen representation to track variations in these values, which are data-dependent.
Finally, the Console will be your way to interact with your low-level systems for most of the time. Until you can have some visual inspection of the state of the game simulation, or ways to enable or disable different options, you should be able to do this from an onscreen console, like the one used in many engines out there. If your engine is script-enabled, your console could allow any script command to be input, thus allowing some closer access to the engine internals.
The Zombie Engine inherited the console from nebula2, and it remains one of the most useful ways to interact with the core engine. But in order to make it really useful, you need another fundamental part of the system, as discussed below.
ObjectsMetaclasses, Namespace, Life cycle (instancing, releasing, reference counting), serialization, signaling, scriptingAn object system, for those of you who haven't heard the name before, is a way to describe the relevant objects in your engine (and in your game) and manage them as if they all belong to the same space of names and access. Ok, this may not be a very formal definition, but what I try to describe is that all relevant objects in your game (objects in the OOP sense) need to be described in a standard way. If you're like to work with Smalltalk or some other pure OOP environment, you don't need to worry, because you already have this. If you're into good old C++, you want a way to describe the interface of your objects, supported commands and properties, namespaces, saving and restoring, and other such things. If you don't have one of these object systems, you'll have to emulate all this functionality on your own.
If you're just creating some casual game, you can skip this if you want. But if you're into serious game development, you need an object system. By that I mean a way to describe what your game objects, resources and system look from the outside. This includes how they are named, created, referenced, persisted and restored, accessed and intercommunicated. These may look like a lot of things, and they are. But this is one basic need to you need to cover in modern game engines.
The object system in Zombie is inherited and extended from the one in nebula2. It defines metaclasses holding a set of commands, a kernel that acts as an object manager and is responsible for creating and destroying the objects, and putting them in a named hierarchy, which similar to a folder system, where nodes are objects belonging to this object systems.
Finally, there are objects that are managed by their own managers, including the all-important Entities, that not only are objects themselves, but are an aggregation of multiple components (which are objects of its own), and belong to a different namespace, namely the Entity namespace, where they are identified using a unique Id instead of a name. More details about this in a future article on Game Entities.
ResourcesReferencing, Loading, MemoryIf you're going to work with meshes, textures, sounds, animations, or any other such system-specific data, you need resources. These are objects (in the above sense, or not) that contain data that applies only to a specific system. For example, TGA files are to be used as textures, while MP3 files contain sound samples. These resources will contain basically all data that is not generated inside the game (or editing) framework itself. All art and sound assets are typical resources, and all of them are managed in a similar way.
You want to be able to reference your resources from your code, or your game objects, in a consistent way, access and load them, and track how much memory each kind of resource is eating up. A Resource Manager should provide these and other functionalities, and allow the client application to just claim and dismiss resources as they are needed.
ToolsResource processing: conversion, packing, exporting, viewingResources are not always generated in the most efficient way for the game runtime to access or use them. Many times you'll organized your data in folders so that you can easily access and edit it from your authoring tools, but this may not be in the most efficient organization or format for the game.
This is why having tools to collect, convert, optimize, pack, and do other operations with your resources is a strong “should”. And if you're generating data packages for versions of the game on multiple platforms, it is simply a must. Fortunately, you don't always need to create these tools, there are conversion tools for many different formats, but you probably want to automate the process using a data build script or a similarly automated process.
Finally, having a way to preview your resources is a necessity, specially when they start behaving in all the wrong ways in your application. During debugging, you'll want to make sure that the data itself is correct, so that you can discard that being the source of a bug. Specific tools to check and preview resource data should be always on your toolbox.
...
And this will be all for this “basics” post. You may have noticed a few things you didn't think were actually required to start building a game, but you'll have a problem if you don't have them from the very beginning. Which is why, my original advice remains: don't try to build a game engine from scratch unless you have to.
And if you have to, now you need what fundamental pieces you need, and may be interested in looking for them in standard libraries rather than making the effort to write them yourself completely. Trust me, it's not worth it.
In the next post, I'll be covering the first major system, which is not graphics but Input. Stay tuned!