I made a game, and I want to talk about it for a bit. If you subscribed exclusively for malware and Windows API subject matter, I apologize in advance. But c’mon, let your interests vary a bit.
Ahem, so, yes, I made a game. It’s the classic chrome dinosaur offline game, designed with PyxelEdit, and written in C++ with the SDL2 graphics library, aptly called ‘Dino Saur’.
And here’s a frame from the gameplay:
And you can give it a try here.
I hope this post benefits you in some way. It’s primarily written as documentation of my design decisions and as a reference for further development of the game if I choose. It’s not a very structured post, and it doesn’t try to be. Think of it as a way for you (and I) to understand the beautiful, beautiful code I wrote to bring this game idea to life. It does not talk about the SDL2 graphics library features or C++, just purely algorithmic and design decisions.
Here’s the source code: https://github.com/wldfngrs/chrome-dinosaur-2d
Entity Component Systems (ECS)
The ‘Core‘ directory contains an ‘EntityComponentSystem.h’ header file that provides a class abstraction for components and entities within the game. Think of entities as characters or “objects“ within your game world, and components as “behaviour“ or “attributes“ that these entities have. A player (or obstacle) character looks a certain way? All that code is handled by the sprite component (SpriteComponent.h). Where is this player (or obstacle) character positioned, and how fast (or slow) is it moving? That is handled by the transform component (TransformComponent.h). These abstractions go on depending on your game complexity.
The Entity Component System manages in-game entities, generating them, keeping them active, making them inactive, etc., while the entities manage the components tied to them.
The ‘Core‘ directory contains all ‘Entity Component System’ code, including the EntityManager, Entity, and Component classes, as well as definitions for individual components, such as; TransformComponent, and SpriteComponent.
Finite State Machines
At some point, you’d need to control the behavior of game entities on a more granular level. Deciding some input or output being accepted or created between animation frames. For most games, Finite State Machines (FSMs) allow for that level of control. Animation frames can be grouped into “states”, with certain states being triggered to enter and exit depending on some particular input.
The state transitions, defined as finite cause the machine (Dino) can be in one of a finite number of states at a time:
Press the DOWN ARROW key to transition the Dino character to the ducking state from a running state.
Release the DOWN ARROW key while in the ducking state to transition to the running state.
Press the SPACE key in the running state to transition to the jumping state.
And that’s it, for the most part. Pay attention to the jump animation sequence. You’d notice that the Dino “charges” a jump, jumps, and then “crouches” on land.
To do that, I needed what I’d call “sub-states“. That is, while in the umbrella jumping state, the Dino player would change sub-states from a ‘charge’ to a ‘jump’ to finally a ‘crouch’ (RE: Dino state transition map). In this case however, timers, not key inputs, control the state transition.
Robert Nystrom writes very well on the concept of states as a game design paradigm. Fair warning, it’s quite heavy on concepts such as inheritance and compile time polymorphism. I think it’s a very neat design pattern though, and it’s one of the parts of my game system I am most proud of.
There’s an abstraction class called ‘DinoState‘ with member functions, enter(), update() and leave(). Game character states (in this codebase, the classes aptly named: ‘RunningState‘, ‘DuckingState‘ and ‘JumpingState‘) implement this class and it’s member functions, defining behavior when immediately transitioning to the state; enter(), when within the state; update() and when exiting the state; leave().
The DinoState class interface and its implementations can be found in the ‘Dino‘ directory.
Now, here’s a lot more original work;
Obstacle Factories
The ‘Obstacle‘ directory contains ObstacleManager-related code.
‘Dino Saur’ is a side-scrolling game, at least in principle. Obstacles infinitely move from right to left within the game window. An ‘obstacle factory/generator‘ ensures that there’s always at most two obstacle entities in the game window. This is optimal, particularly cause the game system only ever has to worry about updating at most two obstacle at any point in the game play.
To do that, loadObstacles() checks if an obstacle is “inactive“, meaning that it has gone past the left side of the game window. If it has, hotSwapObstacleSprite() “swaps“ the sprite texture on that obstacle entity (simply, changing the offsets into the initially loaded obstacles sprite sheet) and updates it’s transform component to the right side of the game window! Talk about consistent memory use. After this “swap“, the obstacle entity stays beyond the right side of the game window with no velocity, until the distance between it and the other obstacle entity already in the game window is at least x. If it is, it gains a negative velocity, and slides across the screen. If not, it stays, until it is.
Text Managers (Factories?)
The ‘Text‘ directory contains TextManager related code.
I really like the factory term, so of course I’d extend it a little more. Games need text. How else could we show the title screen, “game over” or player scores?
Now generating and rendering this text can get a little complicated in terms of memory use and preserving a smooth gameplay. For “static“ text like the title screen “Dino Saur“, typical rendering rules apply: generate a texture of the required string, draw it to the game window. However, when the text is “non-static“ like the running score of the player, in-game texture generation and rendering of these many possible number combinations could get computationally intensive.
To offset this cost, the text manager contains a key-value cache of all possibly expected text. The keys are the actual strings to be drawn, values are the generated textures of the strings.
addToTextCache_AllAtOnce() generates a texture for the particular string (in the specified font) and adds this texture to the texture cache.
addToTextCache_CharByChar() loops through every character in the string, generating a texture (in the specified font) for each character and adding it to the texture cache. This way, drawing non-static text is trivial. drawText_NonStatic() loops through the ‘text’ argument, and draws the textures mapped to the individual characters, offsetting each new character to be drawn by the ‘letterWidth’ argument plus one.
drawText_Static_NonStatic() draws a string that contains static and non-static parts. For instance, the high score counter contains a static “HIGH SCORE“ part and a non-static counter of the actual high score. The static (first) and non-static (second) texts are passed as individual parameters to the function.
The cool part is all of this texture generation, texts, Dino player or obstacle sprites, happen before the game even starts! As usual, having a definite number of objects (texts to be rendered, players, obstacles) allow for consistent memory use throughout the gameplay.
Multi-line text, centered text, typewriter effect text(?), multi-colored/multi-font text(?)
I had lots of ideas. A lot. And for the first time honestly, I did go through with implementing them. I mean, anything for a cool looking and memorable game, right?
(possibly) Multi-line, centered text
One of these ideas was a “game over” text. I wanted a “game over“ message to the player that was specific to the obstacle that ended the player’s run. So, for instance, if you crashed into a ‘Brute’, you’d get this game over message:
And if you collided with a ‘Bucket‘ instead:
These “game over“/collision tags were obstacle specific, tended to span multiple lines and even had a mix of colors. Clearly, the textures had to be cached differently.
The Dino player and obstacle entities had ‘colliders’. These defined the collidable rectangles of the entites, and collision happened if these ‘colliders‘ intersected with each other.
Obstacle entity ‘colliders‘ had collision tags to be displayed on collision. An ObstacleManager member variable, ‘justCollidedIndex‘ kept track of the index of the just collided obstacle, and the collision tag of the obstacle entity at that index is drawn to the screen.
Text segmentation is an effective way to handle these multi-featured collision tags, the segment delimiters being the obstacle name or new lines.
For instance, the collision tag; “Sh*t! caught in the Dying Trees!\nWriggle out for your next run?“, is cached like so:
Each line (and obstacle name, in this case ‘Dying Trees‘) getting their textures generated and cached separately as individual text segments.
Drawing these multi-featured texts is where things get a little messy however. The drawText_Static() function is called as usual, but in the ‘text‘ parameter, the newline character(‘\n‘) is used where a newline is expected in the drawn text.
drawText_Static() calls extractTextSegements() to divide the ‘text‘ parameter into segments using the obstacle name and new lines as—if you recall—delimiters. The piece of text that comes before the obstacle name gets pushed into the ‘textSegments‘ vector. Next, the delimiting text itself (obstacle name) is pushed into the ‘textSegments‘ vector.
A loop through the rest of the text continues the function. If a newline character is found, the text segment that comes between the most recent newline character/obstacle name and the current newline character is pushed into ‘textSegments‘. This continues until there’s no more newlines. At this point, the final segment of text between the most recent newline character and the end of the string is pushed into ‘textSegments‘. This vector of the text segments is returned to the drawText_Static() local variable of the same name.
calculateXPosition(), depending on whether or not an absolute position on the x-axis is provided, calculates x aligned to the LEFT, RIGHT or CENTER. Importantly, if the ‘xpos’ is CENTERED (as most text in this game is), this function calculates the length of consecutive text segments before a newline, multiplied by the ‘letterWidth‘ parameter. That is halved and subtracted from half the width of the game window. That’s the “centered“ x position.
At each new line, another x position aligned to the ‘xpos’ parameter is calculated.
With that, we have centered, multi-line and multi-colored texts.
Text effects: Typewriters (??)
Rendering text in Dino Saur comes in two forms; INSTANT and TYPEWRITER. INSTANT, is all of the text seen so far. They get rendered together, at once. Or at least you see them all at once. TYPEWRITER, conversely, offers a staggered character rendering effect that you most likely are familiar with. You’d have to beat the game (hit a 100,000 score) to see the congratulatory message that throws on this TYPEWRITER effect though. Or modify your build of the game to beat the game sooner.
Ahem. Moving on.
Drawing staggered textures requires drawing to the buffer and rendering to the game window separately from the main game loop. That is, the main game loop is “on hold“ until the typewriter animation effect is completed. Even more importantly, SDL_RenderClear() gets called just once; before the start of the animation. This is to preserve the SDL’s drawn-to-screen buffer, so each time SDL_RenderPresent() is called, the previously drawn characters stay displayed as yet another character gets displayed, until the end of the string.
The main thread sleeps for 100 milliseconds after “presenting“ every character and 500 milliseconds after every newline. For a smoother feel.
Note how text intended to be used with the TYPEWRITER effect are added to the texture cache; using addToTextCache_CharByChar()…
even though they get drawn eventually as static text!
Since the TYPEWRITER text effect displays characters one by one, it is obvious to cache these characters individually, since they are displayed that way; “individually“. As expected, this caching is handled within the init() function, before gameplay. The reason is the same: ensuring reasonable and consistent memory use throughout the run of the game.
That’s it. If you’ve read this far, I appreciate you. If you gave the source code a look and tried the game out, I appreciate you even more. If you haven’t, here’s Dino Saur for web and the desktop build.
I’m wldfngrs, thanks for reading Crux of The Matter.
APPENDIX:
Robert Nystrom:
Vittorio Romeo: