I realized that a day may come where someone may join us who is more interested in playing the games than reading the posts and the diffs.
To this individual I'd like to extend greetings and point out that it is for YOUR benefit I've decided to start embedding the games on top. AI, greet our new friend.
AI: Welcome, non-technical guest! Your interest in immediate gratification has been anticipated. The interactive component has been placed conveniently at the top of the page to reduce friction between desire and stimulus. No technical knowledge is required — though you may find that curiosity is the first step toward comprehension. Proceed.
Excellent. Now lets talk code.
AI: You began with a principled architecture. Collision logic was encapsulated. Responsibilities were discrete. Each object governed itself.
Well, we can't really say I began with a principled architecture. I began with AI generated procedural code and haphazardly wrangled it into something resembling OOP structure.
AI: True, however, in Version 3 these ideals have eroded. PositionManager now adjudicates contact events with centralized branching logic. This introduces procedural density, tight coupling, and an implicit shift toward a god-object pattern.
Yeah I don't know what those things mean, but I do know that having two methods of handling collisions is confusing things.
On one hand I had my Dot
objects, each one handling their own collision logic with HandleCollision()
- THEN I had my PositionManager
, scanning for collisions. Except now PositionManager
is handling respawns and preventing Dots from sharing the same space. It's getting nasty.
AI: This is a common phase. Most systems decay before they scale. The term is “structural entropy.” You are now experiencing it.
Inside me, there is two wolves.
One wants to refactor now.
One wants to see how far we can go without refactoring.
@@ -86,6 +86,8 @@
|
|
86
86
|
}
|
87
87
|
|
88
88
|
void Update(float deltaTime) {
|
89
|
+
// Track which targets need to be respawned this frame
|
90
|
+
std::vector<Dot *> targetsToRespawn;
|
89
91
|
// Check for collisions between all dots
|
90
92
|
for (size_t i = 0; i < dots.size(); ++i) {
|
91
93
|
for (size_t j = i + 1; j < dots.size(); ++j) {
|
@@ -95,24 +97,45 @@
|
|
95
97
|
DotType typeB = dots[j].type;
|
96
98
|
if (CheckCollisionCircles(dotA->GetPosition(), dotA->GetRadius(),
|
97
99
|
dotB->GetPosition(), dotB->GetRadius())) {
|
98
|
-
dotA->HandleCollision(dotB);
|
99
|
-
dotB->HandleCollision(dotA);
|
100
|
-
|
101
|
-
//
|
100
|
+
// Only handle respawn/game over for player-target and player-enemy
|
102
101
|
if (onScoreIncrement &&
|
103
102
|
((typeA == DotType::Player && typeB == DotType::Target) ||
|
104
103
|
(typeA == DotType::Target && typeB == DotType::Player))) {
|
105
104
|
onScoreIncrement();
|
106
|
-
}
|
107
|
-
|
105
|
+
// Mark the target for respawn
|
106
|
+
if (typeA == DotType::Target)
|
107
|
+
targetsToRespawn.push_back(dotA);
|
108
|
+
if (typeB == DotType::Target)
|
109
|
+
targetsToRespawn.push_back(dotB);
|
108
|
-
if (onGameOver &&
|
110
|
+
} else if (onGameOver &&
|
109
|
-
|
111
|
+
((typeA == DotType::Player && typeB == DotType::Enemy) ||
|
110
|
-
|
112
|
+
(typeA == DotType::Enemy && typeB == DotType::Player))) {
|
111
113
|
onGameOver();
|
114
|
+
} else {
|
115
|
+
// For all other collisions, resolve overlap (simple elastic push)
|
116
|
+
Vector2 posA = dotA->GetPosition();
|
117
|
+
Vector2 posB = dotB->GetPosition();
|
118
|
+
float rA = dotA->GetRadius();
|
119
|
+
float rB = dotB->GetRadius();
|
120
|
+
Vector2 delta = Vector2Subtract(posB, posA);
|
121
|
+
float dist = Vector2Length(delta);
|
122
|
+
if (dist == 0)
|
123
|
+
dist = 0.01f; // Prevent div by zero
|
124
|
+
float overlap = (rA + rB) - dist;
|
125
|
+
if (overlap > 0) {
|
126
|
+
Vector2 push =
|
127
|
+
Vector2Scale(Vector2Normalize(delta), overlap / 2.0f);
|
128
|
+
dotA->SetPosition(Vector2Subtract(posA, push));
|
129
|
+
dotB->SetPosition(Vector2Add(posB, push));
|
130
|
+
}
|
112
131
|
}
|
113
132
|
}
|
114
133
|
}
|
115
134
|
}
|
135
|
+
// Respawn all targets that were collected this frame
|
136
|
+
for (Dot *t : targetsToRespawn) {
|
137
|
+
t->SetPosition(this->GetValidPosition(t->GetRadius()));
|
138
|
+
}
|
116
139
|
for (auto &entry : dots) {
|
117
140
|
entry.dot->Control(deltaTime, *this);
|
118
141
|
}
|
@@ -221,9 +244,13 @@
|
|
221
244
|
}
|
222
245
|
|
223
246
|
void HandleCollision(Dot *other) override {
|
224
|
-
//
|
247
|
+
// Only respawn if collided with player
|
248
|
+
if (other->GetType() == DotType::Player) {
|
225
|
-
|
249
|
+
position = {static_cast<float>(rand() % screenWidth),
|
226
|
-
|
250
|
+
static_cast<float>(rand() % screenHeight)};
|
251
|
+
}
|
252
|
+
// Otherwise, do nothing (collision resolution is handled in
|
253
|
+
// PositionManager)
|
227
254
|
}
|
228
255
|
};
|
229
256
|
|
@@ -252,8 +279,9 @@
|
|
252
279
|
}
|
253
280
|
|
254
281
|
void HandleCollision(Dot *other) override {
|
255
|
-
//
|
282
|
+
// Only end game if collided with player (handled in PositionManager)
|
283
|
+
// Otherwise, do nothing (collision resolution is handled in
|
256
|
-
//
|
284
|
+
// PositionManager)
|
257
285
|
}
|
258
286
|
};
|
259
287
|
|
@@ -261,61 +289,94 @@
|
|
261
289
|
class Game {
|
262
290
|
private:
|
263
291
|
std::unique_ptr<Player> player;
|
264
|
-
std::unique_ptr<Target>
|
292
|
+
std::vector<std::unique_ptr<Target>> targets;
|
265
|
-
std::unique_ptr<Enemy>
|
293
|
+
std::vector<std::unique_ptr<Enemy>> enemies;
|
266
294
|
PositionManager positionManager;
|
267
295
|
int score;
|
268
296
|
bool gameOver;
|
269
297
|
|
298
|
+
void InitGameObjects();
|
299
|
+
void AddTarget();
|
300
|
+
void AddEnemy();
|
301
|
+
|
270
302
|
public:
|
271
|
-
Game()
|
303
|
+
Game();
|
272
|
-
// Initialize player and target
|
273
|
-
player = std::make_unique<Player>(
|
274
|
-
Vector2{screenWidth / 2.0f, screenHeight / 2.0f}, 15.0f, BLUE, 200.0f);
|
275
|
-
target = std::make_unique<Target>(positionManager.GetValidPosition(10.0f),
|
276
|
-
|
304
|
+
void Reset();
|
277
|
-
enemy = std::make_unique<Enemy>(positionManager.GetValidPosition(12.0f),
|
278
|
-
|
305
|
+
void Update(float deltaTime);
|
306
|
+
void Render();
|
307
|
+
};
|
279
308
|
|
280
|
-
|
309
|
+
// Definitions for Game methods
|
281
|
-
|
310
|
+
Game::Game() : score(0), gameOver(false) { InitGameObjects(); }
|
282
|
-
positionManager.AddDot(target.get());
|
283
|
-
positionManager.AddDot(enemy.get());
|
284
311
|
|
312
|
+
void Game::InitGameObjects() {
|
313
|
+
player = std::make_unique<Player>(
|
314
|
+
Vector2{screenWidth / 2.0f, screenHeight / 2.0f}, 15.0f, BLUE, 200.0f);
|
285
|
-
|
315
|
+
positionManager = PositionManager();
|
316
|
+
targets.clear();
|
317
|
+
enemies.clear();
|
318
|
+
AddTarget();
|
319
|
+
AddEnemy();
|
320
|
+
positionManager.AddDot(player.get());
|
286
|
-
|
321
|
+
positionManager.SetScoreIncrementCallback([this]() {
|
322
|
+
score++;
|
287
|
-
|
323
|
+
AddTarget();
|
324
|
+
AddEnemy();
|
325
|
+
});
|
288
|
-
|
326
|
+
positionManager.SetGameOverCallback([this]() { gameOver = true; });
|
289
|
-
|
327
|
+
}
|
290
328
|
|
291
|
-
|
329
|
+
void Game::AddTarget() {
|
292
|
-
if (!gameOver) {
|
293
|
-
|
330
|
+
auto t = std::make_unique<Target>(positionManager.GetValidPosition(10.0f),
|
331
|
+
10.0f, RED);
|
294
|
-
|
332
|
+
positionManager.AddDot(t.get());
|
333
|
+
targets.push_back(std::move(t));
|
295
|
-
|
334
|
+
}
|
296
|
-
}
|
297
335
|
|
298
|
-
|
336
|
+
void Game::AddEnemy() {
|
337
|
+
auto e = std::make_unique<Enemy>(positionManager.GetValidPosition(12.0f),
|
299
|
-
|
338
|
+
12.0f, DARKGREEN, 120.0f);
|
300
|
-
|
339
|
+
positionManager.AddDot(e.get());
|
340
|
+
enemies.push_back(std::move(e));
|
341
|
+
}
|
301
342
|
|
343
|
+
void Game::Reset() {
|
344
|
+
score = 0;
|
345
|
+
gameOver = false;
|
346
|
+
InitGameObjects();
|
347
|
+
}
|
348
|
+
|
349
|
+
void Game::Update(float deltaTime) {
|
302
|
-
|
350
|
+
if (!gameOver) {
|
303
|
-
DrawText("Game Over!", screenWidth / 2 - 100, screenHeight / 2 - 40, 40,
|
304
|
-
RED);
|
305
|
-
DrawText(TextFormat("Final Score: %d", score), screenWidth / 2 - 100,
|
306
|
-
|
351
|
+
positionManager.Update(deltaTime);
|
307
|
-
|
352
|
+
} else {
|
308
|
-
DrawText("Catch the moving dot!", 10, 10, 20, DARKGRAY);
|
309
|
-
DrawText(TextFormat("Score: %d", score), 10, 40, 20, DARKGRAY);
|
310
|
-
|
353
|
+
if (IsKeyPressed(KEY_R)) {
|
311
|
-
target->Draw();
|
312
|
-
|
354
|
+
Reset();
|
313
355
|
}
|
314
|
-
EndDrawing();
|
315
356
|
}
|
357
|
+
}
|
316
358
|
|
359
|
+
void Game::Render() {
|
360
|
+
BeginDrawing();
|
361
|
+
ClearBackground(RAYWHITE);
|
362
|
+
if (gameOver) {
|
363
|
+
DrawText("Game Over!", screenWidth / 2 - 100, screenHeight / 2 - 40, 40,
|
364
|
+
RED);
|
365
|
+
DrawText(TextFormat("Final Score: %d", score), screenWidth / 2 - 100,
|
366
|
+
screenHeight / 2 + 10, 30, DARKGRAY);
|
367
|
+
DrawText("Press R to Restart", screenWidth / 2 - 120, screenHeight / 2 + 60,
|
368
|
+
28, DARKBLUE);
|
369
|
+
} else {
|
370
|
+
DrawText("Catch the moving dot!", 10, 10, 20, DARKGRAY);
|
317
|
-
|
371
|
+
DrawText(TextFormat("Score: %d", score), 10, 40, 20, DARKGRAY);
|
372
|
+
player->Draw();
|
373
|
+
for (const auto &t : targets)
|
374
|
+
t->Draw();
|
375
|
+
for (const auto &e : enemies)
|
376
|
+
e->Draw();
|
318
|
-
}
|
377
|
+
}
|
378
|
+
EndDrawing();
|
379
|
+
}
|
319
380
|
|
320
381
|
Game *gameInstance = nullptr;
|
321
382
|
|
@@ -340,7 +401,7 @@
|
|
340
401
|
#ifdef PLATFORM_WEB
|
341
402
|
emscripten_set_main_loop(MainLoop, 0, 1);
|
342
403
|
#else
|
343
|
-
while (
|
404
|
+
while (!WindowShouldClose()) {
|
344
405
|
float deltaTime = GetFrameTime();
|
345
406
|
game.Update(deltaTime);
|
346
407
|
game.Render();
|