Simple tower defense tutorial, part 13: Framerate independent smooth tower movement
While the display of the tower and the arrows looks nice when it is not moved, the rigid cell movement looks almost distracting. We can improve this by smoothing the movement of the tower from one frame to the next.
Let's change just a few lines of code to smooth the tower movement before covering the math that this is based on:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4 #include <stdlib.h>
5 #include <math.h>
6
7 //# Variables
8 GUIState guiState = {0};
9 GameTime gameTime = {0};
10
11 Model floorTileAModel = {0};
12 Model floorTileBModel = {0};
13 Model treeModel[2] = {0};
14 Model firTreeModel[2] = {0};
15 Model rockModels[5] = {0};
16 Model grassPatchModel[1] = {0};
17
18 Model pathArrowModel = {0};
19 Model greenArrowModel = {0};
20
21 Texture2D palette, spriteSheet;
22
23 Level levels[] = {
24 [0] = {
25 .state = LEVEL_STATE_BUILDING,
26 .initialGold = 20,
27 .waves[0] = {
28 .enemyType = ENEMY_TYPE_MINION,
29 .wave = 0,
30 .count = 5,
31 .interval = 2.5f,
32 .delay = 1.0f,
33 .spawnPosition = {2, 6},
34 },
35 .waves[1] = {
36 .enemyType = ENEMY_TYPE_MINION,
37 .wave = 0,
38 .count = 5,
39 .interval = 2.5f,
40 .delay = 1.0f,
41 .spawnPosition = {-2, 6},
42 },
43 .waves[2] = {
44 .enemyType = ENEMY_TYPE_MINION,
45 .wave = 1,
46 .count = 20,
47 .interval = 1.5f,
48 .delay = 1.0f,
49 .spawnPosition = {0, 6},
50 },
51 .waves[3] = {
52 .enemyType = ENEMY_TYPE_MINION,
53 .wave = 2,
54 .count = 30,
55 .interval = 1.2f,
56 .delay = 1.0f,
57 .spawnPosition = {0, 6},
58 }
59 },
60 };
61
62 Level *currentLevel = levels;
63
64 //# Game
65
66 static Model LoadGLBModel(char *filename)
67 {
68 Model model = LoadModel(TextFormat("data/%s.glb",filename));
69 for (int i = 0; i < model.materialCount; i++)
70 {
71 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
72 }
73 return model;
74 }
75
76 void LoadAssets()
77 {
78 // load a sprite sheet that contains all units
79 spriteSheet = LoadTexture("data/spritesheet.png");
80 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
81
82 // we'll use a palette texture to colorize the all buildings and environment art
83 palette = LoadTexture("data/palette.png");
84 // The texture uses gradients on very small space, so we'll enable bilinear filtering
85 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
86
87 floorTileAModel = LoadGLBModel("floor-tile-a");
88 floorTileBModel = LoadGLBModel("floor-tile-b");
89 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
90 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
91 firTreeModel[0] = LoadGLBModel("firtree-1-a");
92 firTreeModel[1] = LoadGLBModel("firtree-1-b");
93 rockModels[0] = LoadGLBModel("rock-1");
94 rockModels[1] = LoadGLBModel("rock-2");
95 rockModels[2] = LoadGLBModel("rock-3");
96 rockModels[3] = LoadGLBModel("rock-4");
97 rockModels[4] = LoadGLBModel("rock-5");
98 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
99
100 pathArrowModel = LoadGLBModel("direction-arrow-x");
101 greenArrowModel = LoadGLBModel("green-arrow");
102 }
103
104 void InitLevel(Level *level)
105 {
106 level->seed = (int)(GetTime() * 100.0f);
107
108 TowerInit();
109 EnemyInit();
110 ProjectileInit();
111 ParticleInit();
112 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
113
114 level->placementMode = 0;
115 level->state = LEVEL_STATE_BUILDING;
116 level->nextState = LEVEL_STATE_NONE;
117 level->playerGold = level->initialGold;
118 level->currentWave = 0;
119
120 Camera *camera = &level->camera;
121 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
122 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
123 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
124 camera->fovy = 10.0f;
125 camera->projection = CAMERA_ORTHOGRAPHIC;
126 }
127
128 void DrawLevelHud(Level *level)
129 {
130 const char *text = TextFormat("Gold: %d", level->playerGold);
131 Font font = GetFontDefault();
132 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
133 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
134 }
135
136 void DrawLevelReportLostWave(Level *level)
137 {
138 BeginMode3D(level->camera);
139 DrawLevelGround(level);
140 TowerDraw();
141 EnemyDraw();
142 ProjectileDraw();
143 ParticleDraw();
144 guiState.isBlocked = 0;
145 EndMode3D();
146
147 TowerDrawHealthBars(level->camera);
148
149 const char *text = "Wave lost";
150 int textWidth = MeasureText(text, 20);
151 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
152
153 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
154 {
155 level->nextState = LEVEL_STATE_RESET;
156 }
157 }
158
159 int HasLevelNextWave(Level *level)
160 {
161 for (int i = 0; i < 10; i++)
162 {
163 EnemyWave *wave = &level->waves[i];
164 if (wave->wave == level->currentWave)
165 {
166 return 1;
167 }
168 }
169 return 0;
170 }
171
172 void DrawLevelReportWonWave(Level *level)
173 {
174 BeginMode3D(level->camera);
175 DrawLevelGround(level);
176 TowerDraw();
177 EnemyDraw();
178 ProjectileDraw();
179 ParticleDraw();
180 guiState.isBlocked = 0;
181 EndMode3D();
182
183 TowerDrawHealthBars(level->camera);
184
185 const char *text = "Wave won";
186 int textWidth = MeasureText(text, 20);
187 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
188
189
190 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
191 {
192 level->nextState = LEVEL_STATE_RESET;
193 }
194
195 if (HasLevelNextWave(level))
196 {
197 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
198 {
199 level->nextState = LEVEL_STATE_BUILDING;
200 }
201 }
202 else {
203 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
204 {
205 level->nextState = LEVEL_STATE_WON_LEVEL;
206 }
207 }
208 }
209
210 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
211 {
212 static ButtonState buttonStates[8] = {0};
213 int cost = GetTowerCosts(towerType);
214 const char *text = TextFormat("%s: %d", name, cost);
215 buttonStates[towerType].isSelected = level->placementMode == towerType;
216 buttonStates[towerType].isDisabled = level->playerGold < cost;
217 if (Button(text, x, y, width, height, &buttonStates[towerType]))
218 {
219 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
220 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
221 }
222 }
223
224 float GetRandomFloat(float min, float max)
225 {
226 int random = GetRandomValue(0, 0xfffffff);
227 return ((float)random / (float)0xfffffff) * (max - min) + min;
228 }
229
230 void DrawLevelGround(Level *level)
231 {
232 // draw checkerboard ground pattern
233 for (int x = -5; x <= 5; x += 1)
234 {
235 for (int y = -5; y <= 5; y += 1)
236 {
237 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
238 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
239 }
240 }
241
242 int oldSeed = GetRandomValue(0, 0xfffffff);
243 SetRandomSeed(level->seed);
244 // increase probability for trees via duplicated entries
245 Model borderModels[64];
246 int maxRockCount = GetRandomValue(2, 6);
247 int maxTreeCount = GetRandomValue(10, 20);
248 int maxFirTreeCount = GetRandomValue(5, 10);
249 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
250 int grassPatchCount = GetRandomValue(5, 30);
251
252 int modelCount = 0;
253 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
254 {
255 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
256 }
257 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
258 {
259 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
260 }
261 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
262 {
263 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
264 }
265 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
266 {
267 borderModels[modelCount++] = grassPatchModel[0];
268 }
269
270 // draw some objects around the border of the map
271 Vector3 up = {0, 1, 0};
272 // a pseudo random number generator to get the same result every time
273 const float wiggle = 0.75f;
274 const int layerCount = 3;
275 for (int layer = 0; layer < layerCount; layer++)
276 {
277 int layerPos = 6 + layer;
278 for (int x = -6 + layer; x <= 6 + layer; x += 1)
279 {
280 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
281 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
282 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
283 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
284 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
285 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
286 }
287
288 for (int z = -5 + layer; z <= 5 + layer; z += 1)
289 {
290 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
291 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
292 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
293 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
294 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
295 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
296 }
297 }
298
299 SetRandomSeed(oldSeed);
300 }
301
302 void DrawEnemyPath(Level *level, Color arrowColor)
303 {
304 const int castleX = 0, castleY = 0;
305 const int maxWaypointCount = 200;
306 const float timeStep = 1.0f;
307 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
308
309 // we start with a time offset to simulate the path,
310 // this way the arrows are animated in a forward moving direction
311 // The time is wrapped around the time step to get a smooth animation
312 float timeOffset = fmodf(GetTime(), timeStep);
313
314 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
315 {
316 EnemyWave *wave = &level->waves[i];
317 if (wave->wave != level->currentWave)
318 {
319 continue;
320 }
321
322 // use this dummy enemy to simulate the path
323 Enemy dummy = {
324 .enemyType = ENEMY_TYPE_MINION,
325 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
326 .nextX = wave->spawnPosition.x,
327 .nextY = wave->spawnPosition.y,
328 .currentX = wave->spawnPosition.x,
329 .currentY = wave->spawnPosition.y,
330 };
331
332 float deltaTime = timeOffset;
333 for (int j = 0; j < maxWaypointCount; j++)
334 {
335 int waypointPassedCount = 0;
336 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
337 // after the initial variable starting offset, we use a fixed time step
338 deltaTime = timeStep;
339 dummy.simPosition = pos;
340
341 // Update the dummy's position just like we do in the regular enemy update loop
342 for (int k = 0; k < waypointPassedCount; k++)
343 {
344 dummy.currentX = dummy.nextX;
345 dummy.currentY = dummy.nextY;
346 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
347 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
348 {
349 break;
350 }
351 }
352 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
353 {
354 break;
355 }
356
357 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
358 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
359 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
360 }
361 }
362 }
363
364 void DrawEnemyPaths(Level *level)
365 {
366 // disable depth testing for the path arrows
367 // flush the 3D batch to draw the arrows on top of everything
368 rlDrawRenderBatchActive();
369 rlDisableDepthTest();
370 DrawEnemyPath(level, (Color){64, 64, 64, 160});
371
372 rlDrawRenderBatchActive();
373 rlEnableDepthTest();
374 DrawEnemyPath(level, WHITE);
375 }
376
377 void DrawLevelBuildingPlacementState(Level *level)
378 {
379 BeginMode3D(level->camera);
380 DrawLevelGround(level);
381
382 int blockedCellCount = 0;
383 Vector2 blockedCells[1];
384 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
385 float planeDistance = ray.position.y / -ray.direction.y;
386 float planeX = ray.direction.x * planeDistance + ray.position.x;
387 float planeY = ray.direction.z * planeDistance + ray.position.z;
388 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
389 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
390 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
391 {
392 level->placementX = mapX;
393 level->placementY = mapY;
394 }
395 else
396 {
397 mapX = level->placementX;
398 mapY = level->placementY;
399 }
400 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
401 PathFindingMapUpdate(blockedCellCount, blockedCells);
402
403 TowerDraw();
404 EnemyDraw();
405 ProjectileDraw();
406 ParticleDraw();
407 DrawEnemyPaths(level);
408
409 const float lambda = 15.0f;
410 const float deltaTime = fmin(GetFrameTime(), 0.3f);
411 float factor = 1.0f - expf(-lambda * deltaTime);
412
413 level->placementTransitionPosition =
414 Vector2Lerp(
415 level->placementTransitionPosition,
416 (Vector2){mapX, mapY}, factor);
417
418 rlPushMatrix();
419 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y);
420 DrawCubeWires((Vector3){0.0f, 0.0f, 0.0f}, 1.0f, 0.0f, 1.0f, RED);
421 Tower dummy = {
422 .towerType = level->placementMode,
423 };
424 TowerDrawSingle(dummy);
425
426 float bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f;
427 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
428 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
429 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
430
431 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
432 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
433 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
434 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
435 rlPopMatrix();
436
437 guiState.isBlocked = 0;
438
439 EndMode3D();
440
441 TowerDrawHealthBars(level->camera);
442
443 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
444 {
445 level->nextState = LEVEL_STATE_BUILDING;
446 level->placementMode = TOWER_TYPE_NONE;
447 TraceLog(LOG_INFO, "Cancel building");
448 }
449
450 if (Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
451 {
452 level->nextState = LEVEL_STATE_BUILDING;
453 if (TowerTryAdd(level->placementMode, mapX, mapY))
454 {
455 level->playerGold -= GetTowerCosts(level->placementMode);
456 level->placementMode = TOWER_TYPE_NONE;
457 }
458 }
459 }
460
461 void DrawLevelBuildingState(Level *level)
462 {
463 BeginMode3D(level->camera);
464 DrawLevelGround(level);
465
466 PathFindingMapUpdate(0, 0);
467 TowerDraw();
468 EnemyDraw();
469 ProjectileDraw();
470 ParticleDraw();
471 DrawEnemyPaths(level);
472
473 guiState.isBlocked = 0;
474
475 EndMode3D();
476
477 TowerDrawHealthBars(level->camera);
478
479 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
480 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
481 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
482 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
483
484 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
485 {
486 level->nextState = LEVEL_STATE_RESET;
487 }
488
489 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
490 {
491 level->nextState = LEVEL_STATE_BATTLE;
492 }
493
494 const char *text = "Building phase";
495 int textWidth = MeasureText(text, 20);
496 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
497 }
498
499 void InitBattleStateConditions(Level *level)
500 {
501 level->state = LEVEL_STATE_BATTLE;
502 level->nextState = LEVEL_STATE_NONE;
503 level->waveEndTimer = 0.0f;
504 for (int i = 0; i < 10; i++)
505 {
506 EnemyWave *wave = &level->waves[i];
507 wave->spawned = 0;
508 wave->timeToSpawnNext = wave->delay;
509 }
510 }
511
512 void DrawLevelBattleState(Level *level)
513 {
514 BeginMode3D(level->camera);
515 DrawLevelGround(level);
516 TowerDraw();
517 EnemyDraw();
518 ProjectileDraw();
519 ParticleDraw();
520 guiState.isBlocked = 0;
521 EndMode3D();
522
523 EnemyDrawHealthbars(level->camera);
524 TowerDrawHealthBars(level->camera);
525
526 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
527 {
528 level->nextState = LEVEL_STATE_RESET;
529 }
530
531 int maxCount = 0;
532 int remainingCount = 0;
533 for (int i = 0; i < 10; i++)
534 {
535 EnemyWave *wave = &level->waves[i];
536 if (wave->wave != level->currentWave)
537 {
538 continue;
539 }
540 maxCount += wave->count;
541 remainingCount += wave->count - wave->spawned;
542 }
543 int aliveCount = EnemyCount();
544 remainingCount += aliveCount;
545
546 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
547 int textWidth = MeasureText(text, 20);
548 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
549 }
550
551 void DrawLevel(Level *level)
552 {
553 switch (level->state)
554 {
555 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
556 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
557 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
558 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
559 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
560 default: break;
561 }
562
563 DrawLevelHud(level);
564 }
565
566 void UpdateLevel(Level *level)
567 {
568 if (level->state == LEVEL_STATE_BATTLE)
569 {
570 int activeWaves = 0;
571 for (int i = 0; i < 10; i++)
572 {
573 EnemyWave *wave = &level->waves[i];
574 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
575 {
576 continue;
577 }
578 activeWaves++;
579 wave->timeToSpawnNext -= gameTime.deltaTime;
580 if (wave->timeToSpawnNext <= 0.0f)
581 {
582 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
583 if (enemy)
584 {
585 wave->timeToSpawnNext = wave->interval;
586 wave->spawned++;
587 }
588 }
589 }
590 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
591 level->waveEndTimer += gameTime.deltaTime;
592 if (level->waveEndTimer >= 2.0f)
593 {
594 level->nextState = LEVEL_STATE_LOST_WAVE;
595 }
596 }
597 else if (activeWaves == 0 && EnemyCount() == 0)
598 {
599 level->waveEndTimer += gameTime.deltaTime;
600 if (level->waveEndTimer >= 2.0f)
601 {
602 level->nextState = LEVEL_STATE_WON_WAVE;
603 }
604 }
605 }
606
607 PathFindingMapUpdate(0, 0);
608 EnemyUpdate();
609 TowerUpdate();
610 ProjectileUpdate();
611 ParticleUpdate();
612
613 if (level->nextState == LEVEL_STATE_RESET)
614 {
615 InitLevel(level);
616 }
617
618 if (level->nextState == LEVEL_STATE_BATTLE)
619 {
620 InitBattleStateConditions(level);
621 }
622
623 if (level->nextState == LEVEL_STATE_WON_WAVE)
624 {
625 level->currentWave++;
626 level->state = LEVEL_STATE_WON_WAVE;
627 }
628
629 if (level->nextState == LEVEL_STATE_LOST_WAVE)
630 {
631 level->state = LEVEL_STATE_LOST_WAVE;
632 }
633
634 if (level->nextState == LEVEL_STATE_BUILDING)
635 {
636 level->state = LEVEL_STATE_BUILDING;
637 }
638
639 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
640 {
641 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
642 level->placementTransitionPosition = (Vector2){
643 level->placementX, level->placementY};
644 }
645
646 if (level->nextState == LEVEL_STATE_WON_LEVEL)
647 {
648 // make something of this later
649 InitLevel(level);
650 }
651
652 level->nextState = LEVEL_STATE_NONE;
653 }
654
655 float nextSpawnTime = 0.0f;
656
657 void ResetGame()
658 {
659 InitLevel(currentLevel);
660 }
661
662 void InitGame()
663 {
664 TowerInit();
665 EnemyInit();
666 ProjectileInit();
667 ParticleInit();
668 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
669
670 currentLevel = levels;
671 InitLevel(currentLevel);
672 }
673
674 //# Immediate GUI functions
675
676 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
677 {
678 const float healthBarHeight = 6.0f;
679 const float healthBarOffset = 15.0f;
680 const float inset = 2.0f;
681 const float innerWidth = healthBarWidth - inset * 2;
682 const float innerHeight = healthBarHeight - inset * 2;
683
684 Vector2 screenPos = GetWorldToScreen(position, camera);
685 float centerX = screenPos.x - healthBarWidth * 0.5f;
686 float topY = screenPos.y - healthBarOffset;
687 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
688 float healthWidth = innerWidth * healthRatio;
689 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
690 }
691
692 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
693 {
694 Rectangle bounds = {x, y, width, height};
695 int isPressed = 0;
696 int isSelected = state && state->isSelected;
697 int isDisabled = state && state->isDisabled;
698 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
699 {
700 Color color = isSelected ? DARKGRAY : GRAY;
701 DrawRectangle(x, y, width, height, color);
702 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
703 {
704 isPressed = 1;
705 }
706 guiState.isBlocked = 1;
707 }
708 else
709 {
710 Color color = isSelected ? WHITE : LIGHTGRAY;
711 DrawRectangle(x, y, width, height, color);
712 }
713 Font font = GetFontDefault();
714 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
715 Color textColor = isDisabled ? GRAY : BLACK;
716 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
717 return isPressed;
718 }
719
720 //# Main game loop
721
722 void GameUpdate()
723 {
724 float dt = GetFrameTime();
725 // cap maximum delta time to 0.1 seconds to prevent large time steps
726 if (dt > 0.1f) dt = 0.1f;
727 gameTime.time += dt;
728 gameTime.deltaTime = dt;
729
730 UpdateLevel(currentLevel);
731 }
732
733 int main(void)
734 {
735 int screenWidth, screenHeight;
736 GetPreferredSize(&screenWidth, &screenHeight);
737 InitWindow(screenWidth, screenHeight, "Tower defense");
738 SetTargetFPS(30);
739
740 LoadAssets();
741 InitGame();
742
743 while (!WindowShouldClose())
744 {
745 if (IsPaused()) {
746 // canvas is not visible in browser - do nothing
747 continue;
748 }
749
750 BeginDrawing();
751 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
752
753 GameUpdate();
754 DrawLevel(currentLevel);
755
756 EndDrawing();
757 }
758
759 CloseWindow();
760
761 return 0;
762 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 Vector3 scale;
28 } Particle;
29
30 #define TOWER_MAX_COUNT 400
31 enum TowerType
32 {
33 TOWER_TYPE_NONE,
34 TOWER_TYPE_BASE,
35 TOWER_TYPE_ARCHER,
36 TOWER_TYPE_BALLISTA,
37 TOWER_TYPE_CATAPULT,
38 TOWER_TYPE_WALL,
39 TOWER_TYPE_COUNT
40 };
41
42 typedef struct HitEffectConfig
43 {
44 float damage;
45 float areaDamageRadius;
46 float pushbackPowerDistance;
47 } HitEffectConfig;
48
49 typedef struct TowerTypeConfig
50 {
51 float cooldown;
52 float range;
53 float projectileSpeed;
54
55 uint8_t cost;
56 uint8_t projectileType;
57 uint16_t maxHealth;
58
59 HitEffectConfig hitEffect;
60 } TowerTypeConfig;
61
62 typedef struct Tower
63 {
64 int16_t x, y;
65 uint8_t towerType;
66 Vector2 lastTargetPosition;
67 float cooldown;
68 float damage;
69 } Tower;
70
71 typedef struct GameTime
72 {
73 float time;
74 float deltaTime;
75 } GameTime;
76
77 typedef struct ButtonState {
78 char isSelected;
79 char isDisabled;
80 } ButtonState;
81
82 typedef struct GUIState {
83 int isBlocked;
84 } GUIState;
85
86 typedef enum LevelState
87 {
88 LEVEL_STATE_NONE,
89 LEVEL_STATE_BUILDING,
90 LEVEL_STATE_BUILDING_PLACEMENT,
91 LEVEL_STATE_BATTLE,
92 LEVEL_STATE_WON_WAVE,
93 LEVEL_STATE_LOST_WAVE,
94 LEVEL_STATE_WON_LEVEL,
95 LEVEL_STATE_RESET,
96 } LevelState;
97
98 typedef struct EnemyWave {
99 uint8_t enemyType;
100 uint8_t wave;
101 uint16_t count;
102 float interval;
103 float delay;
104 Vector2 spawnPosition;
105
106 uint16_t spawned;
107 float timeToSpawnNext;
108 } EnemyWave;
109
110 #define ENEMY_MAX_WAVE_COUNT 10
111
112 typedef struct Level
113 {
114 int seed;
115 LevelState state;
116 LevelState nextState;
117 Camera3D camera;
118 int placementMode;
119 int16_t placementX;
120 int16_t placementY;
121 Vector2 placementTransitionPosition;
122
123 int initialGold;
124 int playerGold;
125
126 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
127 int currentWave;
128 float waveEndTimer;
129 } Level;
130
131 typedef struct DeltaSrc
132 {
133 char x, y;
134 } DeltaSrc;
135
136 typedef struct PathfindingMap
137 {
138 int width, height;
139 float scale;
140 float *distances;
141 long *towerIndex;
142 DeltaSrc *deltaSrc;
143 float maxDistance;
144 Matrix toMapSpace;
145 Matrix toWorldSpace;
146 } PathfindingMap;
147
148 // when we execute the pathfinding algorithm, we need to store the active nodes
149 // in a queue. Each node has a position, a distance from the start, and the
150 // position of the node that we came from.
151 typedef struct PathfindingNode
152 {
153 int16_t x, y, fromX, fromY;
154 float distance;
155 } PathfindingNode;
156
157 typedef struct EnemyId
158 {
159 uint16_t index;
160 uint16_t generation;
161 } EnemyId;
162
163 typedef struct EnemyClassConfig
164 {
165 float speed;
166 float health;
167 float radius;
168 float maxAcceleration;
169 float requiredContactTime;
170 float explosionDamage;
171 float explosionRange;
172 float explosionPushbackPower;
173 int goldValue;
174 } EnemyClassConfig;
175
176 typedef struct Enemy
177 {
178 int16_t currentX, currentY;
179 int16_t nextX, nextY;
180 Vector2 simPosition;
181 Vector2 simVelocity;
182 uint16_t generation;
183 float walkedDistance;
184 float startMovingTime;
185 float damage, futureDamage;
186 float contactTime;
187 uint8_t enemyType;
188 uint8_t movePathCount;
189 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
190 } Enemy;
191
192 // a unit that uses sprites to be drawn
193 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
194 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
195 typedef struct SpriteUnit
196 {
197 Rectangle srcRect;
198 Vector2 offset;
199 int frameCount;
200 float frameDuration;
201 Rectangle srcWeaponIdleRect;
202 Vector2 srcWeaponIdleOffset;
203 Rectangle srcWeaponCooldownRect;
204 Vector2 srcWeaponCooldownOffset;
205 } SpriteUnit;
206
207 #define PROJECTILE_MAX_COUNT 1200
208 #define PROJECTILE_TYPE_NONE 0
209 #define PROJECTILE_TYPE_ARROW 1
210 #define PROJECTILE_TYPE_CATAPULT 2
211 #define PROJECTILE_TYPE_BALLISTA 3
212
213 typedef struct Projectile
214 {
215 uint8_t projectileType;
216 float shootTime;
217 float arrivalTime;
218 float distance;
219 Vector3 position;
220 Vector3 target;
221 Vector3 directionNormal;
222 EnemyId targetEnemy;
223 HitEffectConfig hitEffectConfig;
224 } Projectile;
225
226 //# Function declarations
227 float TowerGetMaxHealth(Tower *tower);
228 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
229 int EnemyAddDamageRange(Vector2 position, float range, float damage);
230 int EnemyAddDamage(Enemy *enemy, float damage);
231
232 //# Enemy functions
233 void EnemyInit();
234 void EnemyDraw();
235 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
236 void EnemyUpdate();
237 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
238 float EnemyGetMaxHealth(Enemy *enemy);
239 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
240 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
241 EnemyId EnemyGetId(Enemy *enemy);
242 Enemy *EnemyTryResolve(EnemyId enemyId);
243 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
244 int EnemyAddDamage(Enemy *enemy, float damage);
245 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
246 int EnemyCount();
247 void EnemyDrawHealthbars(Camera3D camera);
248
249 //# Tower functions
250 void TowerInit();
251 Tower *TowerGetAt(int16_t x, int16_t y);
252 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
253 Tower *GetTowerByType(uint8_t towerType);
254 int GetTowerCosts(uint8_t towerType);
255 float TowerGetMaxHealth(Tower *tower);
256 void TowerDraw();
257 void TowerDrawSingle(Tower tower);
258 void TowerUpdate();
259 void TowerDrawHealthBars(Camera3D camera);
260 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
261
262 //# Particles
263 void ParticleInit();
264 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
265 void ParticleUpdate();
266 void ParticleDraw();
267
268 //# Projectiles
269 void ProjectileInit();
270 void ProjectileDraw();
271 void ProjectileUpdate();
272 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
273
274 //# Pathfinding map
275 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
276 float PathFindingGetDistance(int mapX, int mapY);
277 Vector2 PathFindingGetGradient(Vector3 world);
278 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
279 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
280 void PathFindingMapDraw();
281
282 //# UI
283 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
284
285 //# Level
286 void DrawLevelGround(Level *level);
287 void DrawEnemyPath(Level *level, Color arrowColor);
288
289 //# variables
290 extern Level *currentLevel;
291 extern Enemy enemies[ENEMY_MAX_COUNT];
292 extern int enemyCount;
293 extern EnemyClassConfig enemyClassConfigs[];
294
295 extern GUIState guiState;
296 extern GameTime gameTime;
297 extern Tower towers[TOWER_MAX_COUNT];
298 extern int towerCount;
299
300 extern Texture2D palette, spriteSheet;
301
302 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 6.0f,
28 .pushbackPowerDistance = 0.25f,
29 }
30 },
31 [TOWER_TYPE_CATAPULT] = {
32 .cooldown = 1.7f,
33 .range = 5.0f,
34 .cost = 10,
35 .maxHealth = 10,
36 .projectileSpeed = 3.0f,
37 .projectileType = PROJECTILE_TYPE_CATAPULT,
38 .hitEffect = {
39 .damage = 2.0f,
40 .areaDamageRadius = 1.75f,
41 }
42 },
43 [TOWER_TYPE_WALL] = {
44 .cost = 2,
45 .maxHealth = 10,
46 },
47 };
48
49 Tower towers[TOWER_MAX_COUNT];
50 int towerCount = 0;
51
52 Model towerModels[TOWER_TYPE_COUNT];
53
54 // definition of our archer unit
55 SpriteUnit archerUnit = {
56 .srcRect = {0, 0, 16, 16},
57 .offset = {7, 1},
58 .frameCount = 1,
59 .frameDuration = 0.0f,
60 .srcWeaponIdleRect = {16, 0, 6, 16},
61 .srcWeaponIdleOffset = {8, 0},
62 .srcWeaponCooldownRect = {22, 0, 11, 16},
63 .srcWeaponCooldownOffset = {10, 0},
64 };
65
66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
67 {
68 float xScale = flip ? -1.0f : 1.0f;
69 Camera3D camera = currentLevel->camera;
70 float size = 0.5f;
71 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
72 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
73 // we want the sprite to face the camera, so we need to calculate the up vector
74 Vector3 forward = Vector3Subtract(camera.target, camera.position);
75 Vector3 up = {0, 1, 0};
76 Vector3 right = Vector3CrossProduct(forward, up);
77 up = Vector3Normalize(Vector3CrossProduct(right, forward));
78
79 Rectangle srcRect = unit.srcRect;
80 if (unit.frameCount > 1)
81 {
82 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
83 }
84 if (flip)
85 {
86 srcRect.x += srcRect.width;
87 srcRect.width = -srcRect.width;
88 }
89 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
90
91 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
92 {
93 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
94 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
95 srcRect = unit.srcWeaponCooldownRect;
96 if (flip)
97 {
98 // position.x = flip * scale.x * 0.5f;
99 srcRect.x += srcRect.width;
100 srcRect.width = -srcRect.width;
101 offset.x = scale.x - offset.x;
102 }
103 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104 }
105 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106 {
107 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109 srcRect = unit.srcWeaponIdleRect;
110 if (flip)
111 {
112 // position.x = flip * scale.x * 0.5f;
113 srcRect.x += srcRect.width;
114 srcRect.width = -srcRect.width;
115 offset.x = scale.x - offset.x;
116 }
117 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118 }
119 }
120
121 void TowerInit()
122 {
123 for (int i = 0; i < TOWER_MAX_COUNT; i++)
124 {
125 towers[i] = (Tower){0};
126 }
127 towerCount = 0;
128
129 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131
132 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133 {
134 if (towerModels[i].materials)
135 {
136 // assign the palette texture to the material of the model (0 is not used afaik)
137 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138 }
139 }
140 }
141
142 static void TowerGunUpdate(Tower *tower)
143 {
144 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145 if (tower->cooldown <= 0.0f)
146 {
147 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148 if (enemy)
149 {
150 tower->cooldown = config.cooldown;
151 // shoot the enemy; determine future position of the enemy
152 float bulletSpeed = config.projectileSpeed;
153 Vector2 velocity = enemy->simVelocity;
154 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155 Vector2 towerPosition = {tower->x, tower->y};
156 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157 for (int i = 0; i < 8; i++) {
158 velocity = enemy->simVelocity;
159 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160 float distance = Vector2Distance(towerPosition, futurePosition);
161 float eta2 = distance / bulletSpeed;
162 if (fabs(eta - eta2) < 0.01f) {
163 break;
164 }
165 eta = (eta2 + eta) * 0.5f;
166 }
167
168 ProjectileTryAdd(config.projectileType, enemy,
169 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
170 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171 bulletSpeed, config.hitEffect);
172 enemy->futureDamage += config.hitEffect.damage;
173 tower->lastTargetPosition = futurePosition;
174 }
175 }
176 else
177 {
178 tower->cooldown -= gameTime.deltaTime;
179 }
180 }
181
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184 for (int i = 0; i < towerCount; i++)
185 {
186 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187 {
188 return &towers[i];
189 }
190 }
191 return 0;
192 }
193
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196 if (towerCount >= TOWER_MAX_COUNT)
197 {
198 return 0;
199 }
200
201 Tower *tower = TowerGetAt(x, y);
202 if (tower)
203 {
204 return 0;
205 }
206
207 tower = &towers[towerCount++];
208 tower->x = x;
209 tower->y = y;
210 tower->towerType = towerType;
211 tower->cooldown = 0.0f;
212 tower->damage = 0.0f;
213 return tower;
214 }
215
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218 for (int i = 0; i < towerCount; i++)
219 {
220 if (towers[i].towerType == towerType)
221 {
222 return &towers[i];
223 }
224 }
225 return 0;
226 }
227
228 int GetTowerCosts(uint8_t towerType)
229 {
230 return towerTypeConfigs[towerType].cost;
231 }
232
233 float TowerGetMaxHealth(Tower *tower)
234 {
235 return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237
238 void TowerDrawSingle(Tower tower)
239 {
240 if (tower.towerType == TOWER_TYPE_NONE)
241 {
242 return;
243 }
244
245 switch (tower.towerType)
246 {
247 case TOWER_TYPE_ARCHER:
248 {
249 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
250 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
251 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
252 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
253 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
254 }
255 break;
256 case TOWER_TYPE_BALLISTA:
257 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
258 break;
259 case TOWER_TYPE_CATAPULT:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
261 break;
262 default:
263 if (towerModels[tower.towerType].materials)
264 {
265 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
266 } else {
267 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
268 }
269 break;
270 }
271 }
272
273 void TowerDraw()
274 {
275 for (int i = 0; i < towerCount; i++)
276 {
277 TowerDrawSingle(towers[i]);
278 }
279 }
280
281 void TowerUpdate()
282 {
283 for (int i = 0; i < towerCount; i++)
284 {
285 Tower *tower = &towers[i];
286 switch (tower->towerType)
287 {
288 case TOWER_TYPE_CATAPULT:
289 case TOWER_TYPE_BALLISTA:
290 case TOWER_TYPE_ARCHER:
291 TowerGunUpdate(tower);
292 break;
293 }
294 }
295 }
296
297 void TowerDrawHealthBars(Camera3D camera)
298 {
299 for (int i = 0; i < towerCount; i++)
300 {
301 Tower *tower = &towers[i];
302 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
303 {
304 continue;
305 }
306
307 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
308 float maxHealth = TowerGetMaxHealth(tower);
309 float health = maxHealth - tower->damage;
310 float healthRatio = health / maxHealth;
311
312 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
313 }
314 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 SpriteUnit enemySprites[] = {
24 [ENEMY_TYPE_MINION] = {
25 .srcRect = {0, 16, 16, 16},
26 .offset = {8.0f, 0.0f},
27 .frameCount = 6,
28 .frameDuration = 0.1f,
29 },
30 };
31
32 void EnemyInit()
33 {
34 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
35 {
36 enemies[i] = (Enemy){0};
37 }
38 enemyCount = 0;
39 }
40
41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
42 {
43 return enemyClassConfigs[enemy->enemyType].speed;
44 }
45
46 float EnemyGetMaxHealth(Enemy *enemy)
47 {
48 return enemyClassConfigs[enemy->enemyType].health;
49 }
50
51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
52 {
53 int16_t castleX = 0;
54 int16_t castleY = 0;
55 int16_t dx = castleX - currentX;
56 int16_t dy = castleY - currentY;
57 if (dx == 0 && dy == 0)
58 {
59 *nextX = currentX;
60 *nextY = currentY;
61 return 1;
62 }
63 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
64
65 if (gradient.x == 0 && gradient.y == 0)
66 {
67 *nextX = currentX;
68 *nextY = currentY;
69 return 1;
70 }
71
72 if (fabsf(gradient.x) > fabsf(gradient.y))
73 {
74 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
75 *nextY = currentY;
76 return 0;
77 }
78 *nextX = currentX;
79 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
80 return 0;
81 }
82
83
84 // this function predicts the movement of the unit for the next deltaT seconds
85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
86 {
87 const float pointReachedDistance = 0.25f;
88 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
89 const float maxSimStepTime = 0.015625f;
90
91 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
92 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
93 int16_t nextX = enemy->nextX;
94 int16_t nextY = enemy->nextY;
95 Vector2 position = enemy->simPosition;
96 int passedCount = 0;
97 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
98 {
99 float stepTime = fminf(deltaT - t, maxSimStepTime);
100 Vector2 target = (Vector2){nextX, nextY};
101 float speed = Vector2Length(*velocity);
102 // draw the target position for debugging
103 //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106 {
107 // we reached the target position, let's move to the next waypoint
108 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109 target = (Vector2){nextX, nextY};
110 // track how many waypoints we passed
111 passedCount++;
112 }
113
114 // acceleration towards the target
115 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117 *velocity = Vector2Add(*velocity, acceleration);
118
119 // limit the speed to the maximum speed
120 if (speed > maxSpeed)
121 {
122 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123 }
124
125 // move the enemy
126 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127 }
128
129 if (waypointPassedCount)
130 {
131 (*waypointPassedCount) = passedCount;
132 }
133
134 return position;
135 }
136
137 void EnemyDraw()
138 {
139 for (int i = 0; i < enemyCount; i++)
140 {
141 Enemy enemy = enemies[i];
142 if (enemy.enemyType == ENEMY_TYPE_NONE)
143 {
144 continue;
145 }
146
147 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148
149 // don't draw any trails for now; might replace this with footprints later
150 // if (enemy.movePathCount > 0)
151 // {
152 // Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153 // DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154 // }
155 // for (int j = 1; j < enemy.movePathCount; j++)
156 // {
157 // Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158 // Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159 // DrawLine3D(p, q, GREEN);
160 // }
161
162 switch (enemy.enemyType)
163 {
164 case ENEMY_TYPE_MINION:
165 DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y},
166 enemy.walkedDistance, 0, 0);
167 break;
168 }
169 }
170 }
171
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174 // damage the tower
175 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178 float explosionRange2 = explosionRange * explosionRange;
179 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180 // explode the enemy
181 if (tower->damage >= TowerGetMaxHealth(tower))
182 {
183 tower->towerType = TOWER_TYPE_NONE;
184 }
185
186 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
187 explosionSource,
188 (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189
190 enemy->enemyType = ENEMY_TYPE_NONE;
191
192 // push back enemies & dealing damage
193 for (int i = 0; i < enemyCount; i++)
194 {
195 Enemy *other = &enemies[i];
196 if (other->enemyType == ENEMY_TYPE_NONE)
197 {
198 continue;
199 }
200 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201 if (distanceSqr > 0 && distanceSqr < explosionRange2)
202 {
203 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205 EnemyAddDamage(other, explosionDamge);
206 }
207 }
208 }
209
210 void EnemyUpdate()
211 {
212 const float castleX = 0;
213 const float castleY = 0;
214 const float maxPathDistance2 = 0.25f * 0.25f;
215
216 for (int i = 0; i < enemyCount; i++)
217 {
218 Enemy *enemy = &enemies[i];
219 if (enemy->enemyType == ENEMY_TYPE_NONE)
220 {
221 continue;
222 }
223
224 int waypointPassedCount = 0;
225 Vector2 prevPosition = enemy->simPosition;
226 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227 enemy->startMovingTime = gameTime.time;
228 enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229 // track path of unit
230 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231 {
232 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233 {
234 enemy->movePath[j] = enemy->movePath[j - 1];
235 }
236 enemy->movePath[0] = enemy->simPosition;
237 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238 {
239 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240 }
241 }
242
243 if (waypointPassedCount > 0)
244 {
245 enemy->currentX = enemy->nextX;
246 enemy->currentY = enemy->nextY;
247 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249 {
250 // enemy reached the castle; remove it
251 enemy->enemyType = ENEMY_TYPE_NONE;
252 continue;
253 }
254 }
255 }
256
257 // handle collisions between enemies
258 for (int i = 0; i < enemyCount - 1; i++)
259 {
260 Enemy *enemyA = &enemies[i];
261 if (enemyA->enemyType == ENEMY_TYPE_NONE)
262 {
263 continue;
264 }
265 for (int j = i + 1; j < enemyCount; j++)
266 {
267 Enemy *enemyB = &enemies[j];
268 if (enemyB->enemyType == ENEMY_TYPE_NONE)
269 {
270 continue;
271 }
272 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275 float radiusSum = radiusA + radiusB;
276 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277 {
278 // collision
279 float distance = sqrtf(distanceSqr);
280 float overlap = radiusSum - distance;
281 // move the enemies apart, but softly; if we have a clog of enemies,
282 // moving them perfectly apart can cause them to jitter
283 float positionCorrection = overlap / 5.0f;
284 Vector2 direction = (Vector2){
285 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289 }
290 }
291 }
292
293 // handle collisions between enemies and towers
294 for (int i = 0; i < enemyCount; i++)
295 {
296 Enemy *enemy = &enemies[i];
297 if (enemy->enemyType == ENEMY_TYPE_NONE)
298 {
299 continue;
300 }
301 enemy->contactTime -= gameTime.deltaTime;
302 if (enemy->contactTime < 0.0f)
303 {
304 enemy->contactTime = 0.0f;
305 }
306
307 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308 // linear search over towers; could be optimized by using path finding tower map,
309 // but for now, we keep it simple
310 for (int j = 0; j < towerCount; j++)
311 {
312 Tower *tower = &towers[j];
313 if (tower->towerType == TOWER_TYPE_NONE)
314 {
315 continue;
316 }
317 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319 if (distanceSqr > combinedRadius * combinedRadius)
320 {
321 continue;
322 }
323 // potential collision; square / circle intersection
324 float dx = tower->x - enemy->simPosition.x;
325 float dy = tower->y - enemy->simPosition.y;
326 float absDx = fabsf(dx);
327 float absDy = fabsf(dy);
328 Vector3 contactPoint = {0};
329 if (absDx <= 0.5f && absDx <= absDy) {
330 // vertical collision; push the enemy out horizontally
331 float overlap = enemyRadius + 0.5f - absDy;
332 if (overlap < 0.0f)
333 {
334 continue;
335 }
336 float direction = dy > 0.0f ? -1.0f : 1.0f;
337 enemy->simPosition.y += direction * overlap;
338 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339 }
340 else if (absDy <= 0.5f && absDy <= absDx)
341 {
342 // horizontal collision; push the enemy out vertically
343 float overlap = enemyRadius + 0.5f - absDx;
344 if (overlap < 0.0f)
345 {
346 continue;
347 }
348 float direction = dx > 0.0f ? -1.0f : 1.0f;
349 enemy->simPosition.x += direction * overlap;
350 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351 }
352 else
353 {
354 // possible collision with a corner
355 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357 float cornerX = tower->x + cornerDX;
358 float cornerY = tower->y + cornerDY;
359 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360 if (cornerDistanceSqr > enemyRadius * enemyRadius)
361 {
362 continue;
363 }
364 // push the enemy out along the diagonal
365 float cornerDistance = sqrtf(cornerDistanceSqr);
366 float overlap = enemyRadius - cornerDistance;
367 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369 enemy->simPosition.x -= directionX * overlap;
370 enemy->simPosition.y -= directionY * overlap;
371 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372 }
373
374 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375 {
376 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378 {
379 EnemyTriggerExplode(enemy, tower, contactPoint);
380 }
381 }
382 }
383 }
384 }
385
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388 return (EnemyId){enemy - enemies, enemy->generation};
389 }
390
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393 if (enemyId.index >= ENEMY_MAX_COUNT)
394 {
395 return 0;
396 }
397 Enemy *enemy = &enemies[enemyId.index];
398 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 return 0;
401 }
402 return enemy;
403 }
404
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407 Enemy *spawn = 0;
408 for (int i = 0; i < enemyCount; i++)
409 {
410 Enemy *enemy = &enemies[i];
411 if (enemy->enemyType == ENEMY_TYPE_NONE)
412 {
413 spawn = enemy;
414 break;
415 }
416 }
417
418 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419 {
420 spawn = &enemies[enemyCount++];
421 }
422
423 if (spawn)
424 {
425 spawn->currentX = currentX;
426 spawn->currentY = currentY;
427 spawn->nextX = currentX;
428 spawn->nextY = currentY;
429 spawn->simPosition = (Vector2){currentX, currentY};
430 spawn->simVelocity = (Vector2){0, 0};
431 spawn->enemyType = enemyType;
432 spawn->startMovingTime = gameTime.time;
433 spawn->damage = 0.0f;
434 spawn->futureDamage = 0.0f;
435 spawn->generation++;
436 spawn->movePathCount = 0;
437 spawn->walkedDistance = 0.0f;
438 }
439
440 return spawn;
441 }
442
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445 int count = 0;
446 float range2 = range * range;
447 for (int i = 0; i < enemyCount; i++)
448 {
449 Enemy *enemy = &enemies[i];
450 if (enemy->enemyType == ENEMY_TYPE_NONE)
451 {
452 continue;
453 }
454 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455 if (distance2 <= range2)
456 {
457 EnemyAddDamage(enemy, damage);
458 count++;
459 }
460 }
461 return count;
462 }
463
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466 enemy->damage += damage;
467 if (enemy->damage >= EnemyGetMaxHealth(enemy))
468 {
469 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470 enemy->enemyType = ENEMY_TYPE_NONE;
471 return 1;
472 }
473
474 return 0;
475 }
476
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479 int16_t castleX = 0;
480 int16_t castleY = 0;
481 Enemy* closest = 0;
482 int16_t closestDistance = 0;
483 float range2 = range * range;
484 for (int i = 0; i < enemyCount; i++)
485 {
486 Enemy* enemy = &enemies[i];
487 if (enemy->enemyType == ENEMY_TYPE_NONE)
488 {
489 continue;
490 }
491 float maxHealth = EnemyGetMaxHealth(enemy);
492 if (enemy->futureDamage >= maxHealth)
493 {
494 // ignore enemies that will die soon
495 continue;
496 }
497 int16_t dx = castleX - enemy->currentX;
498 int16_t dy = castleY - enemy->currentY;
499 int16_t distance = abs(dx) + abs(dy);
500 if (!closest || distance < closestDistance)
501 {
502 float tdx = towerX - enemy->currentX;
503 float tdy = towerY - enemy->currentY;
504 float tdistance2 = tdx * tdx + tdy * tdy;
505 if (tdistance2 <= range2)
506 {
507 closest = enemy;
508 closestDistance = distance;
509 }
510 }
511 }
512 return closest;
513 }
514
515 int EnemyCount()
516 {
517 int count = 0;
518 for (int i = 0; i < enemyCount; i++)
519 {
520 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521 {
522 count++;
523 }
524 }
525 return count;
526 }
527
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530 for (int i = 0; i < enemyCount; i++)
531 {
532 Enemy *enemy = &enemies[i];
533 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534 {
535 continue;
536 }
537 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538 float maxHealth = EnemyGetMaxHealth(enemy);
539 float health = maxHealth - enemy->damage;
540 float healthRatio = health / maxHealth;
541
542 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543 }
544 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 typedef struct ProjectileConfig
8 {
9 float arcFactor;
10 Color color;
11 Color trailColor;
12 } ProjectileConfig;
13
14 ProjectileConfig projectileConfigs[] = {
15 [PROJECTILE_TYPE_ARROW] = {
16 .arcFactor = 0.15f,
17 .color = RED,
18 .trailColor = BROWN,
19 },
20 [PROJECTILE_TYPE_CATAPULT] = {
21 .arcFactor = 0.5f,
22 .color = RED,
23 .trailColor = GRAY,
24 },
25 [PROJECTILE_TYPE_BALLISTA] = {
26 .arcFactor = 0.025f,
27 .color = RED,
28 .trailColor = BROWN,
29 },
30 };
31
32 void ProjectileInit()
33 {
34 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
35 {
36 projectiles[i] = (Projectile){0};
37 }
38 }
39
40 void ProjectileDraw()
41 {
42 for (int i = 0; i < projectileCount; i++)
43 {
44 Projectile projectile = projectiles[i];
45 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
46 {
47 continue;
48 }
49 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
50 if (transition >= 1.0f)
51 {
52 continue;
53 }
54
55 ProjectileConfig config = projectileConfigs[projectile.projectileType];
56 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
57 {
58 float t = transition + transitionOffset * 0.3f;
59 if (t > 1.0f)
60 {
61 break;
62 }
63 Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
64 Color color = config.color;
65 color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
66 // fake a ballista flight path using parabola equation
67 float parabolaT = t - 0.5f;
68 parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
69 position.y += config.arcFactor * parabolaT * projectile.distance;
70
71 float size = 0.06f * (transitionOffset + 0.25f);
72 DrawCube(position, size, size, size, color);
73 }
74 }
75 }
76
77 void ProjectileUpdate()
78 {
79 for (int i = 0; i < projectileCount; i++)
80 {
81 Projectile *projectile = &projectiles[i];
82 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
83 {
84 continue;
85 }
86 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
87 if (transition >= 1.0f)
88 {
89 projectile->projectileType = PROJECTILE_TYPE_NONE;
90 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
91 if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
92 {
93 Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
94 enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
95 }
96
97 if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
98 {
99 EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100 // pancaked sphere explosion
101 float r = projectile->hitEffectConfig.areaDamageRadius;
102 ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103 }
104 else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105 {
106 EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107 }
108 continue;
109 }
110 }
111 }
112
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116 {
117 Projectile *projectile = &projectiles[i];
118 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119 {
120 projectile->projectileType = projectileType;
121 projectile->shootTime = gameTime.time;
122 float distance = Vector3Distance(position, target);
123 projectile->arrivalTime = gameTime.time + distance / speed;
124 projectile->position = position;
125 projectile->target = target;
126 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127 projectile->distance = distance;
128 projectile->targetEnemy = EnemyGetId(enemy);
129 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130 projectile->hitEffectConfig = hitEffectConfig;
131 return projectile;
132 }
133 }
134 return 0;
135 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4
5 static Particle particles[PARTICLE_MAX_COUNT];
6 static int particleCount = 0;
7
8 void ParticleInit()
9 {
10 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
11 {
12 particles[i] = (Particle){0};
13 }
14 particleCount = 0;
15 }
16
17 static void DrawExplosionParticle(Particle *particle, float transition)
18 {
19 Vector3 scale = particle->scale;
20 float size = 1.0f * (1.0f - transition);
21 Color startColor = WHITE;
22 Color endColor = RED;
23 Color color = ColorLerp(startColor, endColor, transition);
24
25 rlPushMatrix();
26 rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
27 rlScalef(scale.x, scale.y, scale.z);
28 DrawSphere(Vector3Zero(), size, color);
29 rlPopMatrix();
30 }
31
32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
33 {
34 if (particleCount >= PARTICLE_MAX_COUNT)
35 {
36 return;
37 }
38
39 int index = -1;
40 for (int i = 0; i < particleCount; i++)
41 {
42 if (particles[i].particleType == PARTICLE_TYPE_NONE)
43 {
44 index = i;
45 break;
46 }
47 }
48
49 if (index == -1)
50 {
51 index = particleCount++;
52 }
53
54 Particle *particle = &particles[index];
55 particle->particleType = particleType;
56 particle->spawnTime = gameTime.time;
57 particle->lifetime = lifetime;
58 particle->position = position;
59 particle->velocity = velocity;
60 particle->scale = scale;
61 }
62
63 void ParticleUpdate()
64 {
65 for (int i = 0; i < particleCount; i++)
66 {
67 Particle *particle = &particles[i];
68 if (particle->particleType == PARTICLE_TYPE_NONE)
69 {
70 continue;
71 }
72
73 float age = gameTime.time - particle->spawnTime;
74
75 if (particle->lifetime > age)
76 {
77 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
78 }
79 else {
80 particle->particleType = PARTICLE_TYPE_NONE;
81 }
82 }
83 }
84
85 void ParticleDraw()
86 {
87 for (int i = 0; i < particleCount; i++)
88 {
89 Particle particle = particles[i];
90 if (particle.particleType == PARTICLE_TYPE_NONE)
91 {
92 continue;
93 }
94
95 float age = gameTime.time - particle.spawnTime;
96 float transition = age / particle.lifetime;
97 switch (particle.particleType)
98 {
99 case PARTICLE_TYPE_EXPLOSION:
100 DrawExplosionParticle(&particle, transition);
101 break;
102 default:
103 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104 break;
105 }
106 }
107 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif

In order to achieve this smoothing, we need the right formula for the job and we need to understand the constraints here:
- The smoothing needs to be frame rate independent. It must behave the same, regardless how many frames are rendered per second.
- The target position can change each frame, so the smoothing needs to adapt to the new target position (no tweening between fixed start and end position).
- The velocity of the tower needs to appear smooth and natural, even when the target position changes rapidly.
- We want to quickly reach the target position, but not overshoot it in a way that it looks like the tower is slowing down before it reaches the target position.
The formula I used is based on the exponential decay function:
1 const float lambda = 15.0f;
2 const float deltaTime = fmin(GetFrameTime(), 0.3f);
3 float factor = 1.0f - expf(-lambda * deltaTime);
4
5 level->placementTransitionPosition =
6 Vector2Lerp(level->placementTransitionPosition,
7 (Vector2){mapX, mapY}, factor);
I always have to look up this formula, every time I need it. The important part is to know that such a formula exists and what properties it has.
The formula is based on the exponential decay function 1 - exp(-λ * Δt). This formula is described also as memoryless. Memoryless means, that the function doesn't care about the past, it only cares about the current state and the target state and how much time it will need to reach the target state. Since it does not care about the past, it doesn't matter how much time has passed since the last frame, the result will always be the same.
A plot is maybe the best way to understand what this means:
The plot above shows 3 curves of the exponential decay function, each sampled at a different rate. It is quite clear, that the sampling rate has no effect on the curve itself, it only affects the smoothness of the curve. The results at each calculation point are all the same.
Let's see how this function compares with a function that is often used for smoothing; let's say we would use this interpolation linear interpolation between the current position and the target position instead, using the delta time as the factor:
1 const float factor = GetFrameTime() * 10.0f;
2 level->placementTransitionPosition =
3 Vector2Lerp(level->placementTransitionPosition,
4 (Vector2){mapX, mapY}, factor);
If we would use this formula, the tower would move in a very similar way as the exponential decay function because it operates on the differences between the target position and the current position, which gets smaller each frame.
But let's throw it into the plot to see how it compares when sampled at different rates:
Ouch! There is a quite big difference between the different sampling rates.
Let's do some detailed calculations to compare the two formulas. To do that, we look at only a single axis movement from x=0 to x=2 and over 2 consecutive frames in two cases:
- Case A: Both frames have a delta time of 0.1 seconds (10 frames per second)
- Case B: The first frame has a delta time of 0.05 seconds and the second frame has a delta time of 0.15 seconds
Since the same amount of time has passed in both cases, we would want that the tower has moves the same distance in both cases. Let's calculate if this is the case:
Case A
Frame, time | Exponential decay | Interpolation |
---|---|---|
0, t=0 | 0.0 | 0.0 |
1, t=0.1 | x = 1.553740 | x = 1.500000 |
2, t=0.2 | x = 1.900426 | x = 1.875000 |
Case B
Frame, time | Exponential decay | Interpolation |
---|---|---|
0, t=0 | 0.0 | 0.0 |
1, t=0.05 | x = 1.055267 | x = 0.750000 |
2, t=0.2 | x = 1.900426 | x = 2.156250 |
Results
To compare the results, we only have to look at the final value of x in frame 2 for both cases: The exponential decay function result is 1.900426 in both cases, while the interpolation result is 1.875 in case A and 2.15625 in case B.
Additionally in case B, the result of the interpolation is simply wrong: It overshot.
Using a linear interpolation on a changing delta value is the wrong choice here. If the start and end value were fixed, any interpolation strategy would work, but since we have changing inputs, we can't do that.
Another strategy to avoid problems with the interpolation is to run the interpolation in a fixed time step. This is a common strategy in physics simulations where the math and interactions are too complex to be solved in a frame rate independent way (hint: don't use variable time steps in physics simulations, it can produce quite "funny" results). Fixed time steps have their own problems, but they are easier to use and understand. Just don't change the fixed time step rate mid-project, it tends to break things because for the same reason as variable time steps: The results depend on the time step resolution. Any previously fine-tuned values will be off and need to be adjusted.
Fixing the lerping
That all being said,it is possibleto fix the lerp function by changing the lerp factor, replacing dt with something sensible:
The trick here is to replace the factor dt in lerp(a, b, dt) with 1 - pow(k, dt), where k is the damping factor (which in this case is 0.00001). So what we use here is lerp(a, b, 1 - pow(k, dt)). This will make the lerp function frame rate independent and it will behave the same, regardless of the frame rate.
To intuitively understand this, think of it this way: pow(k, dt) will result in a value between 0 and 1 (for k >= 0 and k <= 1), regardless of the dt value (for positive values at least). If dt is 0, pow(k, 0) is 1, if dt is 1, pow(k, 1) is k.
A more detailed explanation can be found in this blogpost by Rory Driscoll.
Conclusion
This is probably the blog post with the least amount of code line changes, but I wanted to give the topic the attention it deserves. Framerate independence is a reoccurring topic in game development and it is important to handle it correctly.
The smoothed tower movement improves the overall feel of the building placement. Using the right formula for the job is crucial to get the desired results and making sure that the game feels the same, regardless of the frame rate.
There are a few framerate independent strategies to choose from, one just needs to know what to look out for.