Back to Home

Avoid the Walls V2

View project on GitHub

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.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

// 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.

Makefile (root level)

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

Makefile (desktop)

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

loop_desktop.cpp

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.

Makefile (web)

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

loop_web.cpp

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().

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.

Next: Systems Previous: Myst - Thoughts on the Channelwood Age