64-Bit Mazers
Since Maybe Either Option, and actually a lot before then, I've been working on writing a simple 2D game called Mazer 2D. The aim of this project is not to create the next AAA game or anything like that but instead, to learn, understand and appreciate the mechanics required for creating a game from scratch in C++. And as it happens, writing a game from first principles is pretty challenging (and fun!).
It has been a labour of love. I've scratched my head and banged my head but in the process, I've learnt stuff, and that's the point. It doesn't look like much but the mechanics and the workings are well-defined, and in my opinion, well designed which gives me my quiet satisfaction.
One aspect that I've found interesting is how challenging it is to keep the architecture of the game coherent and organised. Doing so is important, not only so that it is not fragile when changes need to occur but is flexible enough when they do but also that it is not too cognitively complex or confusing to read. I've found myself constantly changing how the game code is structured and making improvements and I think I've found an architecture that meets these requirements.
What I've got so far is a level system that is comprised of drawing and strategically removing walls that make up a maze that the players and NPCs inhabit. Think Pac-man. I can also dynamically generate new levels on the fly too.
I've Implemented animated NPCs that move around and chase you down if they spot you. This is pretty cool and makes my game have some semblance of a retro tile sheet-based platformer - think cartoon animation with keyframes. My player character moves in a similar way to the NPCs and so it animates itself depending on which direction it is moving in (there are different keyframes for different facing directions). It also obviously responds to keyboard input and moves around based on what keys are pressed:
void AnimatedSprite::Update(const unsigned long deltaMs)
{
const unsigned long durationSinceLastFrameMs = timeGetTime() - deltaTime;
// Switch to the next frame if we've been on the current frame too long
if (static_cast<float>(durationSinceLastFrameMs) >= frameDurationMs)
{
AdvanceCurrentFrameNumber();
SkipUnsupportedAnimationGroupFrames(); // Only cycle through supported animation groups
deltaTime = timeGetTime();
}
}
I've got a system of pickups which also animate (so they look interesting) and give you (the player) points when you collide with them. This obviously depends on a collision detection system which ensures that the player and NPCs cannot move through walls but can pick up items. I've got an event-based messaging system that sends events throughout the game to game objects that 'subscribe' to them. This is pretty useful and I got the idea after reading Game Complete.
Moving along, I've also got a simple level editor that allows me to create levels visually, and then export the level files which can then be loaded as levels in the game. This is pretty cool because I can design the level, put enemies in various places on the level and change how much damage each gives. This is all written into an XML-based level file. Speaking of XML, I've also got a settings system so that components can read their settings without having to recompile the game to use different settings/behaviours. The NPCs use finite state machines to dictate how they behave at any one moment in time. This technique I first learnt at University and I love it because its so simple, yet so effective:
void Enemy::ConfigureEnemyBehavior()
{
upState = gamelib::FSMState("Up", DoLookForPlayer());
downState = gamelib::FSMState("Down", DoLookForPlayer());
leftState = gamelib::FSMState("Left", DoLookForPlayer());
rightState = gamelib::FSMState("Right", DoLookForPlayer());
invalidMoveTransition = gamelib::FSMTransition([&]()-> bool { return !isValidMove; },
[&]()-> gamelib::FSMState* { return &hitWallState; });
hitWallState = gamelib::FSMState("Invalid",
gamelib::FSMState::NoUpdate, [&] { SwapCurrentDirection(); });
// Set the state depending on which direction the enemy is facing
onUpDirection = gamelib::FSMTransition(IfMovedIn(gamelib::Direction::Up),
[&]()-> gamelib::FSMState* { return &upState; });
onDownDirection = gamelib::FSMTransition(IfMovedIn(gamelib::Direction::Down),
[&]()-> gamelib::FSMState* { return &downState; });
onLeftDirection = gamelib::FSMTransition(IfMovedIn(gamelib::Direction::Left),
[&]()-> gamelib::FSMState* { return &leftState; });
onRightDirection = gamelib::FSMTransition(IfMovedIn(gamelib::Direction::Right),
[&]()-> gamelib::FSMState* { return &rightState; });
// Configure valid transitions
upState.Transitions = {onDownDirection, onLeftDirection, onRightDirection, invalidMoveTransition};
downState.Transitions = {onUpDirection, onLeftDirection, onRightDirection, invalidMoveTransition};
leftState.Transitions = {onUpDirection, onDownDirection, onRightDirection, invalidMoveTransition};
rightState.Transitions = {onUpDirection, onDownDirection, onLeftDirection, invalidMoveTransition};
hitWallState.Transitions = {onUpDirection, onDownDirection, onLeftDirection, onRightDirection};
// Set state machine to states it can be in
stateMachine.States = {upState, downState, leftState, rightState, hitWallState};
// Set the initial state to down
stateMachine.InitialState = &downState;
}
I've put together a very simplistic HUD that shows the player score, and health as well as a framerate. This is custom drawing done using fonts and drawing from SDL. I intend to add some more 'widgets' in the future but for now, it's just these basic elements. The game also has music/sound capability and sound plays in the background and changes when you finish the level (to do that you need to collect all pickups). When the player tries to smash down a wall, it makes a FX sound. Audio is pretty awesome part of any game and it makes me happier listening to the upbeat music while I try to escape from the baddies.
This is what it looks like so far, It is not much in terms of aesthetics but I think that the assets and the visuals aren't important at this stage and it's far more important to have a solid foundation of technology that can easily support loading new visuals moving forward which I think this code currently has - it's not pretty but considering where I started from, it has improved a lot.
Some other important but less visually evident aspects are Timers and workflows that can help produce situations such as playing level-ending music, pausing and then moving to the next level. Loading resources and resource and asset Management capability is really important too and helps organise memory usage. For example, loading only assets that need to be used for the level that is in play. It also makes finding assets easy in the code, you just ask for the asset to be loaded and it's done:
void LevelManager::OnGameWon()
{
const auto a = std::static_pointer_cast<Process>(
std::make_shared<Action>([&]() { gameCommands->ToggleMusic(verbose); }));
const auto b = std::static_pointer_cast<Process>(std::make_shared<Action>([&]()
{
AudioManager::Get()->Play(ResourceManager::Get()->GetAssetInfo(
SettingsManager::String("audio", "win_music")));
}));
const auto c = std::static_pointer_cast<Process>(std::make_shared<DelayProcess>(5000));
const auto d = std::static_pointer_cast<Process>(std::make_shared<Action>([&]()
{
gameCommands->LoadNewLevel(static_cast<int>(++currentLevel));
}));
// Chain a set of subsequent processes
a->AttachChild(b);
b->AttachChild(c);
c->AttachChild(d);
processManager.AttachProcess(a);
const auto msg = "All Pickups Collected Well Done!";
Logger::Get()->LogThis(msg);
}
As my game is 2D and relies on geometry I've had to incorporate models for supporting rectangles and manipulating them eg. removing their sides. I've also got logging and exception handling to help diagnose when things go wrong but so far I've not really used them much because I'm mainly in the debugger and I catch all the exceptions but I know that having logs is always a good thing.
I've also got a system for representing game objects in the game and common code for working with them eg. drawing, updating and moving them around. This is really useful because pretty much everything in the game derives from these objects. I have the ability to control the game loop and to load different game loop strategies such as FixedTimeStep or VariableGameLoop for example - this was mostly out of pure frustration while trying to find out why I had a periodic jitter in my framerate. This is what the FixedTimeStep loop looks like, which I use to run my game at 16ms per frame or ~60 Fps:
void gamelib::FixedStepGameLoop::Loop(const GameWorldData* gameWorldData)
{
// fixed loop strategy: https://gameprogrammingpatterns.com/game-loop.html
double previous = GetTimeNowMs();
double lag = 0.0;
while (!gameWorldData->IsGameDone)
{
const double current = GetTimeNowMs();
const double elapsed = current - previous;
previous = current;
lag += elapsed;
InputFunc(TimeStepMs);
while (lag >= TimeStepMs)
{
Update(TimeStepMs);
lag -= TimeStepMs;
}
Draw();
}
}
Even though it takes a long time to do all this stuff, it's pretty interesting and frustrating, but frustration normally turns into joy and excitement if you stick with it long enough. For example, until very recently I've been for months unable to track down a 'jitter' or periodic pause in my framerate. I've been ignoring it for so long now and reminding myself that other things are still worthy of my time even though I can't find the cause of this bug (and it irritates me). so I've just had it nagging at me every time I run the game.
Recently, for no real good reason other than it was also bothering me, I decided to move from x86 to x64 and would you believe it, the periodic framerate jitter just disappeared! Sure enough, if I ran in x86 mode, it came back. The cause of my jitter was a bug in the x86 code of the SDL library that I use.
Needless to say, I'm over the moon that it's resolved. I run in x64-bit mode now and I'm ever so pleased that every time I play the game, I no longer witness that jitter, which for such a long time has been a constant attribute of the game. Developing the game in 64-bit, like focusing on the aesthetics of the game was just not necessary in my opinion to achieve the project's objectives. Lucky then I guess I scratched that itch!
There is more to come, for example, I'm looking to add networking and multiplayer support. I'm also interested in adding a new Enemy class that is based on Behavior Trees (BTs) as opposed to Finite State Machines (FSMs). Another thing I'd like to add is scripting and I think this will work really well with the BTs, especially being able to load behaviour from a file like loading levels. I'm still not that interested in actually creating a fully-fledged game yet, like pretty graphics and polished scenes but that will come...