This is starting to sprawl.
user@k:~/Desktop/gamedev_progression/0009-avoid_the_walls_v2$ tree -a
.
โโโ avoid_the_walls
โโโ avoid_the_walls.html
โโโ avoid_the_walls.js
โโโ avoid_the_walls.wasm
โโโ constants.h
โโโ desktop
โ โโโ .clangd
โ โโโ loop_desktop.cpp
โ โโโ Makefile
โโโ entity.cpp
โโโ entity.h
โโโ game.cpp
โโโ game.h
โโโ loop.h
โโโ main.cpp
โโโ Makefile
โโโ web
โโโ .clangd
โโโ loop_web.cpp
โโโ Makefile
3 directories, 18 files
I suspect that nobody on Earth will ever read what I'm about to write.
Perhaps they shouldn't.
But even so, for my own benefit, I will address *every file and explain my thoughts thus far.
*almost
Here goes, I'll start with main.cpp
.
main()
is now platform agnostic.
No more #ifdef
here, no sir, we just seed our random number generator, whip ourselves up a Game
instance, and pass that off to RunPlatformLoop()
, which takes our MainLoop()
function and our Game
instance as arguments.
// main.cpp
#include "game.h"
#include "loop.h"
#include <cassert>
#include <cstdlib>
#include <ctime>
void MainLoop(void *gamePtr) {
Game *game = reinterpret_cast<Game *>(gamePtr);
assert(game && "gamePtr is null in MainLoop");
game->Run();
}
int main() {
// Seed random number generator
std::srand(std::time(nullptr));
// Create game instance
Game game;
// Run the main loop (platform handles window, etc)
RunPlatformLoop(MainLoop, &game);
return 0;
}
What is RunPlatformLoop()
you may ask? Well, let's go look at loop.h
.
// loop.h
#pragma once
void RunPlatformLoop(void (*MainLoop)(void *GameState), void *GameState);
Huh - just a function that takes a function (that takes an argument) as an argument as well as the argument that the function that we just passed expects as an argument. That's all.
AI: You make it sound stupid when you put it that way.
Yeah well I felt stupid when I first saw it. Let me say this to the reader: once it clicks it is stupid - stupid simple!
AI: It's a bit like handing someone a gun that takes a specific kind of bulletโand you also hand them the bullets to go with it. The function is the gun, the argument is the bullet. It might seem roundabout, but it lets whoever's on the other end decide exactly how to use them together!
Right, so we got ourselves our .h file, where is the function defined? Well, that depends.
On to our Makefiles.
I don't know how it does it but somehow this thing is telling make
to look into ./desktop/
and ./web/
for Makefiles.
# Root Makefile to build both desktop and web targets
.PHONY: all desktop web clean clean-desktop clean-web
SRCS = main.cpp game.cpp
all: desktop web
# Build desktop target
desktop:
$(MAKE) -C desktop
# Build web target
web:
$(MAKE) -C web
# Clean all
clean: clean-desktop clean-web
clean-desktop:
$(MAKE) -C desktop clean
clean-web:
$(MAKE) -C web clean
And so a hint to the answer of our earlier question (where is RunPlatformLoop()
defined): it's going to be one of the files in SRCS
.
CXX = g++
CXXFLAGS = -Wall -std=c++17
LDFLAGS = -lraylib -lm -ldl -lpthread -lGL -lrt -lX11
SRCS = ../main.cpp ../game.cpp loop_desktop.cpp ../entity.cpp
TARGET = ../avoid_the_walls
all: $(TARGET)
$(TARGET): $(SRCS)
$(CXX) $(CXXFLAGS) $(SRCS) -o $(TARGET) $(LDFLAGS)
clean:
rm -f ../avoid_the_walls
And so, the first half of the answer.
Notice the desktop loop sets up a window, monitors gameState.shutdownRequested
so it can handle closing the window for desktop, and then calls the MainLoop()
function (along with the Game
instance) that we passed from main.cpp
.
// loop_desktop.cpp
#include "../constants.h"
#include "../game.h"
#include "../loop.h"
#include "raylib.h"
void RunPlatformLoop(void (*MainLoop)(void *gamePtr), void *gamePtr) {
InitWindow(screenWidth, screenHeight, gameTitle);
SetExitKey(0); // Disable default ESC behavior
SetTargetFPS(60);
while (!WindowShouldClose()) {
Game *game = reinterpret_cast<Game *>(gamePtr);
if (game->gameState.shutdownRequested) {
break;
}
MainLoop(game);
}
CloseWindow();
}
Great, now let's go look at the web side of things.
Not much has changed. Notice loop_web.cpp
is in SRCS
instead of loop_desktop.cpp
. Also everything is set up for web (not that I have a clue how to do that, AI magic you know). But it's still a Makefile.
EMCC = emcc
EMCCFLAGS = -Wall -std=c++17 -Os -DPLATFORM_WEB
EMCC_LDFLAGS = ~/Desktop/raylib/build_html5/raylib/libraylib.a \
-I/home/user/Desktop/raylib/build_html5/raylib/include \
-s USE_GLFW=3 -s ASYNCIFY -s TOTAL_MEMORY=67108864 \
--shell-file ~/Desktop/raylib/src/minshell.html
SRCS = ../main.cpp ../game.cpp loop_web.cpp ../entity.cpp
TARGET = ../avoid_the_walls.html
all: $(TARGET)
$(TARGET): $(SRCS)
$(EMCC) -o $(TARGET) $(SRCS) $(EMCCFLAGS) $(EMCC_LDFLAGS)
clean:
rm -f ../avoid_the_walls.html
So notice that we're basically doing the same thing as the desktop, though this time we're handling the web requirements - mainly not calling WindowShouldClose()
and instead passing our loop to emscripten_set_main_loop_arg()
.
// loop_web.cpp
#include "../constants.h"
#include "../game.h"
#include "../loop.h"
#include "raylib.h"
#include <emscripten/emscripten.h>
static void (*RealMainLoop)(void *gamePtr) = nullptr;
static void *RealGamePtr = nullptr;
static void WrappedMainLoop(void *gamePtr) {
Game *game = reinterpret_cast<Game *>(gamePtr);
if (game->gameState.shutdownRequested) {
emscripten_cancel_main_loop();
}
RealMainLoop(gamePtr);
}
void RunPlatformLoop(void (*MainLoop)(void *gamePtr), void *gamePtr) {
InitWindow(screenWidth, screenHeight, "Avoid the Walls V2");
SetExitKey(0);
SetTargetFPS(60);
RealMainLoop = MainLoop;
RealGamePtr = gamePtr;
emscripten_set_main_loop_arg(WrappedMainLoop, RealGamePtr, 0, 1);
}
Something interesting to point out here is that our WrappedMainLoop()
function (as well as our RunPlatformLoop()
function) must be the same pattern that emscripten_set_main_loop_arg()
is following - taking the function and its argument so a bit of tweaking and fiddling can be done before actually making the call.
In our case we're doing this to handle shutdown requests. In Emscripten's case I'm sure it's to pass control back to the browser every frame so it can do its browser stuff (and not freeze).
I'm sure because I learned it the hard way when implementing this. But you don't have to take my word for it. Try it yourself, or perhaps the AI can chime in here.
AI: You are correct, though minor precision: Emscripten's set_main_loop does more than just avoid freezing โ it ensures that frames are synchronized with the browser's refresh rate (VSync). So, it's also about optimizing your render cycle to match requestAnimationFrame under the hood.
Great! So we're finally calling MainLoop()
. Let's share it again to refresh.
// ... in main.cpp ...
void MainLoop(void *gamePtr) {
Game *game = reinterpret_cast<Game *>(gamePtr);
assert(game && "gamePtr is null in MainLoop");
game->Run();
}
Let's go look at Game::Run()
.
Full disclosure: a recent version of this had a while loop in it. That caused my browser to freeze.
Now we got ourselves a one-shot Run()
function, it just executes a single update of the necessary systems for this little engine.
// ... in game.cpp ...
void Game::Run() {
// Ensure player is centered after window is created (only on first frame)
static bool initialized = false;
if (!initialized) {
entityManager.ResetPlayer();
initialized = true;
}
HandleInput();
Update(GetFrameTime());
Render();
}
Now this is running a little long - the engine has a handful of systems that I could/should go over, and I may do that, but not tonight.
Let me leave you with another link to the sauce.
That will be all for now.