What, and Why?
Ever wondered what makes Unreal Engine, Unity, or Godot tick? That curiosity sparked my adventure into crafting a 2D game engine in C++. Inspired by Dave Churchill’s lectures at Memorial University, I set out to demystify the magic behind game development.
Architecture
The engine uses an entity-component-system (ECS) architecture:
- Entities are the actors on your game’s stage; game objects which can possess components.
- Components are the building blocks that give entities their unique traits; structures of data (and sometimes, functions) attached to each entity.
- Systems are the directors orchestrating how entities behave based on their components; functions that enact the game logic on each entity according to their components’ data.
For example, take the Player entity. It has components like Input, Transform, and Animation. The Input component stores key presses (e.g., pressing ‘up’ makes “up” true), and the Movement system, called every game frame, reads those inputs, updates the player’s velocity (from the Transform component), and changes their position according to this velocity.
I opted for ECS over traditional object-oriented architecture for its modularity, performance, and flexibility. Unlike the rigid class hierarchies of OOP, ECS separates the “what” (components) from the “how” (systems), making it much easier to extend and maintain, while keeping memory and CPU usage efficient.
Core Components
Scene Management
The scene management system organises the various states of the game, such as menus, levels, and game over screens.
Each scene is derived from a base Scene class, which provides essential functionality like action registration and rendering. The GameEngine class maintains a map of available scenes, allowing for quick access and dynamic changes. When transitioning between scenes, the changeScene()
method of the GameEngine class updates the current scene and ensures that the new scene is properly initialised.
This modular design not only enhances organisation but also simplifies the addition of new scenes, making it easier to expand a game’s content.
Input Handling with Action System
The input system is designed to be flexible and responsive, allowing players to interact with the game seamlessly. It utilises a mapping of keyboard inputs to specific actions, which are registered in each scene. The sUserInput()
method in the GameEngine class polls for events from the SFML window, checking for key presses and releases.
When a key is pressed or released, the system looks up the corresponding action in the current scene’s action map (a mapping of SFML key code to a string) and triggers the appropriate response.
This design allows for easy remapping of controls, ensures that each scene can define its own unique set of actions, and enables an action-replay system.
Movement System
The movement system is responsible for updating the positions of entities based on player input and physics calculations.
Each entity can have a Body component that contains properties such as velocity and acceleration. The sMovement()
method in the ScenePlatformer class processes input actions to adjust the entity’s velocity accordingly.
For example, pressing the left or right keys modifies the horizontal velocity, while the jump key changes the vertical velocity if the player is grounded.
The system also incorporates gravity, which continuously affects the vertical velocity of the player, ensuring realistic movement dynamics.
Collision System
The collision system is crucial for maintaining the integrity of the game world by preventing entities from overlapping inappropriately. It employs an axis-aligned bounding box (AABB) approach to detect collisions between entities.
The sCollision()
method in the ScenePlatformer class iterates through pairs of entities, checking for overlaps using their bounding boxes. When a collision is detected, the system determines the type of collision (e.g., player with tile, arrow with solid) and executes the appropriate response, such as pushing the player out of an object or destroying an entity.
This system is crucial for ensuring entities react with each other realistically, for example stopping the player falling through the floor, or detecting an arrow hitting another entity, causing it’s destruction.
Animation System
The animation system is designed to provide smooth and dynamic character movements. Each entity can have an Animation component that stores the current animation and its properties, such as whether it should loop. Player animations are linked to the player’s State component, which can take values including running, jumping, or shooting, ensuring that the correct animation plays in response to player actions.
The system assumes horizontally-organised sprite sheets, and uses the information specified in assets.txt (specifically, the number of frames the animation consists of) to display the correct portion of the sprite sheet each frame. This approach simplifies the management of sprite assets and enhances the flexibility of the animation system, enabling developers to easily adjust and expand animations as needed.
The system efficiently checks for animation completion and handles transitions seamlessly, allowing for features like looping animations or one-time effects.
By decoupling the animation logic from the game entities, developers can easily extend and modify animations without affecting the core gameplay mechanics, fostering a flexible and maintainable codebase.
Rendering System
The window, graphical rendering, and sound are handled by SFML, to enable me to focus on the higher-level engine features.
The rendering system is responsible for visually displaying the game world and its entities on the screen. It operates within the sRender() method, which clears the window, sets the view, and draws all entities in the correct order.
The system first renders background elements and then iterates through the entity manager to draw each entity using the renderEntity() method. Special care is taken to render the player last, ensuring it appears on top of other entities.
Additionally, the system includes options for debugging, such as rendering bounding boxes and a grid, which can be toggled on or off:
Design Considerations
A key design consideration was the implementation of collision detection and resolution within the Physics
namespace.
In my engine, collision detection relies on axis-aligned bounding boxes (AABB). To detect a collision, the overlap between two entities’ bounding boxes is calculated. If there’s overlap along both the X and Y axes, this indicates a collision.
To resolve the collision, the direction in which to push the entity must be determined — either along the Y axis (for example, when the player lands on a block) or the X axis (such as when the player runs into a wall). To achieve this, the overlap between the two objects in the previous frame is calculated:
To avoid code duplication, I implemented a helper function, getOverlapHelper
, which calculates the overlap for either the current or the previous frame based on a boolean flag. This function is placed in an anonymous namespace to limit its scope to the current file, ensuring encapsulation. Only the public-facing functions, getOverlap
and getPreviousOverlap
, are part of the Physics
namespace:
These functions rely on getOverlapHelper
to handle the core logic:
Future Improvements
Several enhancements are planned:
-
The collision handling in the sCollision method iterates through all entities in a nested loop, which can lead to performance bottleneck, especially with a large number of entities. Instead, I could implement spatial partitioning techniques (like quad-trees) to reduce the number of collision checks.
-
The player state management in ScenePlatformer uses a series of if-else statements to determine the player’s state based on velocity. This can become cumbersome and error-prone as more states are added. A state machine design pattern could improve clarity and maintainability.
-
For clarity, I have used strings to hold information, for example the names of animations, scenes, and actions and their types (i.e. start or end). However, a more efficient data structure to store the names of actions and animations would be an enum.
Next Steps
I’m excited to continue developing this engine by implementing are a level creation tool using a Dear ImGui-based UI, a save system, and an action-replay system.
I will then further generalise the engine to facilitate the development of a rhythm-based top-down shooter (think Metal: Hellsinger, neon geometric shapes, and house music!), for which I will also implement a particle effects system, global illumination using radiance cascades, and NPC pathfinding.
Acknowledgements
Assets used in the project include the Elementals: Leaf Ranger asset pack by chierit, Fantasy World Explosion pack by Kenrick ML, and Super Mario World block and coin sprites.