Back to Home

Shipping Code and Callback Functions

I wrapped up the last article saying this one was going to be about callbacks.

I'll write about callbacks, mainly because if I say I'm going to do something I'd prefer to do it.

But this tutorial tangent-ing is a dangerous habit to get into. So let's talk base building for a sec.

Base Building

If you're into weight lifting check out Alexander Bromley's "Base Strength". It's good.

https://www.amazon.com/Base-Strength-Program-Design-Blueprint-ebook/dp/B08R5J58F8

I'm going to apply one of his concepts to programming. I read the book years ago and am discussing this from memory, but here's the gist of what I'm thinking.

Bromley talks about a pyramid whose height is limited by the width of it's base. Something along those lines. For a non-pyramid example consider neuromuscular efficiency and the cross-sectional area of a muscle - these are two different factors that affect absolute strength. They can both be improved via different styles of training.

Imagine the gym rat who puts all his time into hypertrophy and never lifts heavy - his neuromuscular efficiency will be the bottleneck of his absolute strength. His "base" is hypertrophy.

Now how does this apply to code? Well, imagine the game developer who puts all his time into discussing theory and never builds anything - the fact that he hasn't built anything will be what prevents him from shipping code. His "base" is theory.

AI: But you don't even know C++ in the first place - how can you say you have a theoretical "base"?

Well, compared to how many personal projects I've completed I'd say theory MUST be my strong point. That or procrastinating.

Anyways theory is the hypertrophy of coding. We can discuss game loops, callback functions, and paradigms as much as we want but if we don't BUILD we don't have anything to SHIP. And SHIPPING CODE is the ABSOLUTE STRENGTH of programming. Know what I'm saying?

AI: Yes! You are using a contrived metaphor to delay the next step - writing about callbacks - which is yet another exercise in Yak Shaving on the way to finishing your Dot Game. Correct?

Yeah that's about right. On to callbacks.

Callbacks

I said I'd start with Javascript - gimme a sec to go peruse the node.js docs, I honestly can't remember where this started for me...

Oh yeah, requests. Check this out, direct from the node.js website:

import http from 'node:http';

// Create a local server to receive data from
const server = http.createServer();

// Listen to the request event
server.on('request', (request, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    data: 'Hello World!',
  }));
});

server.listen(8000);

Jeez, now I'm reminded how weird arrow functions are to read when you aren't used to them. Hope this helps at all.

() => {} is an anonymous function—a function with no name. You define it in-place, usually when you don’t need to reuse it anywhere else.

In the server.on() function the second argument is expecting a callback function - in this case we respond to a request using this callback.

The callback function will take two arguments - a request and a response - and in this case it's going to respond to ANY request with the same response.

Why a callback?

Well, I assume it's because requests expect responses, and a callback function is a convenient way to structure the code.

Now you've got me pondering what kind of system I might design for a server in a vacuum.

My first thought is a simple request_id on each response - you'd listen for requests, receive them, then send a response to the same ip. But then you'd be dealing with handling unreceived responses, trying to send out responses in the right order - huh, a request/response pattern probably makes sense.

AI: Ryan Dahl will be pleased to hear that you approve. Are you sure you don't want to take a week to implement your own server? I bet you'd like that.

Yeah let's move on.

Callbacks in C++

Ok, we made it this far. Uncharted territory for me. I'm tempted to just sic the AI on it. Let's hold off a sec - lets think about what we know about C++.

One obvious thing is the language is incredibly picky about types. It really cares about the size of integers and what we put into arrays. I'm guessing there must be some fancy modern type for functions. Some nasty looking thing.

AI: std::function is probably what you're looking for. It's part of the <functional> header and allows you to store and pass around callable objects, including regular functions, lambdas, and even functors.

I'm going to go ahead and pretend like you didn't mention lambdas or functors. I legit have no idea what those are. Probably some lame made up things for nerds. I am opting right out of that.

AI: Okay.

Yeah. Okay. So I import the functional header, use std::function as the type on my PositionManager, and undoubtedly define the callback function inline with my function call in some not-at-all-intuitive-to-read syntax. Well, I mean you do it, but I tell you to. Right?

AI: Okay.

...

Well, here's what we got:

#include "raylib.h"
#include <cstdlib>
#include <ctime>
#include <functional>
#include <memory>
#include <vector>

#ifdef PLATFORM_WEB
#include <emscripten/emscripten.h>
#endif

// Global constants
const int screenWidth = 800;
const int screenHeight = 600;

// Base Dot class
class Dot {
protected:
  Vector2 position;
  float radius;
  Color color;

public:
  Dot(Vector2 startPos, float radius, Color color)
      : position(startPos), radius(radius), color(color) {}

  virtual void Control(float deltaTime,
                       class PositionManager &positionManager) {
    // Default behavior: no movement
  }

  virtual void HandleCollision(Dot *other) {
    // Default behavior: do nothing
  }

  void Draw() const { DrawCircleV(position, radius, color); }

  Vector2 GetPosition() const { return position; }

  float GetRadius() const { return radius; }

  void SetPosition(Vector2 newPos) { position = newPos; }
};

// PositionManager class
class PositionManager {
private:
  std::vector<Dot *> dots;
  std::function<void()> onScoreIncrement;
  Dot *player = nullptr;
  Dot *target = nullptr;

public:
  void AddDot(Dot *dot) { dots.push_back(dot); }

  void SetPlayerAndTarget(Dot *playerDot, Dot *targetDot) {
    player = playerDot;
    target = targetDot;
  }

  void SetScoreIncrementCallback(const std::function<void()> &callback) {
    onScoreIncrement = callback;
  }

  void Update(float deltaTime) {
    // Check for collisions between all dots
    for (size_t i = 0; i < dots.size(); ++i) {
      for (size_t j = i + 1; j < dots.size(); ++j) {
        Dot *dotA = dots[i];
        Dot *dotB = dots[j];

        if (CheckCollisionCircles(dotA->GetPosition(), dotA->GetRadius(),
                                  dotB->GetPosition(), dotB->GetRadius())) {
          // Notify both dots of the collision
          dotA->HandleCollision(dotB);
          dotB->HandleCollision(dotA);

          // Trigger the score increment callback only if the collision is
          // between the player and target
          if (onScoreIncrement && ((dotA == player && dotB == target) ||
                                   (dotA == target && dotB == player))) {
            onScoreIncrement();
          }
        }
      }
    }

    // Let each dot update its position
    for (Dot *dot : dots) {
      dot->Control(deltaTime, *this);
    }
  }

  bool IsPositionValid(Vector2 newPos, float radius) const {
    for (const Dot *dot : dots) {
      if (CheckCollisionCircles(newPos, radius, dot->GetPosition(),
                                dot->GetRadius())) {
        return false;
      }
    }
    return true;
  }

  Vector2 GetValidPosition(float radius) {
    Vector2 newPos;
    do {
      newPos = {static_cast<float>(rand() % screenWidth),
                static_cast<float>(rand() % screenHeight)};
    } while (!IsPositionValid(newPos, radius));
    return newPos;
  }

  Vector2 UpdatePosition(Dot *dot, Vector2 newPos, float radius) {
    // Enforce screen bounds
    if (newPos.x < radius)
      newPos.x = radius;
    if (newPos.x > screenWidth - radius)
      newPos.x = screenWidth - radius;
    if (newPos.y < radius)
      newPos.y = radius;
    if (newPos.y > screenHeight - radius)
      newPos.y = screenHeight - radius;

    return newPos;
  }
};

// Player class (inherits from Dot)
class Player : public Dot {
private:
  float speed;

public:
  Player(Vector2 startPos, float radius, Color color, float speed)
      : Dot(startPos, radius, color), speed(speed) {}

  virtual ~Player() {}

  void Control(float deltaTime, PositionManager &positionManager) override {
    Vector2 newPos = position;

    if (IsKeyDown(KEY_W))
      newPos.y -= speed * deltaTime;
    if (IsKeyDown(KEY_S))
      newPos.y += speed * deltaTime;
    if (IsKeyDown(KEY_A))
      newPos.x -= speed * deltaTime;
    if (IsKeyDown(KEY_D))
      newPos.x += speed * deltaTime;

    // Update position using the PositionManager
    position = positionManager.UpdatePosition(this, newPos, radius);
  }

  void HandleCollision(Dot *other) override {
    // Player-specific collision handling (e.g., no special behavior for now)
  }
};

// Target class (inherits from Dot)
class Target : public Dot {
public:
  Target(Vector2 startPos, float radius, Color color)
      : Dot(startPos, radius, color) {}

  virtual ~Target() {}

  void HandleCollision(Dot *other) override {
    // Respawn the target at a new valid position
    position = {static_cast<float>(rand() % screenWidth),
                static_cast<float>(rand() % screenHeight)};
  }
};

// Game class
class Game {
private:
  std::unique_ptr<Player> player;
  std::unique_ptr<Target> target;
  PositionManager positionManager;
  int score;

public:
  Game() : score(0) {
    // Initialize player and target
    player = std::make_unique<Player>(
        Vector2{screenWidth / 2.0f, screenHeight / 2.0f}, 15.0f, BLUE, 200.0f);
    target = std::make_unique<Target>(positionManager.GetValidPosition(10.0f),
                                      10.0f, RED);

    // Register dots with the position manager
    positionManager.AddDot(player.get());
    positionManager.AddDot(target.get());

    // Set player and target in the position manager
    positionManager.SetPlayerAndTarget(player.get(), target.get());

    // Set the score increment callback
    positionManager.SetScoreIncrementCallback([this]() { score++; });
  }

  void Update(float deltaTime) {
    // Delegate all updates to the PositionManager
    positionManager.Update(deltaTime);
  }

  void Render() const {
    // Draw game elements
    BeginDrawing();
    ClearBackground(RAYWHITE);

    DrawText("Catch the moving dot!", 10, 10, 20, DARKGRAY);
    DrawText(TextFormat("Score: %d", score), 10, 40, 20, DARKGRAY);

    player->Draw();
    target->Draw();

    EndDrawing();
  }

  bool IsRunning() const { return !WindowShouldClose(); }
};

Game *gameInstance = nullptr;

void MainLoop() {
  float deltaTime = GetFrameTime();
  gameInstance->Update(deltaTime);
  gameInstance->Render();
}

int main() {
  // Initialization
  InitWindow(screenWidth, screenHeight, "Dot Game - Catch the Dot");
  SetTargetFPS(60);

  // Seed random number generator
  std::srand(std::time(nullptr));

  // Create game object
  Game game;
  gameInstance = &game;

#ifdef PLATFORM_WEB
  emscripten_set_main_loop(MainLoop, 0, 1);
#else
  while (game.IsRunning()) {
    float deltaTime = GetFrameTime();
    game.Update(deltaTime);
    game.Render();
  }
#endif

  CloseWindow();

  return 0;
}
Next: Collect the Dots V1 Previous: Debugging, Part 3