3 Nov 2007

The Nebula3 Application Layer

Nebula3's Application Layer provides a standardized high-level game framework for application programmers. It's the next version of Mangalore (Nebula2's application framework) integrated into Nebula3. N3's Application Layer is the result of years of refactoring (one could say that this started with our very first big project "Urban Assault" 10 years ago). Mangalore has now reached a rather stable state, where no "big" design changes are intended in the foreseeable future. Thus programmers familiar with Mangalore will immediately find their way around in the Nebula3 Application Layer.

Mangalore had officially been started in 2003 when we learned the hard way that it is critical for an independent developer to work on several titles for several publishers at once to spread out the risks (German publishers have the unfortunate tendency to go nearly bankrupt quite frequently). So one very important design goal for Mangalore was to provide a common game application framework for a company where several teams work on game-projects of different game genres, and most importantly, a game-framework which enables us to do small - yet complete - games in a very short time-frame (our canonical "most minimal project" has a 3 month production-time-frame, and a team size of 5 people full-time (2 programmers, 2 graphics guys, 1 level designer), plus a half or third of a project manager). To make a long story short: building several "small projects" in parallel is many times harder then doing one "big project" because there are many parts of a game-project which simply don't scale down with the overall project size. On the content side, our strictly standardized asset pipeline in the form of our toolkit was the precondition for our multi-project strategy because it enabled us to switch modelers and animators between projects with practically no overhead (for instance it is not uncommon to build all graphical assets for a small project in only a few weeks, to directly share character animations between projects, or to have key people (like for instance character animators) help out in another project for a few days).

Mangalore (and now the N3 Application Layer) basically tries to solve the same problems in the programming domain, to enable our application programmers to start a new project with very small setup-overhead, and to help implement its game-play features on-time and on-budget. The following bullet points are probably the most important Mangalore features:

  • provide a fully functional generic "skeleton application" so that something is visible on the screen from Day One of the project
  • integrates with our standardized level-design and modeling work-flows, and thus provides a common "social interface", mindset and vocabulary between our programmers, modelers/animators and level-design guys
  • has very few, very strictly defined concepts how an application should implement its game-specific code (it's relatively easy for a Mangalore programmer to decide the "where" and "how" questions when faced with the implementation of a new game feature)
  • it's modular enough to enable a team of programmers to work on the game-logic without stepping on the other programmers' feet too much
  • "New Game", "Continue Game", "Load Game" and "Save Game" are standardized features (that's an important one: Load/Save works right from the beginning, and mostly without requiring the programmer to write a single line of load/save-related code)
  • provide a growing set of reusable modules for future projects (we formalized this a bit more in the N3 Application Layer compared to Mangalore by introducing so called "GameFeatures")

The Application Layer is built around the following basic concepts:

  • Entity: a game object, container for Properties and Attributes, manipulated through Messages
  • Property: implement per-entity aspects of game-logic
  • Manager: implements global aspects of game-logic
  • Attribute: key/value pairs representing the persistent state of an Entity
  • Message: used for communication between entities
  • GameFeature: groups Properties, Managers, Messages and Attributes by functionality

An Entity represents a single game object, like an enemy space-ship, the player avatar, or a treasure-chest. An important aspect of the Application Layer is that the class Game::Entity is not sub-classed to add functionality, instead Attributes and Properties define the actual game functionality of the entity. We learned very early that representing game objects through a class hierarchy in a complex game project brings all types of problems, where spaghetti-inheritance, fat base classes and redundant code are just the obvious ones (let's say you do a traditional realtime strategy game with vehicle units, so you create a Vehicle class, and derive a GroundVehicle and an AirVehicle class, which can implement navigation on the ground and in the air respectively, but now you want to add combat functionality, hmm... create a CombatGroundVehicle class, and a CombatAirVehicle class? Or would it be better to create a CombatVehicle class and derive GroundCombatVehicle and AirCombatVehicle? But wait, some of the vehicles should gather resources instead of fighting... you get the general idea, I think every game programmer faces this problem very early in his career).

That's where Properties come into play. A property is a small C++ object which is attached to an Entity object and which adds a specific piece of functionality to that entity. For instance, if a specific entity type should be able to render itself, a GraphicsProperty is attached, if it should be able to passively bounce and roll around in the game world, a PhysicsProperty is required, if an entity should be able to control the camera, a CameraProperty is added, and so on. Or for the above strategy game, a GroundMovementProperty, AirMovementProperty, CombatProperty and a GatheringProperty would be required, which are combined to create the functionality of the different unit types. Ideally, Properties are as autonomous and independent of each other as possible, so that they can be combined without restrictions (in a real world project, some Properties will always depend on each other, but experience has shown that the resulting usage restrictions are mostly acceptable). Properties pretty much solve the inheritance-problems outlined above. A new entity type is not implemented by subclassing from the Game::Entity class, instead new entity types are defined by combining several specialized properties.

The next problem we need to solve is the interface and communication problem. Properties need to communicate with each other (either with Properties attached to the same Entity, or with Properties attached to other Entities). Calling C++ methods on other Properties would ultimately introduce a lot of dependencies between Property classes which we want to avoid because this would very soon result in the same set of restrictions we just solved by fixing the Entity-class-inheritance-problem with Properties. Virtual methods would help a little bit because it would limit the dependencies on a few base classes, but this would just move the inheritance problem from entities to properties. Messages are used to solve this dilemma. Think of a Message as the next abstraction level of a virtual method call. While a virtual method call requires that the caller knows a target object and a base C++ interface, a Message just requires the caller to know the target object in the form of an Entity but no specific interface. The Entity will route the Message to interested Properties which will ultimately process the Message. The caller usually doesn't know which Property processes a Message, or whether the Message is even processed at all.

A C# programmer would now raise his arm and remark that this is exactly what Interfaces, Delegates and Events are for. And of course he's completely right. Alas, we're working in old-school C++. There is however more room for optimization in the Application Layer in the future (for instance, Message dispatching could be optimized by implementing a delegate-like mechanism).

Attributes are typed key/value pairs attached to entities. Each entity has a well-defined set of attributes which completely represents the persistent state of the entity. Attributes are used to describe the initial state of an entity, and to save and load the state of an Entity using the standard Load/Save mechanism of the Application Layer. Usually, a specific property is responsible for a subset of the entity's attributes. As a general rule, a specific attribute should only be manipulated by one property, but may be read by everyone.

Many features of a game-application don't fit into entities and are better implemented in some central place (the overall code design of some of our early projects suffered a lot from failing to understand that NOT EVERYTHING is an entity). For instance handling input or controlling the camera in one central place is sometimes (often?) better then spreading the same functionality over several Property classes, which then may need to spend a lot of effort to synchronize and communicate with other properties). Those global aspects of game-logic are put into subclasses of Manager. Managers are typical Singleton objects and usually provide services which need to be available "everywhere" in the game code.

Finally, a GameFeature is a new concept which didn't exist in Mangalore. A GameFeature is more or less just a nifty name for a bunch of source files which compile into a static link library consisting of related Properties, Managers, Messages and Attributes (all of them optional). GameFeatures are mainly an infrastructure to encourage and standardize code reuse between projects. The idea is that at the beginning of a project the lead programmer starts by picking and choosing from the existing set of GameFeatures those which suit his project, and (ideally) to group new functionality into new GameFeatures which may be of use for later projects. The Application Layer comes with a small number of standard GameFeatures, enough to implement a generic 3D application with a player-controllable avatar, chase camera and passive physics for environment objects, but it's really intended for more complex stuff, like a dialog system, a generic inventory system, or lets say, the guts of a typical "horse riding game" ;)

The Application Layer isn't perfect and doesn't solve all problems of course. For instance it may be difficult to decide whether a piece of functionality should better go into Properties or into a Manager. Also, a typical problem we are frequently facing is that functionality is split into too many, too small parts which results in too many, too fine-grained Property classes, which in turn requires too much communication between those properties. So a project *still* needs an experienced and pragmatic lead programmer which lays down the basic infrastructure of the code at the beginning of the project.

No comments: