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 build mode, the tower movement is now smoothed.
Smoothed tower moving demonstration.
Smoothed tower moving demonstration

In order to achieve this smoothing, we need the right formula for the job and we need to understand the constraints here:

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:

1 0 t simulated decay@0.1 simulated decay@0.05 simulated decay@0.01

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:

1 0 t simulated lerp@0.1 simulated lerp@0.05 simulated lerp@0.01

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:

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 decayInterpolation
0, t=00.00.0
1, t=0.1x = 1.553740x = 1.500000
2, t=0.2x = 1.900426x = 1.875000

Case B

Frame, time Exponential decayInterpolation
0, t=00.00.0
1, t=0.05x = 1.055267x = 0.750000
2, t=0.2x = 1.900426x = 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:

1 0 t pow(k, dt) lerp@0.1 pow(k, dt) lerp@0.05 pow(k, dt) lerp@0.01

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.

🍪