/ Building A (Better) Game Engine

Building A (Better) Game Engine: Modeling State And Behavior

For about a year and a half now I've been working on a custom game engine I've been calling Gunship. My primary goals for Gunship are to use it as a leaning tool to familiarize myself with the complex inner systems of a game engine as well as use it as a testing ground to experiment with new game development technologies. While I've been content with the course of development thus far a recent attempt to make a game for Ludume Dare #34 caused me to realize that there are fundamental issues with the way Gunship requires gameplay code to be written. For the duration of Gunship's development I've been working under specific assumptions about how I wanted gameplay code to be written, but I think it's time that I start breaking down those assumptions and try to find something better.

Back To Basics

Let's take a moment to wax philosophical and ask "what really is a game?" I don't mean that in the ludology sense of "what makes a game different than a toy", I mean in terms of the software that runs on a computer. We may look at a game as a collection of systems, as rendering and audio and user input, or possibly as a feedback loop between player and machine. As game developers we tend to think about objects and their methods and the interrelation thereof. But at it's most fundamental a video game is an overly-complex system for transforming data[1].

We can conceptually break a game down into two parts: The game's state, which is some data that resides in the computer's memory; And the game's behavior, which performs transformations on that state[2]. As game developers our job is to specify what state data the game needs and to write the behavior routines that update that state frame to frame. When we're designing our games we think at a high level about objects and systems and how they interact to create pleasing gameplay, but those high level systems are always abstractions over the fundamental fact that we're transforming the game's state data.

That said, most of the time we're not concerned with those low-level details. When working on a game we are mostly preoccupied with high-level questions about how we architect our code and how to implement specific features. The low-level details don't come into play until we start optimizing our game, at which point addressing that fundamental model of data transformation becomes critical. As such we want our engine to provide an API that makes it easy operate at a high level of abstraction while still giving us a way to easily drop down to a lower level to tune performance when necessary.

So why are we so concerned then with the idea of modeling a game as a collection of state data and behavior routines? Because it informs the requirements for engine's API and helps us focus on the important details of the engine's design (at least with regards to how gameplay code is written). With that issue highlighted we realize:

  • The engine needs to provide a way to add new state data as it's created by the game.
  • The engine needs to provide a way for the game to specify which behavior routines should be running at any given time.
  • There needs to be a way for behavior routines to read state data each frame.
  • There needs to be a way for behavior routines to modify (meaning add new data, remove existing data, and modify existing data) state data each frame.

No matter how complex a game engine is or what type of game is being made those are the fundamental services that the game engine must provide. My goal for Gunship is to make it as easy as possible to do those things while also keeping the performance cost for accessing and modifying state data as low as possible.

Modeling State and Behavior

With the understanding that our games will need both some form of state data and some behavior routines we need to decide how we want to go about writing those in code. To do so we have a number of questions we need to answer:

  • What language are we using? Certain programming languages lend themselves more easily to some patterns than others. I am using Rust and so will be focusing on finding a model that works best for Rust.
  • How much control does the engine have? In most modern game engines (I'm looking at you, Unity and Unreal) the game code winds up being more like middleware that gets run by the engine rather than being the core of the application itself. When this is the case the engine provides infrastructure for managing state data and has to provide an API for creating, destroying, and accessing that state. While this is what is commonly thought of when talking about "game engines" it is also also possible to have the engine act as middleware that is loaded and run by the game. This is the model used by Piston. While this model is more flexible in that it doesn't enforce a single way of writing games, he same infrastructure for managing game state still needs to be built at the game level. As such I'm going to write this article as if the engine is providing those facilities but with the assumption that it is possible to pull it out of the engine if so desired.
  • What kind of game is being made? Also, is the game spatially-oriented (is it about traversing a virtual space, be it 2D or 3D; think platformers, shooters, MOBAs, etc.) or more simulation-based? Different games have different requirements, and while some styles of game code are flexible enough to be used for any game they are not best for all games. For example, the type of game model that works best for a spatially-oriented 2D/3D world game like Splatoon may not work best for a simulation-heavy game like Crusader Kings 2. I will be trying to find a system that is flexible enough to work with most types of games, but when tradeoffs need to be made I will err on the side of spatially-oriented game and note the choice I am making.
  • What are the performance requirements? This ties into the last question but is worth noting separately. Video games have always pushed the limits of current computing technology and as such performance tends to be a concern. Even with today's beefy processors and graphics cards in desktop computers we still face performance constraints for mobile devices and game consoles.

Deciding on a game model involves making tradeoffs in each of these areas. The model that, in my experience, strikes a reasonable balance in all areas is the Entity-Component-System model.

A Component-Oriented World Model

In a component-oriented system you create "entities" that can have a variety of components attached. Entities themselves don't have any state and are usually not the direct target of behavior routines, instead the components contain the state data and behavior routines run the simulation for one or more components each frame. Where in a more traditionally object-oriented setup you'd define a struct and give it different member variables to define its functionality, in a component-oriented system most of that functionality is composed dynamically at runtime:

// Only specifies the data for a bullet, other state data like position and
// what mesh it has are provided by other components like `Transform` and `Mesh`.
#[derive(Debug, Clone, Component)]
struct Bullet {
	pub velocity: Vector3,
	pub damage: f32,
}

// Dynamically construct a "bullet" by creating an entity and giving it
// all of the properties of a bullet.
fn create_bullet(velocity: Vector3, damage: f32) -> Entity {
	let entity = Entity::new();

	entity.assign(Transform::new());
	entity.assign(Mesh::new("meshes/bullet_mesh"));
	entity.assign(SphereCollider::new(0.1));
	entity.assign(Bullet {
		velocity: velocity,
		damage: damage
	});

	entity
}

In the component-oriented version there's nothing that statically declares that bullet entities will always have a transform and a mesh component, in fact there's no direct definition for a bullet entity; They're always composed dynamically at runtime to create ad hoc game objects.

This style is well suited for many games because it allows for game objects to change composition on the fly making dynamic game worlds easier to create. Of course all things come at a cost and here that cost is not knowing statically what the composition of your entities is. Whereas in the object-oriented version you can simply define the Bullet type as having a Transform as a member variable in the component-oriented version you can't be sure that the Transform ( or any other component) will be there. The possibility of a component's absence must be handled at runtime which incurs overhead even when, by the design of your game, you know that the component must be there. Depending on implementation you can mitigate this cost (e.g. in Unity you can cache off references to components) but the nature of a component system is to be dynamic and that will always be more costly than a more static alternative.

The Ideal Component System

I intend to explore in greater detail the implementation for a component system, but that's going to need an article of its own. For now I'd like to simply lay out an example of what an ideal component system might look like so that next time we can start tearing into why that won't work and what we can do.

In an ideal system creating and accessing components is simple and sticks as close to idiomatic Rust (or whatever language you're working in) as possible. For example:

let entity = Entity::new(); // Construct objects with `new()` like normal.
entity.assign(Transform::new()); // Assign directly to the entity.

let mut transform = entity.get::<Transform>(); // Get components directly from the entity.
transform.position = Point::new(0.0, 0.0, 0.0); // Directly access component members.
transform.translate(Vector3::new(1.0, 2.0, 3.0)); // Call methods on components.

entity.destroy_component::<Transform>(); // Destroy components directly through the entity.
entity.destroy(); // Destroying the entity destroys all components and consumes the entity making it unusable.

The behavior associated with BulletComponent might look something like this:

impl Behavior for BulletComponent {
    fn update(&mut self) {
        // Retrieve the transform for the bullet and move it based on the velocity.
        let mut transform = Transform::get(self.entity).unwrap();
        transform.translate(self.velocity * time::delta());

        // Slow down the bullet slightly over time. Should loose 10% speed per second.
        self.velocity *= 1.0 - (0.1 * time::delta());

        // Check for collisions and apply damage to the first one with a health
        // component. Destroy the bullet once it has collided.
        let collider = Collider::get(self.entity);
        for other_entity in collider.collisions() {
            if let Some(mut health) = Health::get(other_entity) {
                health.health -= self.damage;
                self.destroy();
                break;
            }
        }
    }
}

With this imaginary API it's easy to get at any component data, add new behaviors to components, and destroy entities and components at any time. Unfortunately it's also completely impractical, though that's a discussion for next time.


  1. "Transforming data" simply means "making changes to data" but generally implies that new values are based on the old values (e.g. 'foo += 12.0 * time::delta()' rather than 'foo = 12.0'). Producing new data based on the old data is key to giving games a persistent state. ↩︎

  2. You could include in this model that there must be some way to display the game state to the player, since the feedback loop between the player's input and the game's output is the very core of gameplay, but I find that it's not relevant to the discussion at hand. The architecture that works for writing gameplay code will also work for the rendering and audio systems so we don't need to consider those directly. ↩︎

David LeGare

David is a game developer and systems programmer. He likes cats, he tolerates dogs, and he often remembers to dress himself before leaving the house.

Read More