Table of Contents
- Writing down some code
- Everything is entity
- The smooth
- The file
- Jump higher
- Resizing the tilemap
- The easy stuff
- Appetite for Destruction
- Push and pull
- Data submodule
- A better editor
- Factories
- Commands
- Scripts
- Dying state
- Enabled flag
- Array of states
- Initial position
- Definitions and factories
- CI and automatic deploy
NcJump is a platform game project developed with a relatively new engine: nCine, an open-source C++ framework for 2D games developed by @encelo.
Some time ago, a bunch of members of the GameLoop community followed CS50's introduction to game development, and I though the sequence of games presented in this course could give me a good insight on what kind of game to create.
A couple of developers have already made or started to make the first few games listed by that course. There is a Pong clone made by @encelo, a Flappy Bird clone made by @Vasile-Peste, and a Breakout clone under development by @mat3. I opted for a Super Mario Bros clone.
The idea was to follow the steps of CS50's lecture 4 by using nCine instead of Love2D.
Some steps are required in order to start developing a game, but luckily everything you need to know is documented on the nCine download web-page. I installed nCine, cloned ncTemplate, and started working on top of it.
Writing down some code
The main thing to consider from a software design perspective is that I renamed the main class of the template MyEventHandler
to JumpHandler
, and introduced a Game
class. While JumpHandler
acts as a bridge between the game and the engine, Game
is the nucleus of the project and it is responsible for the lifetimes of its objects and the logic of the application.
Then I proceeded implementing an Entity
class following the idea of an Entity component system. The entity can have components like a graphics sprite, a finite state machine, or a body for physics and collisions.
A GUI is mandatory to quickly iterate on new elements as they are added to the game, both to have a visual feedback when debugging is needed and to tweak all sort of values and configurations at run-time. For this, I introduced an Editor
class, which makes an heavy use of @ocornut's Dear ImGui, also provided directly by nCine.
Box2D is my choice for 2D physics and collisions. It is made by @erincatto and it is very easy to use.
Everything is entity
I implemented some key Entity states: idle, move, jump, and fall. In my opinion they are the foundations of the whole gameplay and I believe the result is good enough to move on to the next steps. Then I spent some time on refactoring the Entity source code that were becoming too monolithic, and improving tileset and tilemap by using entities instead of raw sprites for both tile prototypes and concrete tiles.
An important consideration is that everything is an entity. Characters are entities, tiles are entities, triggers are entities, and so on. So how do you distinguish between them? Every entity can have multiple components, whether it is an image or a state machine or a physics body, therefore different set of components will determine all the different kinds of entities which will populate a game scene.
component | optional |
---|---|
Transform | no |
Graphics | yes |
Physics | yes |
State | yes |
Transform
The transform component is simply a wrapper around a ncine::SceneNode
. Everything else is supposed to be related to this node, so if we want to move the overall entity, we can just update the node of the transform component. It is worth mentioning that we use a unique pointer to store the node on the heap to make sure its memory would not move. This is needed as the nCine scene graph works with pointers.
Physics
The physics component as well is acting as an abstraction layer between the game and Box2D, it holds values like the maximum velocity, jumping strength, and so on. Most importantly it keeps track of a handle to a b2Body which is the mean to query the current physical state of the entity.
Graphics and State
At the base level we have a graphics component with one single image or animation, and a state component with one single state which never changes. These are possibly the most common among the entities of a game.
Now we need to consider the concept of a moving character with states like idle, move, jump, and fall. This set of states is also very common so we implement it directly in C++. The result is the following class hierarchy.
Subclass | states |
---|---|
SingleState | IDLE |
CharacterState | IDLE, MOVE, JUMP, FALL |
It follows that a CharacterState
class is closely coupled to its related graphics component, which we define as CharacterGraphics
, so the graphics components class hierarchy is quite similar to the previous one.
Subclass | graphics |
---|---|
SingleGraphics | IDLE |
CharacterGraphics | IDLE, MOVE, JUMP, FALL |
As a final consideration, while a state component should be unique for each entity, a graphics component could be shared between multiple entities so to avoid duplicating these kinds of heavy objects.
The smooth
I introduced a camera component, and serialization to load and save to file the state of the game. While the former was pretty easy and straightforward to implement, the latter required introducing a new dependency and various scattered refactorings to pay some technical debt accumulated from the previous development iterations.
That was easy. The camera component has two main attributes: the scene root node and the target node. The scene root node is needed because we need to simulate the camera, which means that what we actually transform is the scene root node. In other words, to move the camera in on direction, we translate the scene root node to the opposite of such a direction. The target node is the one the camera should follow. As the target node moves around the scene, the camera is updated to match its coordinates.
The quickest thing to implement a following camera is to just write down something like camera.position = target.position
, and while it works it is not the best we could do. A better and nicer approach, which might also prevent motion sickness, is to implement a camera with a smooth movement. This can be done by linearly interpolating the camera and the target position by a small factor at every update of the game.
void Camera::update() {
float smooth_factor = 0.125f;
Vec2f smoothed_position = lerp(this->position, target->position, smooth_factor);
this->position = smoothed_position;
}
The file
Conversely, this feature required a bit more work. I started by adding @nlohmann's json as a new dependency of the project for reading and writing to json files. I find it very easy to use in addition to the fact that I already used it for previous projects, like Jamspot: Sokoban. The second step was implementing functions to save certain objects to and load them from file.
void to_json(json& j, const T& t);
void from_json(const json& j, T& t);
The trickiest part was to make sure that all the serializable classes of objects could be default constructed. That means avoiding references as class members and preferring pointers, hence allowing semi-initialized objects while making sure they stay in this state for the shortest period as possible to avoid logic errors or even crashes.
Once all this has been taken care of, I was able to edit the game configuration, the tileset, and the tilemap at runtime, close the game, and re-open it to find everything as I set it before. That is key to work on the levels of the game.
Jump higher
From a gameplay perspective, the player gains confidence with the elements of interactivity of the game. It first moves, then jumps, then jumps higher. The implementation is straightforward thanks to Box2D. If the jump button is still down, keep applying a small force upwards. Keep in mind that force should be weaker than gravity, otherwise the character would take off into space.
void can_jump_higher(Input& input, Entity& entity) {
if (input.jump.down) {
auto force = b2Vec2(0.0f, small_amount);
entity.physics->body->ApplyForceToCenter(force, true);
}
}
Resizing the tilemap
This was less straightforward. I am starting to believe the more something is boring the more is difficult to implement. Resizing is not trivial, many things should be considered. For simplicity here I will focus on the concrete tiles grid only. At the beginning I used a simple vector to store all concrete tiles of the map and with a simple formula (y + x * height
) I was able to index a cell of the grid. Then I realized that resizing this vector was not convenient as the order of the tiles in the cells would not be preserved.
As a solution, I changed the structure from a simple vector to a vector of vectors, so we could say a list of columns. That simplified indexing as you could just use the subscript operator to access the right tile (tiles[x][y]
).
It also simplified resizing. If you want to change the width of the tilemap, you just modify the number of columns by calling tiles.resize(new_width)
. As you can imagine, if the width shrinks, you lose those columns, while if the width grows, you get new columns with a default concrete tile. I can not overstate the importance of default values. Changing the height is slightly different, as you need to resize all the columns.
for (uint32_t i = 0; i < width; ++i) {
tiles[i].resize(new_height);
}
The easy stuff
While a static entity is not affected by physical forces thus does not move, a dynamic entity is affected by forces, so it can fall and can be pushed and pulled around. Thanks again to Box2D, implementing this was just a matter of setting the type of physics bodies to either b2_dynamicBody
or b2_staticBody
.
Another easy but important thing to add was a proper background to give to players' eyes something more interesting to see than a boring solid color. I just added a static sprite, but I guess it would be nice to implement a fancy parallax scrolling effect in the future.
At this point, I realized that something was missing. Tiles and entities could be added and positioned freely in the scene and, as all normal people that like symmetry, I started wanting to destroy them, not by interacting through the editor, but as Mario does by punching (or hitting with his head?)!
Appetite for Destruction
Well, not every tile should be destructible, but some of them certainly could, and I supposed it could be a good idea to give this ability to the player.
I started by introducing a flag to determine whether a tile is destructible or not. Then I implemented a destruction listener, which is a Box2D contact listener, responsible of checking impulses generated from collisions between entities. When this impulse is big enough, both flags of the entities involved in a collision are checked and, if destructible, added to a list. This list is processed later when the actual destruction happens. It is important to note that should not be done in the previous step if you want to avoid errors while Box2D is still processing its update.
After tweaking the threshold for the impulse triggering destructions, last thing to do was to increase manually impulses when the collision involved a character punching. In technical words, when one of the entities is a character and the normal of the collision is close to (0, -1)
destruction is triggered for the other entity.
The result is really cool because not only do entities get destroyed after a collision with the main character, but they also get destroyed when heavy collisions happen between each other.
Push and pull
Dynamic entities have been added to the game, and they can be pushed and pulled around the scene. This meant that new states should be added to characters, push and pull indeed.
The push state is enabled when a character is trying to move and finds an obstacle either on its left or its right, quite easy to implement. Obstacles in the four directions (up, right, down, left) are collected after the physics system update by checking for physics body collisions and the normal of contact points. In addition, some bitflags are set which tell whether there are obstacles in any of the four directions.
The pull state is enabled when the player presses the x
button and there is a dynamic obstacle, which is checked by querying the type of the obstacle's physics body. When the conditions are met, a distance joint is created between the character and the dynamic entity to make sure that, when the character moves, the distance between the two bodies remains the same, effectively giving the ability to the character to carry around the entity. Of course, the joint is destroyed when exiting from the pull state.
Data submodule
A git submodule has been added to the repository under the data/
directory, pointing to Fahien/ncJump-data. There you could find the latest assets of the game: configurations, game maps, tile sets, and animations.
A better editor
Many improvements have been added to the in-game editor: a main menu bar at the top for choosing a placing mode between tile and entity, changing the opacity of certain elements of the scene according to the current mode for an immediate visual feedback; a config window to tweak all sort of values not directly related to the gameplay, like the size of the window, the scale of the scene or the UI, the offset of the camera to move around the scene without necessarily moving the character.
Factories
The time to add enemies to the game has finally come, and that translates to factories!
An enemy is just another kind of entity, which is the reason that pushed me towards implementing a class which sole responsibility is to create entities: the EntityFactory
. This factory contains a list of entity prototypes which can be selected from the editor and placed into the scene. Under the hood, the prototype is cloned, the clone is moved at mouse position, and it is added to the list of entities of the scene.
At this point I realized that every clone was duplicating textures, which is something that could and should be avoided, so I introduced another factory: the GraphicsFactory
. The responsibility of this class is to maintain a list of textures which can be referenced by multiple graphics components, as well as to create any kind of sprite which makes use of one of those textures.
Commands
Once I had enemies in the scene, I began wondering how to make them move. Reusing the state component of the main character sounded OK to me, but there was a problem. The movement logic within the state component was directly checking the gamepad and the keyboard, which meant that player's input would move both the main character and all the enemies. Decoupling to the rescue.
Input → MoveCommand → StateComponent
I removed the dependency to the input from the state component, and I introduced the concept of commands. For simplicity, I will just focus on the MoveCommand
. The state component now is just listening for move commands and, when they are issued, the component inspects the values in a move command and updates its state accordingly. Move commands, and possibly other kinds of commands, can be issued by any source: player input, in-game console, scripts, and so on.
Scripts
Other engines may use terms like behaviors, but I prefer to stick with the technical term: script. With this you can indeed model behaviors of entities, like a wandering behavior that I had in mind for my first class of enemies. Ideally it might also be written with a scripting language, like LUA which is supported by the nCine, but that is something I will maybe explore in the future.
I wrote down a couple of line for the wandering behavior which should just move the entity to the right or to the left until an obstacle is encountered, and then it should turn back and continue in a loop. The result is quite interesting, but I believe it can be further improved.
Dying state
Definitely one of the fundamental states. Any living entity should die at some point, and this state represents that period of time where an animation should play telling the player an enemy is defeated or that the game is over. The update method of this state is super easy and it looks like that.
void DyingState::update(Entity& entity)
{
if (entity.animation.has_finished()) {
entity.set_enabled(false);
// just reuse this entity for something else
}
}
Enabled flag
Of course this explains the Entity::set_enabled()
method just introduced in the previous section. By calling it, we can effectively disable an entity and all its components without destroying it, or freeing its memory, so that it could be recycled to be reused later when we, for example, need to spawn another entity.
Array of states
This was an important optimization. Before this, every state transitions triggered the destruction of the old state and creation of an instance of the new state, which is not really needed and does not scale nicely as a scene grows. Now, a state component keeps track of an array of all the possible state objects, together with a pointer telling us which one is the current state.
Initial position
Another requirement for the scene is to define where the player should start. This initial position is a pair of coordinates which tells us just this. When a scene starts, we put the player there.
Definitions and factories
I love this pattern and I think I will use from now on for all of my game projects. Basically, we have to deal with big and heavy objects which are not easily serializable. Think of wrappers around third party components like Box2D bodies or nCine sprites. I tried to find a way to serialize those objects and I believe I found a very nice solution which is anyway something widely used, so I possibly just reinvented the wheel here, but that is ok.
I do not serialize those objects. I do serialize their definitions. A definition is a collection of all the parameters needed to construct an object. Let us take a ncJump graphics component as an example.
// Very simplified
auto def = GraphicsDef(GraphicsType::TILE);
def.path = String("image.png");
def.layer = 2;
auto gfx = GraphicsComponent(def);
As you can see, the graphics component constructor takes a graphics definition as a parameter. A graphics definition, like all other definitions, is a Plain Old Data (POD) structure, which enables us to use nlohmann macros to quickly define all the functions needed to serialize it. Done. Awesome.
// That's it
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(GraphicsDef, type, path, layer);
CI and automatic deploy
The project builds automatically through GitHub actions, for Linux, Window, MacOS, Android, and believe it or not for the web. Indeed, click here and be prepared to say wow. All the credits for this go to the brilliant nCine author: @encelo!
All the updates can be found on the NcJump GitHub repository.