You know, someday we'll look back on this code and cringe.
So hard.
But today? Today this code is beautiful. It is elegant. It isn't OVERENGINEERED - no way - it reads like poetry.
AI: Oh stop - you're making me blush.
No really. Poetry.
Except for incrementing the score. Not sure how that's going to fit.
So for now, let's review this.
We'll skip the game loop though.
AI has mentioned a few times that I'm on the cusp of some sort of "Entity" system. Something along the lines of just needing a GameObject
class for my Dot
class to inherit from - noted, but I don't think this project needs it.
I think Control()
just feels good. I like the name. I like that it defaults to a no-op. It feels like just the right amount of engineering for my little dots.
Same goes for HandleCollision()
- I suspect if I had enough Dot
types I might start wishing for a central way to handle collisions, but at this scale it feels right.
// 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; }
};
I had to grope around for Update()
- it didn't come to me immediately. The idea of a centralized hunk of logic whose only job is to track locations and notify objects that they bumped into something - I just like it. Kinda wonder how this would scale. How many thousands of Dot
s could this loop approach handle before a single refresh couldn't handle this. Maybe it's millions - I dunno.
// PositionManager class
class PositionManager {
private:
std::vector<Dot *> dots;
public:
void AddDot(Dot *dot) { dots.push_back(dot); }
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);
}
}
}
// 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;
}
};
The only thing I have to say about Player
is that I'm tempted to put score++ inside of the HandleCollision()
method.
// 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)
}
};
But then again maybe it should be left to the target?
// 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)};
}
};
But at the same time score
DOES live inside of Game
class, maybe the position manager should send a message back to Game
.
// 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());
}
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(); }
};
Let me regale you with a story from my youth.
Several months ago I was building a "honeypot" with node.js and Javascript, just a simple thing who's job was to log requests to a SQLite database - I had dreams of eventually identifying malicious scripts and feeding them back false positives.
My memory is kind of hazy now but I remember being confused by callback functions - I think perhaps some of node's stuff was expecting them and I just did not get it. Funny - now it seems simple. But at the time it was not.
So lets talk callbacks for a sec.
Watch me embarrass myself - from memory I will do my best to explain a callback.
A callback function is a function that you pass to another function. When the function that you passed to the other function is called it gets to modify whatever arguments you passed in to the function that you passed into the other function.
Jeez, no wonder I was confused. Did I get that right?
AI: Yes, your explanation of a callback function is mostly correct, but it could be simplified for clarity.
Not sure how that could be clarified. Have a go.
AI: A callback is a function you give to another function to call later. It’s like saying, “When you’re done, call this function to let me know.” The function receiving the callback doesn’t care what the callback does—it just calls it when needed.
Eh, that glosses over the real power - you get to "break" scope - you need to mention that if you wanna one up me...
AI: Alright, let’s one-up your explanation by emphasizing the "breaking scope" aspect of callbacks:
A callback is a function you pass to another function, allowing the receiving function to "call back" into the scope of the original function. This is powerful because it lets you inject custom behavior into generic code, effectively "breaking" the scope barrier. For example, a callback can access variables or state from its original context, even when executed in a completely different part of the program.
That's better.
I don't know if I can do it in C++, but I want to inject callback function from Game
into PositionManager
. That's how I want score to increment. It just seems right.
Does C++ work like that?
AI: Yes, C++ supports injecting a callback function into
PositionManager
to increment the score. You can achieve this by using astd::function
inPositionManager
to store the callback, which can then be invoked when a collision occurs.
Show me a basic example please.
#include <iostream>
void MyCallback() {
std::cout << "Callback executed!" << std::endl;
}
void TriggerEvent(void (*callback)()) {
if (callback) {
callback(); // Call the function passed as a callback
}
}
int main() {
TriggerEvent(MyCallback); // Pass the callback function
return 0;
}
Yuck. Is it just me or does C++ look kinda gross for some things.
You know, I really did struggle with callbacks - I think it deserves more than just a little back and forth at the end of a post.
Tomorrow we'll examine the Javascript callback - it's nicer - and then we'll learn how to do it in C++.