Ponyo
yes, ponyo.
Preamble
This post contains transcribed segments from a development log that I’ve been updating for Ponyo: a C++ DirectX game engine that I’ve been working on as a side project since early 2019.
Motivations
My main motivation for this side project stems from a long-time fascination for game engine architecture.
“If I had 8 hours to chop down a tree, I would spend 6 of those hours sharpening my ax”
I was taking classes for a game programming module last semester. For our final assignment, we were tasked to build a game from scratch, I figured that this would be a good opportunity to test the game engine that I was working on.
Game programming
What is there to program in games?
Games are complex programs that involve components like graphics, sound, input handling, and so on. Games can be expressed in terms of simulations— running a game bears semblance to performing a series of simulations. These simulations are only made interactive by the logic that is defined in code that game developers write.
The thing is, game developers want to focus more on this logic; they want to spend less time worrying about the inner workings of the ‘simulation’ that their code is running on.
Game engines provide the technology and framework to address these concerns, so that game developers can focus solely on the gameplay; interacting with the low-level concerns through abstract APIs.
Architecture
Details matter. Contemporary concepts about the role of architecture in software are especially applicable to complex projects like games. One of the most fundamental concerns when programming games are representing game objects.
Game objects are spread across a wide spectrum of complexity: we have game objects that are player controlled, emit sound, involve collision, and so on. In other words, game objects have variable functionality, this functionality defines behavior that may be shared across different game objects.
Inheritance is evil
Ostensibly, the most direct way to achieve this is to build a few base classes that define base functionality/behavior to abstract off of. Game objects inherit behavior from these base classes and carry their own explicit implementation of data and logic. When building new game objects, we can easily reuse code from common components.
For simpler games that do not involve the management of many game objects, this architecture works. However, the commonly discussed side-effects that come with huge abstraction trees are endemic to game architecture as well.
One issue with the mono-behavioral nature that inheritance creates is redundancy. The more features/components entities have, the more complex the hierarchy becomes; game objects will become impossibly difficult to abstract upon without extensive code rewrites.
Universal management of game objects is another challenge. In order to achieve this in the scope of a tightly coupled game architecture, we would need to force a non-intuitive hierarchy structure which would likely involve tons of repeated code.
Don’t even get me started with multiple inheritance, which will probably happen in the cacophony of abstractions.
Down the road, these issues can be frustrating to deal with, which is why game architecture is a salient concern in this project.
The entity component system (ECS) architecture provides a data-driven, modular, and extensible solution to this.
ECS
With ECS, games objects are represented as entities. Entities are composed of components that hold information. These components are immutable and isolated units of raw data; they can only be updated within systems, which are responsible for component state updates.
This creates a highly decoupled architecture that favors composition over inheritance when assigning behavior to game objects. During runtime, data moves in one way and is updated by their respective systems.
Implementing components and entities was a relatively straightforward process. The main challenge was doing runtime dynamic typing in a statically typed C++, which luckily, was a perfect application of the curiously recurring template pattern.
struct BaseComponent {
// Base component memory management etc.
...
};
template <typename T> struct Component : BaseComponent {
static const ComponentCreateFunction createFunction;
static const ComponentFreeFunction freeFunction;
static const uint32 id;
static const size_t size;
};
I took a minimal approach to entities by representing them as a collection of components.
// pair (Component id, Component index)
typedef std::pair<uint32, uint32> ComponentReference;
// pair (ECSEntity id, components)
typedef std::pair<uint32, Array<ComponentReference>> ECSEntity;
These type definitions exist in the ECS
class, which mostly contains memory management utilities.
Putting everything together and deciding how systems will update components, and how that would tie into scene management was the difficult part.
Base game systems
Game systems register certain component IDs to listen for entities that fulfill their registered components. The beauty in this implementation is that the conditions are loosely defined through functionality in the components that constitute each entity. With this, code repetition was generally reduced across game objects with similar behavior, albeit in certain cases I still find myself repeating code in certain graphics-related operations for more complex entities.
I spent a little over a week working on the core game systems to do up a simple space shooter game. I ended up with 4 of said core systems:
Not all systems are updated at the same instance; some system updates take precedence over others. Some systems need to be updated within each draw call, while others needed to be updated before that. To resolve these conditions, I created a SystemList
class to organize and group related systems.
This was it. Constructing game objects and adding custom, replicable behavior was now a matter of creating custom components and systems and registering them under the ECS manager.
The final challenge in implementing ECS was scene management.
Scene management
Each game scene has components and systems that set up its respective game objects. In order for each game scene to run, memory needs to be allocated for all of its components through the ECS manager as it is attaching to the game.
I implemented a simple stack-based navigation system. The scene at the top of the navigation stack is the scene that is currently attached to the game instance. This is illustrated in the diagram below:
From here on, my ECS implementation was pretty much functional. Creating game objects in each scene became so much easier:
void attach()
{
gameSystems->addSystem(*menuShipSelect);
background = ecs->makeEntity(backgroundImage, titleAnimation);
title = ecs->makeEntity(titleSprite, titleAnimation);
spaceship = ecs->makeEntity(shipSprite, shipAnimation, shipSelectControls);
Scene::attach();
}
All we have to do now is to pass in components, these components can be reused across the project.
Programming game commands
In order to add support for game commands, I had to rewrite the keyboard input handler.
Issues
The input handler currently handles key events directly from the OS and maps it to a binary array. It also keeps a key state buffer from the previous frame to track if the key was just pressed.
Checking individual keys will require multiple interactions with the input class, I would have to check for all 4 key states with
isKeyDown()
,keyJustPressed()
, and heck, there are no getters to check for released states of keys!After determining the state of each key, we will then have to check them against each other to make sure that they form the correct combination. But because their values were accessed separately, we will never know for sure whether the states compared are from the same frame.
Goals
- Input is mapped to their respective game commands
- Game keyboard commands should be configurable (custom key mappings)
To achieve this, I created a simple keyboard input system that consists of:
Key state
At any given frame n, each key can be in 4 states:
Frame (n-1) | Frame n | Key state |
---|---|---|
0 | 0 | Released |
0 | 1 | Just Pressed |
1 | 0 | Just Released |
1 | 1 | Pressed |
Which we can represent with a simple enum:
enum KeyState { Released, JustReleased, Pressed, JustPressed };
Key binding
A key binding binds a given keyCode to a particular state.
struct KeyBinding {
private:
uint32 keyCode;
KeyState keyState;
public:
...
};
Game command
A game command contains a particular set of key bindings that define the conditions on which it should be activated.
struct GameCommand {
private:
std::string name;
std::vector<KeyBinding> chord;
public:
...
};
This way, all that needs to be done in the input class is to poll for whether the game command has been activated by checking the key bindings for every command against the existing key buffer.
Abstracting key binding data allows game commands to be easily configured as well.
At the end of the day…
I feel that through this project, I’ve learned more about the concerns of game architecture.
Spacewar, the game my teammates and I did for an assignment was made with an older fork of Ponyo. It’s honestly barely even a game, but I feel that it demonstrates the capabilities of the game engine, and I’m really proud of the animation system.
There is still a long way to go before any form of release, a good next step would be to switch from DirectX to a cross-platform graphics library. I shall continue to work on this as a side project.
🙌 Thanks for reading!
Update: project is now public on Github