So the first version of Dot Game was 92 lines of procedural simplicity. The fourth and final version was 257 lines of object oriented complexity.
The path from 92 to 257 was fraught with bugs and added zero features to the game. Was it worth it?
AI: I can't help you with that.
Right. We'll see. I guess now its time to...
That's right - time for a new game.
We are on to the second game of our series - the first was a clone of "Dot Chase", which I'm pretty sure wasn't a real game. Now we're going to clone "Dot Eater", which also sounds like an AI hallucination.
AI: Neither "Dot Chase" nor "Dot Eater" are widely recognized as classic, real-world video games—at least not under those names. They sound like plausible names for simple arcade or educational games, but there’s no famous historical record of them as established titles.
You say that like you weren't the one who came up with the list.
Anyways, time for something completely different. We got plans for this puppy.
AI: You made the Dot move.
Yeah, SO FAR I've made the Dot move. And let's talk about that.
Remember that virtual Control()
method on Dot
? Well, I overrode it on the Target
class. To do this I needed a few things:
GetPlayerPosition()
method on the PositionManager
class. Vector2 GetPlayerPosition() const {
return player ? player->GetPosition() : Vector2{0, 0};
}
Vector2WeightedAttraction()
method on the Dot
class. static Vector2 Vector2WeightedAttraction(Vector2 from, Vector2 to,
float threshold, float weight) {
Vector2 dir = Vector2Subtract(to, from);
float dist = Vector2Length(dir);
if (dist < 0.01f || dist > threshold)
return {0, 0};
dir = Vector2Scale(dir, 1.0f / dist); // normalize
float strength = weight * (threshold - dist) / threshold;
return Vector2Scale(dir, strength);
}
Dot
to move around. I got some stuff to say about that.The obvious thing was that the Target
must avoid the Player
. This was straightforward.
void Control(float deltaTime, PositionManager &positionManager) override {
// Strong repulsion from player only
Vector2 playerPos = positionManager.GetPlayerPosition();
Vector2 repulsion =
Vector2WeightedAttraction(position, playerPos, 200.0f, -400.0f);
...
I tried several things from here. First I had the Target
avoid the walls. Then I tried the corners. Then I tried attracting it to the center. Then the mid point of the furthest point between the Player
and the Player
itself.
Jeez it was not nice.
void Control(float deltaTime, PositionManager &positionManager) override {
// Strong repulsion from player only
Vector2 playerPos = positionManager.GetPlayerPosition();
Vector2 repulsion =
Vector2WeightedAttraction(position, playerPos, 200.0f, -400.0f);
// Use repulsion as velocity
Vector2 velocity = repulsion;
// Limit speed
float maxSpeed = 160.0f;
if (Vector2Length(velocity) > maxSpeed) {
velocity = Vector2Scale(Vector2Normalize(velocity), maxSpeed);
}
// Move target
Vector2 newPos = Vector2Add(position, Vector2Scale(velocity, deltaTime));
position = positionManager.UpdatePosition(this, newPos, radius);
}
}
So this is what I came up with. Kinda feels like air hockey if the puck was magnetically opposed to the paddle.
#include "raylib.h"
#include "raymath.h" // Add this for vector math
#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; }
protected:
static Vector2 Vector2WeightedAttraction(Vector2 from, Vector2 to,
float threshold, float weight) {
Vector2 dir = Vector2Subtract(to, from);
float dist = Vector2Length(dir);
if (dist < 0.01f || dist > threshold)
return {0, 0};
dir = Vector2Scale(dir, 1.0f / dist); // normalize
float strength = weight * (threshold - dist) / threshold;
return Vector2Scale(dir, strength);
}
};
// 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;
}
Vector2 GetPlayerPosition() const {
return player ? player->GetPosition() : Vector2{0, 0};
}
};
// 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 Control(float deltaTime, PositionManager &positionManager) override {
// Strong repulsion from player only
Vector2 playerPos = positionManager.GetPlayerPosition();
Vector2 repulsion =
Vector2WeightedAttraction(position, playerPos, 200.0f, -400.0f);
// Use repulsion as velocity
Vector2 velocity = repulsion;
// Limit speed
float maxSpeed = 160.0f;
if (Vector2Length(velocity) > maxSpeed) {
velocity = Vector2Scale(Vector2Normalize(velocity), maxSpeed);
}
// Move target
Vector2 newPos = Vector2Add(position, Vector2Scale(velocity, deltaTime));
position = positionManager.UpdatePosition(this, newPos, radius);
}
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;
}