Superdyne Common Architecture
2010 - 2012As part of working on Nitronic Rush, I worked with another technical director/architect (Robert Onulak) of Team Nitronic’s sister team, Disco Tank, whom made the game “Solstice”. We created the “Superdyne” architecture to be used by both teams. Superdyne was additionally used in a couple of projects by team members.
Following is a list of most of Superdyne’s features. Listed in bold are the ones on which I was a significant part in creating.
“Too Long; Didn’t Read” list of what I worked on: Action list, internal hierarchical profiler, memory debugger, function binding, limited Lua binding, RPC and the ToolServer, C++ Reflection, and Serialization.
- Action List: Alternative to a state machine for describing behavior. Every act() call executes the first action found in the first non-blocked group. Execution continues until no more non-blocked actions are available. Groups can be blocked/unblocked/modified while the list is executing.
- Containers:
- Handles: Used primarily by GameObjectHandle.
- RefString: Holds only pointers to a printf-style format and arguments. This is used to avoid needing a temporary, local buffer to write into, only to be written into the actual destination buffer shortly thereafter.
- Templated Static Array: Basic static array with some out-of-bound access protection.
- String: Late-copy string. We still primarily used std::string in most game code, though.
- Core: Handles program state, ISystem registration, frame-rate control, and game pause/unpause.
- Debugging macros: Provides build-based compiled-out macros such as DebugErrorIf and ReleaseErrorIf. Also had severity-based error and warning macros such as gerror; these were mostly glorified fprintf wrappers.
- Client Variable: Very similar to Quake's CVAR system. A global place to register variables fo any basic type (plus strings) for access in another area. These can also be flagged as data to be saved in a per-user file.
- Internal, Hierarchical Profiler: Alternative to an existing profiler in Superdyne. Can write out all profiled information for a given frame, or just "spike" frames where a drop in performance beyond a specified threshold occurs. In Nitronic Rush, this was set to print debug information to the screen on any spike frame, where it listed which profiled items were most to blame. This profiler is nearly identical to a homework assignment in CS391: "Code Analysis and Optimization" at DigiPen.
- Memory Debugger: Can replace all allocations globally, or just specific ones. Immediately detects any overflow or underflow read/write attempts. Uses Windows OS features to do this (and consumes a great deal of memory in doing so; two pages per allocation it watches). Drop-in copy from an assignment for CS391: "Code Analysis and Optimization" at DigiPen.
- PrintLastOsError(): Just prints to stderr the last Windows error message. Because doing this manually is tedious.
- Factory: Creates GameObjects based on "blueprints". I worked on two of the three implementations of blueprints and serialization we used with the Factory over the course of the primary projects using Superdyne.
- Template-Based Function Binding: Written towards the start of my Sophomore year at DigiPen. Used by bLight (Sophomore year team project), Superdyne, and Fragment (another Junior year project, but one I had no other part in). This system was extremely useful for Action Lists, our message systems, and property reflection. However, this implementation is a hefty amount of code that is not trivial to read or follow. If a compilation error occurs involving it, figuring out what's wrong can be challenging. I'm also no longer satisfied by its speed. The general idea works well -- this implementation is somewhat similar to what flOw by thatgamecompany used -- but it should be preferably re-written before being used again.
- GameObject, ComponentManager, and Component: Game objects can have as many components as desired, and even multiples of the same type. Components are automatically registered/unregistered with their respective managers on GameObject initialization/de-initialization. If you just want a component to be updated once each frame, a special UpdateComponent is available to inherit from.
- Level, GameStateManager: GameStateManager switches between Levels when requested, or quits the game.
- GhostEngine: Wwise-based audio engine.
- Input: Uses XInput for gamepads, and RawInput for keyboard, mouse, and 3D mouse support. Differentiates between developer input and user input for easy toggling on/off of developer hotkeys in one location.
- Interpolator, PolyInterpolator, SplineSystem: Templated, pausable variable interpolators
- Lua binding: For my own part, I mostly wrapped up the Lua state in a nicer C++-style interface, and used a mix of tolua++ and my own function binding for binding to Lua. This old system is barely in use. Nitronic Rush in particular uses almost no Lua. In earlier stages of development, our level file was just a Lua script written by a Blender "exporter" script that would create and modify all the level objects. A newer Lua binding system was written by Robert Onulak, based on John Edwards of thatgamecompany's template meta-programming lecture.
- Math library: Matrices, vectors, quaternions, and some basic AABB-oriented classes. Physics and graphics were not part of Superdyne, so only the most basic, shared math is implemented here. By the end, because of the shared Transform component yet fairly different uses of it between the two games, that component ended up being fairly bloated.
- MessageSystem, and IMessage: An underlying template-based function-bound message system, with an object-based top layer. The underlying function binding is my old system from the Sophomore year. Used by four games in total.
- RPC and the ToolServer: We only used this towards the start of our projects. At its peak, it allowed remote procedure calls on a one-to-one basis. Lua was one of the possible receivers, which was very handy for quickly trying things out on a game. Originally it was intended to allow levels to be edited by our designer while people were play-testing. In the end, the most memorable things it allowed were faster iteration on some scripts (mostly for Solstice), and launching Minecraft on someone else's machine. We just didn't have much need for this.
- C++ Reflection: Four versions of it. The oldest reflection system was from Sophomore year, and based on the template-based function binding code from that year. The second was in use only briefly, while we tinkered with the possibility of using wxWidgets. It had special considerations like proxy-ing the actual data being modified, and the ability for data to refuse a change that wasn't valid. The third was based on John Edwards of thatgamecompany's template meta-programming lecture. The final one (used today to populate AntTweakBar bars for the level editor in Nitronic Rush) is a super straight-forward visitor-style interface. This can be used well for things like serialization and debug output, as well. It has some specializations for common math types in Superdyne, and doesn't have the code generation overhead or potentially confusing compiler error messages the template-based reflection had.
- Serialization: Three versions of it. Originally tried to just use Xerxes-C++ Parser. This ended up being a terrible idea, as Xerxes feels more aimed at business applications (it's very strict and good at verifying XML data). Second was a very brief play with JSON. Finally, ended with the "Superial" (Superdyne serialization) system. Uses TinyXML to parse our XML data (mostly level files and game object "blueprints"; TinyXML because it's small and good for games). Once TinyXML parses the data, I move it over into a SuperialDataMap, made up of SuperialValues. "Superial" covers a smaller set of features than XML, but plenty for our purposes. SuperialValues are the primary means of passing data around to components being serialized. Robert Onulak wrote another layer on top of Superial to simplify working with it. This hides more features, but the underlying Superial data can still be accessed when those features are needed.
- Singleton: Template-based means of defining a singleton type and its "lifetime policy".
- Basic threading library: Thread, Mutex, Lock, ActiveObject, ThreadPool, and WorkerThread types. Not used by Nitronic Rush.