Simple tower defense tutorial, part 2

In the previous part, we set up the basic game loop and rendering for enemies, towers and bullets. In this part, we will add the following features:

But first, let's have a look at the current state of the game:

  • 💾
  1 #include "td-tut-2-main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 typedef struct GameTime
  7 {
  8   float time;
  9   float deltaTime;
 10 } GameTime;
 11 
 12 GameTime gameTime = {0};
 13 
 14 //# Enemies
 15 
 16 #define ENEMY_MAX_COUNT 400
 17 #define ENEMY_TYPE_NONE 0
 18 #define ENEMY_TYPE_MINION 1
 19 
 20 typedef struct EnemyId
 21 {
 22   uint16_t index;
 23   uint16_t generation;
 24 } EnemyId;
 25 
 26 typedef struct Enemy
 27 {
 28   int16_t currentX, currentY;
 29   int16_t nextX, nextY;
 30   uint16_t generation;
 31   float startMovingTime;
 32   uint8_t enemyType;
 33 } Enemy;
 34 
 35 Enemy enemies[ENEMY_MAX_COUNT];
 36 int enemyCount = 0;
 37 
 38 void EnemyInit()
 39 {
 40   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 41   {
 42     enemies[i] = (Enemy){0};
 43   }
 44   enemyCount = 0;
 45 }
 46 
 47 float EnemyGetCurrentSpeed(Enemy *enemy)
 48 {
 49   switch (enemy->enemyType)
 50   {
 51   case ENEMY_TYPE_MINION:
 52     return 1.0f;
 53   }
 54   return 1.0f;
 55 }
 56 
 57 void EnemyDraw()
 58 {
 59   for (int i = 0; i < enemyCount; i++)
 60   {
 61     Enemy enemy = enemies[i];
 62     float speed = EnemyGetCurrentSpeed(&enemy);
 63     float transition = (gameTime.time - enemy.startMovingTime) * speed;
 64 
 65     float x = enemy.currentX + (enemy.nextX - enemy.currentX) * transition;
 66     float y = enemy.currentY + (enemy.nextY - enemy.currentY) * transition;
 67     
 68     switch (enemy.enemyType)
 69     {
 70     case ENEMY_TYPE_MINION:
 71       DrawCube((Vector3){x, 0.2f, y}, 0.4f, 0.4f, 0.4f, GREEN);
 72       break;
 73     }
 74   }
 75 }
 76 
 77 void EnemyUpdate()
 78 {
 79   const int16_t castleX = 0;
 80   const int16_t castleY = 0;
 81 
 82   for (int i = 0; i < enemyCount; i++)
 83   {
 84     Enemy *enemy = &enemies[i];
 85     if (enemy->enemyType == ENEMY_TYPE_NONE)
 86     {
 87       continue;
 88     }
 89     float speed = EnemyGetCurrentSpeed(enemy);
 90     float transition = (gameTime.time - enemy->startMovingTime) * speed;
 91     if (transition >= 1.0f)
 92     {
 93       enemy->startMovingTime = gameTime.time;
 94       enemy->currentX = enemy->nextX;
 95       enemy->currentY = enemy->nextY;
 96       int16_t dx = castleX - enemy->currentX;
 97       int16_t dy = castleY - enemy->currentY;
 98       if (dx == 0 && dy == 0)
 99       {
100         // enemy reached the castle; remove it
101         enemy->enemyType = ENEMY_TYPE_NONE;
102         continue;
103       }
104       if (abs(dx) > abs(dy))
105       {
106         enemy->nextX = enemy->currentX + (dx > 0 ? 1 : -1);
107         enemy->nextY = enemy->currentY;
108       }
109       else
110       {
111         enemy->nextX = enemy->currentX;
112         enemy->nextY = enemy->currentY + (dy > 0 ? 1 : -1);
113       }
114     }
115   }
116 }
117 
118 EnemyId EnemyGetId(Enemy *enemy)
119 {
120   return (EnemyId){enemy - enemies, enemy->generation};
121 }
122 
123 Enemy *EnemyTryResolve(EnemyId enemyId)
124 {
125   if (enemyId.index >= ENEMY_MAX_COUNT)
126   {
127     return 0;
128   }
129   Enemy *enemy = &enemies[enemyId.index];
130   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
131   {
132     return 0;
133   }
134   return enemy;
135 }
136 
137 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
138 {
139   Enemy *spawn = 0;
140   for (int i = 0; i < enemyCount; i++)
141   {
142     Enemy *enemy = &enemies[i];
143     if (enemy->enemyType == ENEMY_TYPE_NONE)
144     {
145       spawn = enemy;
146       break;
147     }
148   }
149 
150   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
151   {
152     spawn = &enemies[enemyCount++];
153   }
154 
155   if (spawn)
156   {
157     spawn->currentX = currentX;
158     spawn->currentY = currentY;
159     spawn->nextX = currentX;
160     spawn->nextY = currentY;
161     spawn->enemyType = enemyType;
162     spawn->startMovingTime = 0;
163     spawn->generation++;
164   }
165 
166   return spawn;
167 }
168 
169 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
170 {
171   int16_t castleX = 0;
172   int16_t castleY = 0;
173   Enemy* closest = 0;
174   int16_t closestDistance = 0;
175   float range2 = range * range;
176   for (int i = 0; i < enemyCount; i++)
177   {
178     Enemy* enemy = &enemies[i];
179     if (enemy->enemyType == ENEMY_TYPE_NONE)
180     {
181       continue;
182     }
183     int16_t dx = castleX - enemy->currentX;
184     int16_t dy = castleY - enemy->currentY;
185     int16_t distance = abs(dx) + abs(dy);
186     if (!closest || distance < closestDistance)
187     {
188       float tdx = towerX - enemy->currentX;
189       float tdy = towerY - enemy->currentY;
190       float tdistance2 = tdx * tdx + tdy * tdy;
191       if (tdistance2 <= range2)
192       {
193         closest = enemy;
194         closestDistance = distance;
195       }
196     }
197   }
198   return closest;
199 }
200 
201 //# Projectiles
202 #define PROJECTILE_MAX_COUNT 1200
203 #define PROJECTILE_TYPE_NONE 0
204 #define PROJECTILE_TYPE_BULLET 1
205 
206 typedef struct Projectile
207 {
208   uint8_t projectileType;
209   float shootTime;
210   float arrivalTime;
211   float damage;
212   Vector2 position;
213   Vector2 target;
214   Vector2 directionNormal;
215   EnemyId targetEnemy;
216 } Projectile;
217 
218 Projectile projectiles[PROJECTILE_MAX_COUNT];
219 int projectileCount = 0;
220 
221 void ProjectileInit()
222 {
223   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
224   {
225     projectiles[i] = (Projectile){0};
226   }
227 }
228 
229 void ProjectileDraw()
230 {
231   for (int i = 0; i < projectileCount; i++)
232   {
233     Projectile projectile = projectiles[i];
234     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
235     {
236       continue;
237     }
238     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
239     if (transition >= 1.0f)
240     {
241       continue;
242     }
243     Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
244     float x = position.x;
245     float y = position.y;
246     float dx = projectile.directionNormal.x;
247     float dy = projectile.directionNormal.y;
248     for (float d = 1.0f; d > 0.0f; d -= 0.25f)
249     {
250       x -= dx * 0.1f;
251       y -= dy * 0.1f;
252       float size = 0.1f * d;
253       DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
254     }
255   }
256 }
257 
258 void ProjectileUpdate()
259 {
260   for (int i = 0; i < projectileCount; i++)
261   {
262     Projectile *projectile = &projectiles[i];
263     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
264     {
265       continue;
266     }
267     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
268     if (transition >= 1.0f)
269     {
270       projectile->projectileType = PROJECTILE_TYPE_NONE;
271       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
272       if (enemy)
273       {
274         enemy->enemyType = ENEMY_TYPE_NONE;
275       }
276       continue;
277     }
278   }
279 }
280 
281 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
282 {
283   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
284   {
285     Projectile *projectile = &projectiles[i];
286     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
287     {
288       projectile->projectileType = projectileType;
289       projectile->shootTime = gameTime.time;
290       projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
291       projectile->damage = damage;
292       projectile->position = position;
293       projectile->target = target;
294       projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
295       projectile->targetEnemy = EnemyGetId(enemy);
296       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
297       return projectile;
298     }
299   }
300   return 0;
301 }
302 
303 //# Towers
304 
305 #define TOWER_MAX_COUNT 400
306 #define TOWER_TYPE_NONE 0
307 #define TOWER_TYPE_BASE 1
308 #define TOWER_TYPE_GUN 2
309 
310 typedef struct Tower
311 {
312   int16_t x, y;
313   uint8_t towerType;
314   float cooldown;
315 } Tower;
316 
317 Tower towers[TOWER_MAX_COUNT];
318 int towerCount = 0;
319 
320 void TowerInit()
321 {
322   for (int i = 0; i < TOWER_MAX_COUNT; i++)
323   {
324     towers[i] = (Tower){0};
325   }
326   towerCount = 0;
327 }
328 
329 Tower *TowerGetAt(int16_t x, int16_t y)
330 {
331   for (int i = 0; i < towerCount; i++)
332   {
333     if (towers[i].x == x && towers[i].y == y)
334     {
335       return &towers[i];
336     }
337   }
338   return 0;
339 }
340 
341 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
342 {
343   if (towerCount >= TOWER_MAX_COUNT)
344   {
345     return 0;
346   }
347 
348   Tower *tower = TowerGetAt(x, y);
349   if (tower)
350   {
351     return 0;
352   }
353 
354   tower = &towers[towerCount++];
355   tower->x = x;
356   tower->y = y;
357   tower->towerType = towerType;
358   return tower;
359 }
360 
361 void TowerDraw()
362 {
363   for (int i = 0; i < towerCount; i++)
364   {
365     Tower tower = towers[i];
366     DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
367     switch (tower.towerType)
368     {
369     case TOWER_TYPE_BASE:
370       DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
371       break;
372     case TOWER_TYPE_GUN:
373       DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
374       break;
375     }
376   }
377 }
378 
379 void TowerGunUpdate(Tower *tower)
380 {
381   if (tower->cooldown <= 0)
382   {
383     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
384     if (enemy)
385     {
386       tower->cooldown = 0.5f;
387       // shoot the enemy
388       ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, (Vector2){tower->x, tower->y}, (Vector2){enemy->currentX, enemy->currentY}, 5.0f, 1.0f);
389     }
390   }
391   else
392   {
393     tower->cooldown -= gameTime.deltaTime;
394   }
395 }
396 
397 void TowerUpdate()
398 {
399   for (int i = 0; i < towerCount; i++)
400   {
401     Tower *tower = &towers[i];
402     switch (tower->towerType)
403     {
404     case TOWER_TYPE_GUN:
405       TowerGunUpdate(tower);
406       break;
407     }
408   }
409 }
410 
411 //# Game
412 
413 float nextSpawnTime = 0.0f;
414 
415 void InitGame()
416 {
417   TowerInit();
418   EnemyInit();
419   ProjectileInit();
420 
421   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
422   TowerTryAdd(TOWER_TYPE_GUN, 2, 0);
423   TowerTryAdd(TOWER_TYPE_GUN, -2, 0);
424   EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4);
425 }
426 
427 void GameUpdate()
428 {
429   float dt = GetFrameTime();
430   // cap maximum delta time to 0.1 seconds to prevent large time steps
431   if (dt > 0.1f) dt = 0.1f;
432   gameTime.time += dt;
433   gameTime.deltaTime = dt;
434   EnemyUpdate();
435   TowerUpdate();
436   ProjectileUpdate();
437 
438   // spawn a new enemy every second
439   if (gameTime.time >= nextSpawnTime)
440   {
441     nextSpawnTime = gameTime.time + 1.0f;
442     // add a new enemy at the boundary of the map
443     int randValue = GetRandomValue(-5, 5);
444     int randSide = GetRandomValue(0, 3);
445     int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue;
446     int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue;
447     EnemyTryAdd(ENEMY_TYPE_MINION, x, y);
448   }
449 }
450 
451 int main(void)
452 {
453   int screenWidth, screenHeight;
454   GetPreferredSize(&screenWidth, &screenHeight);
455   InitWindow(screenWidth, screenHeight, "Tower defense");
456   SetTargetFPS(30);
457 
458   Camera3D camera = {0};
459   camera.position = (Vector3){0.0f, 10.0f, 5.0f};
460   camera.target = (Vector3){0.0f, 0.0f, 0.0f};
461   camera.up = (Vector3){0.0f, 0.0f, -1.0f};
462   camera.fovy = 45.0f;
463   camera.projection = CAMERA_PERSPECTIVE;
464 
465   InitGame();
466 
467   while (!WindowShouldClose())
468   {
469     if (IsPaused()) {
470       // canvas is not visible in browser - do nothing
471       continue;
472     }
473     BeginDrawing();
474     ClearBackground(DARKBLUE);
475 
476     BeginMode3D(camera);
477     DrawGrid(10, 1.0f);
478     TowerDraw();
479     EnemyDraw();
480     ProjectileDraw();
481     GameUpdate();
482     EndMode3D();
483 
484     DrawText("Tower defense tutorial", 5, 5, 20, WHITE);
485     EndDrawing();
486   }
487 
488   CloseWindow();
489 
490   return 0;
491 }
  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 #endif
  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

Let's start with the health and damage system by adding enemy classes that specify their basic properties such as maximum health and speed:

  1 typedef struct EnemyClassConfig
  2 {
  3   float speed;
  4   float health;
  5 } EnemyClassConfig;
  6 
  7 EnemyClassConfig enemyClassConfigs[] = {
  8   [ENEMY_TYPE_MINION] = {.health = 3.0f, .speed = 1.0f},
  9 };

To the enemy struct, we add the sustained damage so far:

  1 typedef struct Enemy
  2 {
  3   int16_t currentX, currentY;
  4   int16_t nextX, nextY;
  5   uint16_t generation;
  6   float startMovingTime;
  7   float damage;
  8   uint8_t enemyType;
  9 } Enemy;

We could also use health and set it to the maximum health when spawning an enemy, but this is pretty much interchangeable with tracking the damage:

If we store the health, it's easy to detect if a unit has died when it sustained damage, because if it drops to zero, the unit is dead. But if we later want to display healthbars only on damaged units, we would need to calculate the damage sustained so far by subtracting the current health from the maximum.

On the other hand, if we store the damage, we can easily determine if we want to draw a healthbar. Determining if a unit is dead requires the comparison of the damage to the maximum health.

But it really doesn't matter for now - we only need some way to track the health.

There are now two functions to handle getting the speed and health of an enemy:

  1 float EnemyGetCurrentSpeed(Enemy *enemy)
  2 {
  3   return enemyClassConfigs[enemy->enemyType].speed;
  4 }
  5 
  6 float EnemyGetMaxHealth(Enemy *enemy)
  7 {
  8   return enemyClassConfigs[enemy->enemyType].health;
  9 }

Using a getter function here might be useful later in case we want to add buffs and debuffs, in which case we can modify the health and speed temporarily.

Since we have now a bit more health for the enemies, let's ramp up the shooting frequency and see how it looks:

  • 💾
  1 #include "td-tut-2-main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 typedef struct GameTime
  7 {
  8   float time;
  9   float deltaTime;
 10 } GameTime;
 11 
 12 GameTime gameTime = {0};
 13 
 14 //# Enemies
 15 
 16 #define ENEMY_MAX_COUNT 400
 17 #define ENEMY_TYPE_NONE 0
 18 #define ENEMY_TYPE_MINION 1
 19 
 20 typedef struct EnemyId
 21 {
 22   uint16_t index;
 23   uint16_t generation;
 24 } EnemyId;
 25 
26 typedef struct EnemyClassConfig 27 { 28 float speed; 29 float health; 30 } EnemyClassConfig; 31
32 typedef struct Enemy 33 { 34 int16_t currentX, currentY; 35 int16_t nextX, nextY; 36 uint16_t generation;
37 float startMovingTime; 38 float damage;
39 uint8_t enemyType; 40 } Enemy; 41 42 Enemy enemies[ENEMY_MAX_COUNT];
43 int enemyCount = 0; 44 45 EnemyClassConfig enemyClassConfigs[] = { 46 [ENEMY_TYPE_MINION] = {.health = 3.0f, .speed = 1.0f}, 47 };
48 49 void EnemyInit() 50 { 51 for (int i = 0; i < ENEMY_MAX_COUNT; i++) 52 { 53 enemies[i] = (Enemy){0}; 54 } 55 enemyCount = 0; 56 } 57 58 float EnemyGetCurrentSpeed(Enemy *enemy) 59 {
60 return enemyClassConfigs[enemy->enemyType].speed; 61 } 62 63 float EnemyGetMaxHealth(Enemy *enemy) 64 { 65 return enemyClassConfigs[enemy->enemyType].health;
66 } 67 68 void EnemyDraw() 69 { 70 for (int i = 0; i < enemyCount; i++) 71 { 72 Enemy enemy = enemies[i]; 73 float speed = EnemyGetCurrentSpeed(&enemy); 74 float transition = (gameTime.time - enemy.startMovingTime) * speed; 75 76 float x = enemy.currentX + (enemy.nextX - enemy.currentX) * transition; 77 float y = enemy.currentY + (enemy.nextY - enemy.currentY) * transition; 78 79 switch (enemy.enemyType) 80 { 81 case ENEMY_TYPE_MINION: 82 DrawCube((Vector3){x, 0.2f, y}, 0.4f, 0.4f, 0.4f, GREEN); 83 break; 84 } 85 } 86 } 87 88 void EnemyUpdate() 89 { 90 const int16_t castleX = 0; 91 const int16_t castleY = 0; 92 93 for (int i = 0; i < enemyCount; i++) 94 { 95 Enemy *enemy = &enemies[i]; 96 if (enemy->enemyType == ENEMY_TYPE_NONE) 97 { 98 continue; 99 } 100 float speed = EnemyGetCurrentSpeed(enemy); 101 float transition = (gameTime.time - enemy->startMovingTime) * speed; 102 if (transition >= 1.0f) 103 { 104 enemy->startMovingTime = gameTime.time; 105 enemy->currentX = enemy->nextX; 106 enemy->currentY = enemy->nextY; 107 int16_t dx = castleX - enemy->currentX; 108 int16_t dy = castleY - enemy->currentY; 109 if (dx == 0 && dy == 0) 110 { 111 // enemy reached the castle; remove it 112 enemy->enemyType = ENEMY_TYPE_NONE; 113 continue; 114 } 115 if (abs(dx) > abs(dy)) 116 { 117 enemy->nextX = enemy->currentX + (dx > 0 ? 1 : -1); 118 enemy->nextY = enemy->currentY; 119 } 120 else 121 { 122 enemy->nextX = enemy->currentX; 123 enemy->nextY = enemy->currentY + (dy > 0 ? 1 : -1); 124 } 125 } 126 } 127 } 128 129 EnemyId EnemyGetId(Enemy *enemy) 130 { 131 return (EnemyId){enemy - enemies, enemy->generation}; 132 } 133 134 Enemy *EnemyTryResolve(EnemyId enemyId) 135 { 136 if (enemyId.index >= ENEMY_MAX_COUNT) 137 { 138 return 0; 139 } 140 Enemy *enemy = &enemies[enemyId.index]; 141 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE) 142 { 143 return 0; 144 } 145 return enemy; 146 } 147 148 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 149 { 150 Enemy *spawn = 0; 151 for (int i = 0; i < enemyCount; i++) 152 { 153 Enemy *enemy = &enemies[i]; 154 if (enemy->enemyType == ENEMY_TYPE_NONE) 155 { 156 spawn = enemy; 157 break; 158 } 159 } 160 161 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 162 { 163 spawn = &enemies[enemyCount++]; 164 } 165 166 if (spawn) 167 { 168 spawn->currentX = currentX; 169 spawn->currentY = currentY; 170 spawn->nextX = currentX; 171 spawn->nextY = currentY; 172 spawn->enemyType = enemyType;
173 spawn->startMovingTime = 0; 174 spawn->damage = 0.0f;
175 spawn->generation++; 176 } 177
178 return spawn; 179 } 180 181 int EnemyAddDamage(Enemy *enemy, float damage) 182 { 183 enemy->damage += damage; 184 if (enemy->damage >= EnemyGetMaxHealth(enemy)) 185 { 186 enemy->enemyType = ENEMY_TYPE_NONE; 187 return 1; 188 } 189 190 return 0;
191 } 192 193 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 194 { 195 int16_t castleX = 0; 196 int16_t castleY = 0; 197 Enemy* closest = 0; 198 int16_t closestDistance = 0; 199 float range2 = range * range; 200 for (int i = 0; i < enemyCount; i++) 201 { 202 Enemy* enemy = &enemies[i]; 203 if (enemy->enemyType == ENEMY_TYPE_NONE) 204 { 205 continue; 206 } 207 int16_t dx = castleX - enemy->currentX; 208 int16_t dy = castleY - enemy->currentY; 209 int16_t distance = abs(dx) + abs(dy); 210 if (!closest || distance < closestDistance) 211 { 212 float tdx = towerX - enemy->currentX; 213 float tdy = towerY - enemy->currentY; 214 float tdistance2 = tdx * tdx + tdy * tdy; 215 if (tdistance2 <= range2) 216 { 217 closest = enemy; 218 closestDistance = distance; 219 } 220 } 221 } 222 return closest; 223 } 224 225 //# Projectiles 226 #define PROJECTILE_MAX_COUNT 1200 227 #define PROJECTILE_TYPE_NONE 0 228 #define PROJECTILE_TYPE_BULLET 1 229 230 typedef struct Projectile 231 { 232 uint8_t projectileType; 233 float shootTime; 234 float arrivalTime; 235 float damage; 236 Vector2 position; 237 Vector2 target; 238 Vector2 directionNormal; 239 EnemyId targetEnemy; 240 } Projectile; 241 242 Projectile projectiles[PROJECTILE_MAX_COUNT]; 243 int projectileCount = 0; 244 245 void ProjectileInit() 246 { 247 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 248 { 249 projectiles[i] = (Projectile){0}; 250 } 251 } 252 253 void ProjectileDraw() 254 { 255 for (int i = 0; i < projectileCount; i++) 256 { 257 Projectile projectile = projectiles[i]; 258 if (projectile.projectileType == PROJECTILE_TYPE_NONE) 259 { 260 continue; 261 } 262 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime); 263 if (transition >= 1.0f) 264 { 265 continue; 266 } 267 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition); 268 float x = position.x; 269 float y = position.y; 270 float dx = projectile.directionNormal.x; 271 float dy = projectile.directionNormal.y; 272 for (float d = 1.0f; d > 0.0f; d -= 0.25f) 273 { 274 x -= dx * 0.1f; 275 y -= dy * 0.1f; 276 float size = 0.1f * d; 277 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED); 278 } 279 } 280 } 281 282 void ProjectileUpdate() 283 { 284 for (int i = 0; i < projectileCount; i++) 285 { 286 Projectile *projectile = &projectiles[i]; 287 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 288 { 289 continue; 290 } 291 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime); 292 if (transition >= 1.0f) 293 { 294 projectile->projectileType = PROJECTILE_TYPE_NONE; 295 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy); 296 if (enemy) 297 {
298 EnemyAddDamage(enemy, projectile->damage);
299 } 300 continue; 301 } 302 } 303 } 304 305 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage) 306 { 307 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 308 { 309 Projectile *projectile = &projectiles[i]; 310 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 311 { 312 projectile->projectileType = projectileType; 313 projectile->shootTime = gameTime.time; 314 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed; 315 projectile->damage = damage; 316 projectile->position = position; 317 projectile->target = target; 318 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position)); 319 projectile->targetEnemy = EnemyGetId(enemy); 320 projectileCount = projectileCount <= i ? i + 1 : projectileCount; 321 return projectile; 322 } 323 } 324 return 0; 325 } 326 327 //# Towers 328 329 #define TOWER_MAX_COUNT 400 330 #define TOWER_TYPE_NONE 0 331 #define TOWER_TYPE_BASE 1 332 #define TOWER_TYPE_GUN 2 333 334 typedef struct Tower 335 { 336 int16_t x, y; 337 uint8_t towerType; 338 float cooldown; 339 } Tower; 340 341 Tower towers[TOWER_MAX_COUNT]; 342 int towerCount = 0; 343 344 void TowerInit() 345 { 346 for (int i = 0; i < TOWER_MAX_COUNT; i++) 347 { 348 towers[i] = (Tower){0}; 349 } 350 towerCount = 0; 351 } 352 353 Tower *TowerGetAt(int16_t x, int16_t y) 354 { 355 for (int i = 0; i < towerCount; i++) 356 { 357 if (towers[i].x == x && towers[i].y == y) 358 { 359 return &towers[i]; 360 } 361 } 362 return 0; 363 } 364 365 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 366 { 367 if (towerCount >= TOWER_MAX_COUNT) 368 { 369 return 0; 370 } 371 372 Tower *tower = TowerGetAt(x, y); 373 if (tower) 374 { 375 return 0; 376 } 377 378 tower = &towers[towerCount++]; 379 tower->x = x; 380 tower->y = y; 381 tower->towerType = towerType; 382 return tower; 383 } 384 385 void TowerDraw() 386 { 387 for (int i = 0; i < towerCount; i++) 388 { 389 Tower tower = towers[i]; 390 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 391 switch (tower.towerType) 392 { 393 case TOWER_TYPE_BASE: 394 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 395 break; 396 case TOWER_TYPE_GUN: 397 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 398 break; 399 } 400 } 401 } 402 403 void TowerGunUpdate(Tower *tower) 404 { 405 if (tower->cooldown <= 0) 406 { 407 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 408 if (enemy) 409 {
410 tower->cooldown = 0.25f;
411 // shoot the enemy 412 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, (Vector2){tower->x, tower->y}, (Vector2){enemy->currentX, enemy->currentY}, 5.0f, 1.0f); 413 } 414 } 415 else 416 { 417 tower->cooldown -= gameTime.deltaTime; 418 } 419 } 420 421 void TowerUpdate() 422 { 423 for (int i = 0; i < towerCount; i++) 424 { 425 Tower *tower = &towers[i]; 426 switch (tower->towerType) 427 { 428 case TOWER_TYPE_GUN: 429 TowerGunUpdate(tower); 430 break; 431 } 432 } 433 } 434 435 //# Game 436 437 float nextSpawnTime = 0.0f; 438 439 void InitGame() 440 { 441 TowerInit(); 442 EnemyInit(); 443 ProjectileInit(); 444 445 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 446 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 447 TowerTryAdd(TOWER_TYPE_GUN, -2, 0); 448 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4); 449 } 450 451 void GameUpdate() 452 { 453 float dt = GetFrameTime(); 454 // cap maximum delta time to 0.1 seconds to prevent large time steps 455 if (dt > 0.1f) dt = 0.1f; 456 gameTime.time += dt; 457 gameTime.deltaTime = dt; 458 EnemyUpdate(); 459 TowerUpdate(); 460 ProjectileUpdate(); 461 462 // spawn a new enemy every second 463 if (gameTime.time >= nextSpawnTime) 464 { 465 nextSpawnTime = gameTime.time + 1.0f; 466 // add a new enemy at the boundary of the map 467 int randValue = GetRandomValue(-5, 5); 468 int randSide = GetRandomValue(0, 3); 469 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue; 470 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue; 471 EnemyTryAdd(ENEMY_TYPE_MINION, x, y); 472 } 473 } 474 475 int main(void) 476 { 477 int screenWidth, screenHeight; 478 GetPreferredSize(&screenWidth, &screenHeight); 479 InitWindow(screenWidth, screenHeight, "Tower defense"); 480 SetTargetFPS(30); 481 482 Camera3D camera = {0}; 483 camera.position = (Vector3){0.0f, 10.0f, 5.0f}; 484 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 485 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 486 camera.fovy = 45.0f; 487 camera.projection = CAMERA_PERSPECTIVE; 488 489 InitGame(); 490 491 while (!WindowShouldClose()) 492 { 493 if (IsPaused()) { 494 // canvas is not visible in browser - do nothing 495 continue; 496 } 497 BeginDrawing(); 498 ClearBackground(DARKBLUE); 499 500 BeginMode3D(camera); 501 DrawGrid(10, 1.0f); 502 TowerDraw(); 503 EnemyDraw(); 504 ProjectileDraw(); 505 GameUpdate(); 506 EndMode3D(); 507 508 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 509 EndDrawing(); 510 } 511 512 CloseWindow(); 513 514 return 0; 515 }
  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 #endif
  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

With the higher health and higher shooting frequency, we can see now that the projectiles don't really hit the enemies. They currently aim at the integer position of the enemy, but since we interpolate the enemy position, the projectile will almost always not really hit the enemy.

What we want is a function to calculate the position of the enemy based on the shooting distannce and the speed of the projectile. For straight moving objects, this can be done analytically, but since our enemies can change direction, this isn't so simple. So instead, we can use a simple iterative approach to find the position of the enemy at the time the projectile hits the enemy:

Here's the code that is doing this:

  1 float bulletSpeed = 1.0f;
  2 // calculate the current position of our enemy and assume it's the future position
  3 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime);
  4 Vector2 towerPosition = {tower->x, tower->y};
  5 // how long does the projectile need to hit that position?
  6 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
  7 for (int i = 0; i < 8; i++) {
  8   // We know: The enemy will move forward until the arrival of the bullet. We will use
  9   // this information to calculate the future position of the enemy at the time the bullet.
 10   futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta);
 11 
 12   // However, our travel time will now be different. Let's recalculate the time
 13   float distance = Vector2Distance(towerPosition, futurePosition);
 14   float eta2 = distance / bulletSpeed;
 15 
 16   // Compare the two times; if they are close, we won't get much better and we can stop
 17   if (fabs(eta - eta2) < 0.01f) {
 18     break;
 19   }
 20   // Otherwise, we take the average of the two times and repeat - the 
 21   // solution should be between the two times - unless the enemy is faster than 
 22   // the bullet, then it will only get worse, but let's assume this'll never happen
 23   // in our game.
 24   eta = (eta2 + eta) * 0.5f;
 25 } 

After logging the results, it seems it tends to stop the loop after 2-4 iterations - which is good enough for our purposes.

We can test now the quality of the predictions by reducing the bullet speed and ramping up the damage the towers do:

  • 💾
  1 #include "td-tut-2-main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 typedef struct GameTime
  7 {
  8   float time;
  9   float deltaTime;
 10 } GameTime;
 11 
 12 GameTime gameTime = {0};
 13 
 14 //# Enemies
 15 
 16 #define ENEMY_MAX_COUNT 400
 17 #define ENEMY_TYPE_NONE 0
 18 #define ENEMY_TYPE_MINION 1
 19 
 20 typedef struct EnemyId
 21 {
 22   uint16_t index;
 23   uint16_t generation;
 24 } EnemyId;
 25 
 26 typedef struct EnemyClassConfig
 27 {
 28   float speed;
 29   float health;
 30 } EnemyClassConfig;
 31 
 32 typedef struct Enemy
 33 {
 34   int16_t currentX, currentY;
 35   int16_t nextX, nextY;
 36   uint16_t generation;
 37   float startMovingTime;
 38   float damage;
 39   uint8_t enemyType;
 40 } Enemy;
 41 
 42 Enemy enemies[ENEMY_MAX_COUNT];
 43 int enemyCount = 0;
 44 
 45 EnemyClassConfig enemyClassConfigs[] = {
 46     [ENEMY_TYPE_MINION] = {.health = 3.0f, .speed = 1.0f},
 47 };
 48 
 49 void EnemyInit()
 50 {
 51   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 52   {
 53     enemies[i] = (Enemy){0};
 54   }
 55   enemyCount = 0;
 56 }
 57 
 58 float EnemyGetCurrentSpeed(Enemy *enemy)
 59 {
 60   return enemyClassConfigs[enemy->enemyType].speed;
 61 }
 62 
 63 float EnemyGetMaxHealth(Enemy *enemy)
 64 {
 65   return enemyClassConfigs[enemy->enemyType].health;
 66 }
 67 
68 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
69 {
70 int16_t castleX = 0; 71 int16_t castleY = 0; 72 int16_t dx = castleX - currentX; 73 int16_t dy = castleY - currentY; 74 if (dx == 0 && dy == 0)
75 {
76 *nextX = currentX; 77 *nextY = currentY; 78 return 1; 79 } 80 if (abs(dx) > abs(dy)) 81 { 82 *nextX = currentX + (dx > 0 ? 1 : -1); 83 *nextY = currentY; 84 } 85 else 86 { 87 *nextX = currentX; 88 *nextY = currentY + (dy > 0 ? 1 : -1); 89 } 90 return 0; 91 }
92
93 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT) 94 { 95 float speed = deltaT * EnemyGetCurrentSpeed(enemy); 96 int16_t currentX = enemy->currentX; 97 int16_t currentY = enemy->currentY; 98 int16_t nextX = enemy->nextX; 99 int16_t nextY = enemy->nextY; 100 while (speed > 1.0f) 101 { 102 speed -= 1.0f; 103 currentX = nextX; 104 currentY = nextY; 105 if (EnemyGetNextPosition(currentX, currentY, &nextX, &nextY)) 106 { 107 return (Vector2){currentX, currentY}; 108 } 109 } 110 return Vector2Lerp((Vector2){currentX, currentY}, (Vector2){nextX, nextY}, speed); 111 } 112 113 void EnemyDraw() 114 { 115 for (int i = 0; i < enemyCount; i++) 116 { 117 Enemy enemy = enemies[i]; 118 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime);
119 120 switch (enemy.enemyType) 121 { 122 case ENEMY_TYPE_MINION:
123 DrawCube((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
124 break; 125 } 126 }
127 } 128 129 void EnemyUpdate() 130 {
131 for (int i = 0; i < enemyCount; i++) 132 { 133 Enemy *enemy = &enemies[i];
134 if (enemy->enemyType == ENEMY_TYPE_NONE) 135 { 136 continue; 137 } 138 float speed = EnemyGetCurrentSpeed(enemy); 139 float transition = (gameTime.time - enemy->startMovingTime) * speed; 140 if (transition >= 1.0f) 141 { 142 enemy->startMovingTime = gameTime.time; 143 enemy->currentX = enemy->nextX; 144 enemy->currentY = enemy->nextY;
145 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY)) 146 { 147 // enemy reached the castle; remove it 148 enemy->enemyType = ENEMY_TYPE_NONE; 149 continue; 150 } 151 } 152 } 153 } 154 155 EnemyId EnemyGetId(Enemy *enemy) 156 { 157 return (EnemyId){enemy - enemies, enemy->generation}; 158 } 159 160 Enemy *EnemyTryResolve(EnemyId enemyId) 161 { 162 if (enemyId.index >= ENEMY_MAX_COUNT) 163 { 164 return 0; 165 } 166 Enemy *enemy = &enemies[enemyId.index]; 167 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE) 168 { 169 return 0; 170 } 171 return enemy; 172 } 173 174 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 175 { 176 Enemy *spawn = 0; 177 for (int i = 0; i < enemyCount; i++) 178 { 179 Enemy *enemy = &enemies[i]; 180 if (enemy->enemyType == ENEMY_TYPE_NONE) 181 { 182 spawn = enemy; 183 break;
184 }
185 } 186 187 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 188 { 189 spawn = &enemies[enemyCount++]; 190 } 191 192 if (spawn) 193 { 194 spawn->currentX = currentX; 195 spawn->currentY = currentY; 196 spawn->nextX = currentX; 197 spawn->nextY = currentY; 198 spawn->enemyType = enemyType; 199 spawn->startMovingTime = gameTime.time; 200 spawn->damage = 0.0f; 201 spawn->generation++; 202 } 203 204 return spawn; 205 } 206 207 int EnemyAddDamage(Enemy *enemy, float damage) 208 { 209 enemy->damage += damage; 210 if (enemy->damage >= EnemyGetMaxHealth(enemy)) 211 { 212 enemy->enemyType = ENEMY_TYPE_NONE; 213 return 1; 214 } 215 216 return 0; 217 } 218 219 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 220 { 221 int16_t castleX = 0; 222 int16_t castleY = 0; 223 Enemy* closest = 0; 224 int16_t closestDistance = 0; 225 float range2 = range * range; 226 for (int i = 0; i < enemyCount; i++) 227 { 228 Enemy* enemy = &enemies[i]; 229 if (enemy->enemyType == ENEMY_TYPE_NONE) 230 { 231 continue; 232 } 233 int16_t dx = castleX - enemy->currentX; 234 int16_t dy = castleY - enemy->currentY; 235 int16_t distance = abs(dx) + abs(dy); 236 if (!closest || distance < closestDistance) 237 { 238 float tdx = towerX - enemy->currentX; 239 float tdy = towerY - enemy->currentY; 240 float tdistance2 = tdx * tdx + tdy * tdy; 241 if (tdistance2 <= range2) 242 { 243 closest = enemy; 244 closestDistance = distance; 245 } 246 } 247 } 248 return closest; 249 } 250 251 //# Projectiles 252 #define PROJECTILE_MAX_COUNT 1200 253 #define PROJECTILE_TYPE_NONE 0 254 #define PROJECTILE_TYPE_BULLET 1 255 256 typedef struct Projectile 257 { 258 uint8_t projectileType; 259 float shootTime; 260 float arrivalTime; 261 float damage; 262 Vector2 position; 263 Vector2 target; 264 Vector2 directionNormal; 265 EnemyId targetEnemy; 266 } Projectile; 267 268 Projectile projectiles[PROJECTILE_MAX_COUNT]; 269 int projectileCount = 0; 270 271 void ProjectileInit() 272 { 273 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 274 { 275 projectiles[i] = (Projectile){0}; 276 } 277 } 278 279 void ProjectileDraw() 280 { 281 for (int i = 0; i < projectileCount; i++) 282 { 283 Projectile projectile = projectiles[i]; 284 if (projectile.projectileType == PROJECTILE_TYPE_NONE) 285 { 286 continue; 287 } 288 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime); 289 if (transition >= 1.0f) 290 { 291 continue; 292 } 293 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition); 294 float x = position.x; 295 float y = position.y; 296 float dx = projectile.directionNormal.x; 297 float dy = projectile.directionNormal.y; 298 for (float d = 1.0f; d > 0.0f; d -= 0.25f) 299 { 300 x -= dx * 0.1f; 301 y -= dy * 0.1f; 302 float size = 0.1f * d; 303 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED); 304 } 305 } 306 } 307 308 void ProjectileUpdate() 309 { 310 for (int i = 0; i < projectileCount; i++) 311 { 312 Projectile *projectile = &projectiles[i]; 313 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 314 { 315 continue; 316 } 317 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime); 318 if (transition >= 1.0f) 319 { 320 projectile->projectileType = PROJECTILE_TYPE_NONE; 321 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy); 322 if (enemy) 323 { 324 EnemyAddDamage(enemy, projectile->damage); 325 } 326 continue; 327 } 328 } 329 } 330 331 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage) 332 { 333 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 334 { 335 Projectile *projectile = &projectiles[i]; 336 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 337 { 338 projectile->projectileType = projectileType; 339 projectile->shootTime = gameTime.time; 340 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed; 341 projectile->damage = damage; 342 projectile->position = position; 343 projectile->target = target; 344 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position)); 345 projectile->targetEnemy = EnemyGetId(enemy); 346 projectileCount = projectileCount <= i ? i + 1 : projectileCount; 347 return projectile; 348 } 349 } 350 return 0; 351 } 352 353 //# Towers 354 355 #define TOWER_MAX_COUNT 400 356 #define TOWER_TYPE_NONE 0 357 #define TOWER_TYPE_BASE 1 358 #define TOWER_TYPE_GUN 2 359 360 typedef struct Tower 361 { 362 int16_t x, y; 363 uint8_t towerType; 364 float cooldown; 365 } Tower; 366 367 Tower towers[TOWER_MAX_COUNT]; 368 int towerCount = 0; 369 370 void TowerInit() 371 { 372 for (int i = 0; i < TOWER_MAX_COUNT; i++) 373 { 374 towers[i] = (Tower){0}; 375 } 376 towerCount = 0; 377 } 378 379 Tower *TowerGetAt(int16_t x, int16_t y) 380 { 381 for (int i = 0; i < towerCount; i++) 382 { 383 if (towers[i].x == x && towers[i].y == y) 384 { 385 return &towers[i]; 386 } 387 } 388 return 0; 389 } 390 391 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 392 { 393 if (towerCount >= TOWER_MAX_COUNT) 394 { 395 return 0; 396 } 397 398 Tower *tower = TowerGetAt(x, y); 399 if (tower) 400 { 401 return 0; 402 } 403 404 tower = &towers[towerCount++]; 405 tower->x = x; 406 tower->y = y; 407 tower->towerType = towerType; 408 return tower; 409 } 410 411 void TowerDraw() 412 { 413 for (int i = 0; i < towerCount; i++) 414 { 415 Tower tower = towers[i]; 416 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 417 switch (tower.towerType) 418 { 419 case TOWER_TYPE_BASE: 420 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 421 break;
422 case TOWER_TYPE_GUN: 423 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 424 break; 425 } 426 } 427 } 428 429 void TowerGunUpdate(Tower *tower) 430 { 431 if (tower->cooldown <= 0) 432 { 433 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 434 if (enemy) 435 { 436 tower->cooldown = 0.25f;
437 // shoot the enemy; determine future position of the enemy 438 float bulletSpeed = 1.0f; 439 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime); 440 Vector2 towerPosition = {tower->x, tower->y}; 441 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 442 for (int i = 0; i < 8; i++) { 443 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta); 444 float distance = Vector2Distance(towerPosition, futurePosition); 445 float eta2 = distance / bulletSpeed; 446 if (fabs(eta - eta2) < 0.01f) { 447 break; 448 } 449 eta = (eta2 + eta) * 0.5f; 450 } 451 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition, bulletSpeed, 3.0f); 452 } 453 } 454 else 455 { 456 tower->cooldown -= gameTime.deltaTime; 457 } 458 } 459 460 void TowerUpdate() 461 { 462 for (int i = 0; i < towerCount; i++) 463 { 464 Tower *tower = &towers[i]; 465 switch (tower->towerType) 466 { 467 case TOWER_TYPE_GUN: 468 TowerGunUpdate(tower); 469 break; 470 } 471 } 472 } 473 474 //# Game 475 476 float nextSpawnTime = 0.0f; 477 478 void InitGame() 479 { 480 TowerInit(); 481 EnemyInit(); 482 ProjectileInit(); 483 484 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 485 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 486 TowerTryAdd(TOWER_TYPE_GUN, -2, 0); 487 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4); 488 } 489 490 void GameUpdate() 491 { 492 float dt = GetFrameTime(); 493 // cap maximum delta time to 0.1 seconds to prevent large time steps 494 if (dt > 0.1f) dt = 0.1f; 495 gameTime.time += dt; 496 gameTime.deltaTime = dt; 497 EnemyUpdate(); 498 TowerUpdate(); 499 ProjectileUpdate(); 500 501 // spawn a new enemy every second 502 if (gameTime.time >= nextSpawnTime) 503 { 504 nextSpawnTime = gameTime.time + 1.0f; 505 // add a new enemy at the boundary of the map 506 int randValue = GetRandomValue(-5, 5); 507 int randSide = GetRandomValue(0, 3); 508 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue; 509 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue; 510 EnemyTryAdd(ENEMY_TYPE_MINION, x, y); 511 } 512 } 513 514 int main(void) 515 { 516 int screenWidth, screenHeight; 517 GetPreferredSize(&screenWidth, &screenHeight); 518 InitWindow(screenWidth, screenHeight, "Tower defense"); 519 SetTargetFPS(30); 520 521 Camera3D camera = {0}; 522 camera.position = (Vector3){0.0f, 10.0f, 5.0f}; 523 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 524 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 525 camera.fovy = 45.0f; 526 camera.projection = CAMERA_PERSPECTIVE; 527 528 InitGame(); 529 530 while (!WindowShouldClose()) 531 { 532 if (IsPaused()) { 533 // canvas is not visible in browser - do nothing 534 continue; 535 } 536 BeginDrawing(); 537 ClearBackground(DARKBLUE); 538 539 BeginMode3D(camera); 540 DrawGrid(10, 1.0f); 541 TowerDraw(); 542 EnemyDraw(); 543 ProjectileDraw(); 544 GameUpdate(); 545 EndMode3D(); 546 547 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 548 EndDrawing(); 549 } 550 551 CloseWindow(); 552 553 return 0; 554 }
  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 #endif
  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

Now that the bullets take so much time to arrive, we see another problem: Overdamage. The towers can shoot multiple times at the same enemy when they could have started targeting another enemy. We can add a 2nd damage factor to the enemy struct that indicates how much damage the enemy will sustain in the near future and the tower target selection can then take this into account:

  • 💾
  1 #include "td-tut-2-main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 typedef struct GameTime
  7 {
  8   float time;
  9   float deltaTime;
 10 } GameTime;
 11 
 12 GameTime gameTime = {0};
 13 
 14 //# Enemies
 15 
 16 #define ENEMY_MAX_COUNT 400
 17 #define ENEMY_TYPE_NONE 0
 18 #define ENEMY_TYPE_MINION 1
 19 
 20 typedef struct EnemyId
 21 {
 22   uint16_t index;
 23   uint16_t generation;
 24 } EnemyId;
 25 
 26 typedef struct EnemyClassConfig
 27 {
 28   float speed;
 29   float health;
 30 } EnemyClassConfig;
 31 
 32 typedef struct Enemy
 33 {
 34   int16_t currentX, currentY;
 35   int16_t nextX, nextY;
 36   uint16_t generation;
 37   float startMovingTime;
38 float damage, futureDamage;
39 uint8_t enemyType; 40 } Enemy; 41 42 Enemy enemies[ENEMY_MAX_COUNT]; 43 int enemyCount = 0; 44 45 EnemyClassConfig enemyClassConfigs[] = { 46 [ENEMY_TYPE_MINION] = {.health = 3.0f, .speed = 1.0f}, 47 }; 48 49 void EnemyInit() 50 { 51 for (int i = 0; i < ENEMY_MAX_COUNT; i++) 52 { 53 enemies[i] = (Enemy){0}; 54 } 55 enemyCount = 0; 56 } 57 58 float EnemyGetCurrentSpeed(Enemy *enemy) 59 { 60 return enemyClassConfigs[enemy->enemyType].speed; 61 } 62 63 float EnemyGetMaxHealth(Enemy *enemy) 64 { 65 return enemyClassConfigs[enemy->enemyType].health; 66 } 67 68 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY) 69 { 70 int16_t castleX = 0; 71 int16_t castleY = 0; 72 int16_t dx = castleX - currentX; 73 int16_t dy = castleY - currentY; 74 if (dx == 0 && dy == 0) 75 { 76 *nextX = currentX; 77 *nextY = currentY; 78 return 1; 79 } 80 if (abs(dx) > abs(dy)) 81 { 82 *nextX = currentX + (dx > 0 ? 1 : -1); 83 *nextY = currentY; 84 } 85 else 86 { 87 *nextX = currentX; 88 *nextY = currentY + (dy > 0 ? 1 : -1); 89 } 90 return 0; 91 } 92 93 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT) 94 { 95 float speed = deltaT * EnemyGetCurrentSpeed(enemy); 96 int16_t currentX = enemy->currentX; 97 int16_t currentY = enemy->currentY; 98 int16_t nextX = enemy->nextX; 99 int16_t nextY = enemy->nextY; 100 while (speed > 1.0f) 101 { 102 speed -= 1.0f; 103 currentX = nextX; 104 currentY = nextY; 105 if (EnemyGetNextPosition(currentX, currentY, &nextX, &nextY)) 106 { 107 return (Vector2){currentX, currentY}; 108 } 109 } 110 return Vector2Lerp((Vector2){currentX, currentY}, (Vector2){nextX, nextY}, speed); 111 } 112 113 void EnemyDraw() 114 { 115 for (int i = 0; i < enemyCount; i++) 116 { 117 Enemy enemy = enemies[i]; 118 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime); 119 120 switch (enemy.enemyType) 121 { 122 case ENEMY_TYPE_MINION: 123 DrawCube((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN); 124 break; 125 } 126 } 127 } 128 129 void EnemyUpdate() 130 { 131 for (int i = 0; i < enemyCount; i++) 132 { 133 Enemy *enemy = &enemies[i]; 134 if (enemy->enemyType == ENEMY_TYPE_NONE) 135 { 136 continue; 137 } 138 float speed = EnemyGetCurrentSpeed(enemy); 139 float transition = (gameTime.time - enemy->startMovingTime) * speed; 140 if (transition >= 1.0f) 141 { 142 enemy->startMovingTime = gameTime.time; 143 enemy->currentX = enemy->nextX; 144 enemy->currentY = enemy->nextY; 145 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY)) 146 { 147 // enemy reached the castle; remove it 148 enemy->enemyType = ENEMY_TYPE_NONE; 149 continue; 150 } 151 } 152 } 153 } 154 155 EnemyId EnemyGetId(Enemy *enemy) 156 { 157 return (EnemyId){enemy - enemies, enemy->generation}; 158 } 159 160 Enemy *EnemyTryResolve(EnemyId enemyId) 161 { 162 if (enemyId.index >= ENEMY_MAX_COUNT) 163 { 164 return 0; 165 } 166 Enemy *enemy = &enemies[enemyId.index]; 167 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE) 168 { 169 return 0; 170 } 171 return enemy; 172 } 173 174 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 175 { 176 Enemy *spawn = 0; 177 for (int i = 0; i < enemyCount; i++) 178 { 179 Enemy *enemy = &enemies[i]; 180 if (enemy->enemyType == ENEMY_TYPE_NONE) 181 { 182 spawn = enemy; 183 break; 184 } 185 } 186 187 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 188 { 189 spawn = &enemies[enemyCount++]; 190 } 191 192 if (spawn) 193 { 194 spawn->currentX = currentX; 195 spawn->currentY = currentY; 196 spawn->nextX = currentX; 197 spawn->nextY = currentY; 198 spawn->enemyType = enemyType; 199 spawn->startMovingTime = gameTime.time;
200 spawn->damage = 0.0f; 201 spawn->futureDamage = 0.0f;
202 spawn->generation++; 203 } 204 205 return spawn; 206 } 207 208 int EnemyAddDamage(Enemy *enemy, float damage) 209 { 210 enemy->damage += damage; 211 if (enemy->damage >= EnemyGetMaxHealth(enemy)) 212 { 213 enemy->enemyType = ENEMY_TYPE_NONE; 214 return 1; 215 } 216 217 return 0; 218 } 219 220 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 221 { 222 int16_t castleX = 0; 223 int16_t castleY = 0; 224 Enemy* closest = 0; 225 int16_t closestDistance = 0; 226 float range2 = range * range; 227 for (int i = 0; i < enemyCount; i++) 228 { 229 Enemy* enemy = &enemies[i];
230 if (enemy->enemyType == ENEMY_TYPE_NONE) 231 { 232 continue; 233 } 234 float maxHealth = EnemyGetMaxHealth(enemy); 235 if (enemy->futureDamage >= maxHealth) 236 { 237 // ignore enemies that will die soon
238 continue; 239 } 240 int16_t dx = castleX - enemy->currentX; 241 int16_t dy = castleY - enemy->currentY; 242 int16_t distance = abs(dx) + abs(dy); 243 if (!closest || distance < closestDistance) 244 { 245 float tdx = towerX - enemy->currentX; 246 float tdy = towerY - enemy->currentY; 247 float tdistance2 = tdx * tdx + tdy * tdy; 248 if (tdistance2 <= range2) 249 { 250 closest = enemy; 251 closestDistance = distance; 252 } 253 } 254 } 255 return closest; 256 } 257 258 //# Projectiles 259 #define PROJECTILE_MAX_COUNT 1200 260 #define PROJECTILE_TYPE_NONE 0 261 #define PROJECTILE_TYPE_BULLET 1 262 263 typedef struct Projectile 264 { 265 uint8_t projectileType; 266 float shootTime; 267 float arrivalTime; 268 float damage; 269 Vector2 position; 270 Vector2 target; 271 Vector2 directionNormal; 272 EnemyId targetEnemy; 273 } Projectile; 274 275 Projectile projectiles[PROJECTILE_MAX_COUNT]; 276 int projectileCount = 0; 277 278 void ProjectileInit() 279 { 280 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 281 { 282 projectiles[i] = (Projectile){0}; 283 } 284 } 285 286 void ProjectileDraw() 287 { 288 for (int i = 0; i < projectileCount; i++) 289 { 290 Projectile projectile = projectiles[i]; 291 if (projectile.projectileType == PROJECTILE_TYPE_NONE) 292 { 293 continue; 294 } 295 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime); 296 if (transition >= 1.0f) 297 { 298 continue; 299 } 300 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition); 301 float x = position.x; 302 float y = position.y; 303 float dx = projectile.directionNormal.x; 304 float dy = projectile.directionNormal.y; 305 for (float d = 1.0f; d > 0.0f; d -= 0.25f) 306 { 307 x -= dx * 0.1f; 308 y -= dy * 0.1f; 309 float size = 0.1f * d; 310 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED); 311 } 312 } 313 } 314 315 void ProjectileUpdate() 316 { 317 for (int i = 0; i < projectileCount; i++) 318 { 319 Projectile *projectile = &projectiles[i]; 320 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 321 { 322 continue; 323 } 324 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime); 325 if (transition >= 1.0f) 326 { 327 projectile->projectileType = PROJECTILE_TYPE_NONE; 328 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy); 329 if (enemy) 330 { 331 EnemyAddDamage(enemy, projectile->damage); 332 } 333 continue; 334 } 335 } 336 } 337 338 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage) 339 { 340 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 341 { 342 Projectile *projectile = &projectiles[i]; 343 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 344 { 345 projectile->projectileType = projectileType; 346 projectile->shootTime = gameTime.time; 347 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed; 348 projectile->damage = damage; 349 projectile->position = position; 350 projectile->target = target; 351 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position)); 352 projectile->targetEnemy = EnemyGetId(enemy); 353 projectileCount = projectileCount <= i ? i + 1 : projectileCount; 354 return projectile; 355 } 356 } 357 return 0; 358 } 359 360 //# Towers 361 362 #define TOWER_MAX_COUNT 400 363 #define TOWER_TYPE_NONE 0 364 #define TOWER_TYPE_BASE 1 365 #define TOWER_TYPE_GUN 2 366 367 typedef struct Tower 368 { 369 int16_t x, y; 370 uint8_t towerType; 371 float cooldown; 372 } Tower; 373 374 Tower towers[TOWER_MAX_COUNT]; 375 int towerCount = 0; 376 377 void TowerInit() 378 { 379 for (int i = 0; i < TOWER_MAX_COUNT; i++) 380 { 381 towers[i] = (Tower){0}; 382 } 383 towerCount = 0; 384 } 385 386 Tower *TowerGetAt(int16_t x, int16_t y) 387 { 388 for (int i = 0; i < towerCount; i++) 389 { 390 if (towers[i].x == x && towers[i].y == y) 391 { 392 return &towers[i]; 393 } 394 } 395 return 0; 396 } 397 398 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 399 { 400 if (towerCount >= TOWER_MAX_COUNT) 401 { 402 return 0; 403 } 404 405 Tower *tower = TowerGetAt(x, y); 406 if (tower) 407 { 408 return 0; 409 } 410 411 tower = &towers[towerCount++]; 412 tower->x = x; 413 tower->y = y; 414 tower->towerType = towerType; 415 return tower; 416 } 417 418 void TowerDraw() 419 { 420 for (int i = 0; i < towerCount; i++) 421 { 422 Tower tower = towers[i]; 423 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 424 switch (tower.towerType) 425 { 426 case TOWER_TYPE_BASE: 427 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 428 break; 429 case TOWER_TYPE_GUN: 430 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 431 break; 432 } 433 } 434 } 435 436 void TowerGunUpdate(Tower *tower) 437 { 438 if (tower->cooldown <= 0) 439 { 440 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 441 if (enemy) 442 { 443 tower->cooldown = 0.25f; 444 // shoot the enemy; determine future position of the enemy
445 float bulletSpeed = 1.0f; 446 float bulletDamage = 3.0f;
447 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime); 448 Vector2 towerPosition = {tower->x, tower->y}; 449 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 450 for (int i = 0; i < 8; i++) { 451 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta); 452 float distance = Vector2Distance(towerPosition, futurePosition); 453 float eta2 = distance / bulletSpeed; 454 if (fabs(eta - eta2) < 0.01f) { 455 break; 456 } 457 eta = (eta2 + eta) * 0.5f;
458 } 459 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition, 460 bulletSpeed, bulletDamage); 461 enemy->futureDamage += bulletDamage;
462 } 463 } 464 else 465 { 466 tower->cooldown -= gameTime.deltaTime; 467 } 468 } 469 470 void TowerUpdate() 471 { 472 for (int i = 0; i < towerCount; i++) 473 { 474 Tower *tower = &towers[i]; 475 switch (tower->towerType) 476 { 477 case TOWER_TYPE_GUN: 478 TowerGunUpdate(tower); 479 break; 480 } 481 } 482 } 483 484 //# Game 485 486 float nextSpawnTime = 0.0f; 487 488 void InitGame() 489 { 490 TowerInit(); 491 EnemyInit(); 492 ProjectileInit(); 493 494 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 495 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 496 TowerTryAdd(TOWER_TYPE_GUN, -2, 0); 497 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4); 498 } 499 500 void GameUpdate() 501 { 502 float dt = GetFrameTime(); 503 // cap maximum delta time to 0.1 seconds to prevent large time steps 504 if (dt > 0.1f) dt = 0.1f; 505 gameTime.time += dt; 506 gameTime.deltaTime = dt; 507 EnemyUpdate(); 508 TowerUpdate(); 509 ProjectileUpdate(); 510 511 // spawn a new enemy every second 512 if (gameTime.time >= nextSpawnTime) 513 { 514 nextSpawnTime = gameTime.time + 1.0f; 515 // add a new enemy at the boundary of the map 516 int randValue = GetRandomValue(-5, 5); 517 int randSide = GetRandomValue(0, 3); 518 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue; 519 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue; 520 EnemyTryAdd(ENEMY_TYPE_MINION, x, y); 521 } 522 } 523 524 int main(void) 525 { 526 int screenWidth, screenHeight; 527 GetPreferredSize(&screenWidth, &screenHeight); 528 InitWindow(screenWidth, screenHeight, "Tower defense"); 529 SetTargetFPS(30); 530 531 Camera3D camera = {0}; 532 camera.position = (Vector3){0.0f, 10.0f, 5.0f}; 533 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 534 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 535 camera.fovy = 45.0f; 536 camera.projection = CAMERA_PERSPECTIVE; 537 538 InitGame(); 539 540 while (!WindowShouldClose()) 541 { 542 if (IsPaused()) { 543 // canvas is not visible in browser - do nothing 544 continue; 545 } 546 BeginDrawing(); 547 ClearBackground(DARKBLUE); 548 549 BeginMode3D(camera); 550 DrawGrid(10, 1.0f); 551 TowerDraw(); 552 EnemyDraw(); 553 ProjectileDraw(); 554 GameUpdate(); 555 EndMode3D(); 556 557 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 558 EndDrawing(); 559 } 560 561 CloseWindow(); 562 563 return 0; 564 }
  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 #endif
  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

We can see now much clearer that the bullet prediction is working well: The impact positions seem to match the enemy positions quite well.

Path movement

The next thing we want to improve is enemy movement. Currently, the enemies have some kind of waypoints between they linearly interpolate.

We want to modify the movement to handle following things:

If this sounds like a physics simulation, then this is correct; it is simulating a simple physics system where the enemies are moving spheres.

While we could try a simpler interpolation approach, let's look into the approach to use physics simulation math for this: By using velocity and acceleration, we want to update the position while we use the velocity to guide the direction of the enemy. This is going to be a bit more complex and it usually involves lots of try and error - and I want to reflect this in the following steps to show, how a solution can be found by iterating over the problem, by showing what type of problems can occur and how they can be solved.

The heart of the movement is handled function called "EnemyGetPosition". It simulates the steps to produce a new position based on the current position and the time passed.

On a high level, what we want to do is this:

The first iteration of the code looks now like this:

  • 💾
  1 #include "td-tut-2-main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 typedef struct GameTime
  7 {
  8   float time;
  9   float deltaTime;
 10 } GameTime;
 11 
 12 GameTime gameTime = {0};
 13 
 14 //# Enemies
 15 
 16 #define ENEMY_MAX_COUNT 400
 17 #define ENEMY_TYPE_NONE 0
 18 #define ENEMY_TYPE_MINION 1
 19 
 20 typedef struct EnemyId
 21 {
 22   uint16_t index;
 23   uint16_t generation;
 24 } EnemyId;
 25 
 26 typedef struct EnemyClassConfig
 27 {
 28   float speed;
 29   float health;
30 float radius; 31 float maxAcceleration;
32 } EnemyClassConfig; 33 34 typedef struct Enemy 35 { 36 int16_t currentX, currentY;
37 int16_t nextX, nextY; 38 Vector2 simPosition; 39 Vector2 simVelocity;
40 uint16_t generation; 41 float startMovingTime; 42 float damage, futureDamage; 43 uint8_t enemyType; 44 } Enemy; 45 46 Enemy enemies[ENEMY_MAX_COUNT]; 47 int enemyCount = 0; 48 49 EnemyClassConfig enemyClassConfigs[] = {
50 [ENEMY_TYPE_MINION] = {.health = 3.0f, .speed = 1.0f, .radius = 0.25f, .maxAcceleration = 1.0f},
51 }; 52 53 void EnemyInit() 54 { 55 for (int i = 0; i < ENEMY_MAX_COUNT; i++) 56 { 57 enemies[i] = (Enemy){0}; 58 } 59 enemyCount = 0; 60 } 61
62 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
63 { 64 return enemyClassConfigs[enemy->enemyType].speed; 65 } 66 67 float EnemyGetMaxHealth(Enemy *enemy) 68 { 69 return enemyClassConfigs[enemy->enemyType].health; 70 } 71 72 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY) 73 { 74 int16_t castleX = 0; 75 int16_t castleY = 0; 76 int16_t dx = castleX - currentX; 77 int16_t dy = castleY - currentY; 78 if (dx == 0 && dy == 0) 79 { 80 *nextX = currentX; 81 *nextY = currentY; 82 return 1; 83 } 84 if (abs(dx) > abs(dy)) 85 { 86 *nextX = currentX + (dx > 0 ? 1 : -1); 87 *nextY = currentY; 88 } 89 else 90 { 91 *nextX = currentX; 92 *nextY = currentY + (dy > 0 ? 1 : -1); 93 } 94 return 0; 95 } 96
97 // this function predicts the movement of the unit for the next deltaT seconds 98 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
99 {
100 const float pointReachedDistance = 0.25f; 101 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance; 102 const float maxSimStepTime = 0.015625f; 103 104 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration; 105 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
106 int16_t nextX = enemy->nextX; 107 int16_t nextY = enemy->nextY;
108 Vector2 position = enemy->simPosition; 109 int passedCount = 0; 110 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
111 {
112 float stepTime = fminf(deltaT - t, maxSimStepTime); 113 Vector2 target = (Vector2){nextX, nextY}; 114 // draw the target position for debugging 115 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED); 116 if (Vector2DistanceSqr(target, position) <= pointReachedDistance2)
117 {
118 // we reached the target position, let's move to the next waypoint 119 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY); 120 target = (Vector2){nextX, nextY}; 121 // track how many waypoints we passed 122 passedCount++; 123 } 124 125 // acceleration towards the target 126 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, position)); 127 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime); 128 *velocity = Vector2Add(*velocity, acceleration); 129 130 // limit the speed to the maximum speed 131 float speed = Vector2Length(*velocity); 132 if (speed > maxSpeed) 133 { 134 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
135 }
136 137 // move the enemy 138 position = Vector2Add(position, Vector2Scale(*velocity, stepTime)); 139 } 140 141 if (waypointPassedCount) 142 { 143 (*waypointPassedCount) = passedCount; 144 } 145 146 return position;
147 } 148 149 void EnemyDraw() 150 { 151 for (int i = 0; i < enemyCount; i++) 152 { 153 Enemy enemy = enemies[i];
154 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
155 156 switch (enemy.enemyType) 157 { 158 case ENEMY_TYPE_MINION:
159 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
160 break; 161 } 162 } 163 } 164 165 void EnemyUpdate() 166 { 167 for (int i = 0; i < enemyCount; i++) 168 { 169 Enemy *enemy = &enemies[i]; 170 if (enemy->enemyType == ENEMY_TYPE_NONE) 171 { 172 continue; 173 }
174 175 int waypointPassedCount = 0; 176 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount); 177 enemy->startMovingTime = gameTime.time; 178 if (waypointPassedCount > 0) 179 {
180 enemy->currentX = enemy->nextX; 181 enemy->currentY = enemy->nextY; 182 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY)) 183 { 184 // enemy reached the castle; remove it 185 enemy->enemyType = ENEMY_TYPE_NONE; 186 continue; 187 }
188 } 189
190 } 191 } 192 193 EnemyId EnemyGetId(Enemy *enemy) 194 { 195 return (EnemyId){enemy - enemies, enemy->generation}; 196 } 197 198 Enemy *EnemyTryResolve(EnemyId enemyId) 199 { 200 if (enemyId.index >= ENEMY_MAX_COUNT) 201 { 202 return 0; 203 } 204 Enemy *enemy = &enemies[enemyId.index]; 205 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE) 206 { 207 return 0; 208 } 209 return enemy; 210 } 211 212 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 213 { 214 Enemy *spawn = 0; 215 for (int i = 0; i < enemyCount; i++) 216 { 217 Enemy *enemy = &enemies[i]; 218 if (enemy->enemyType == ENEMY_TYPE_NONE) 219 { 220 spawn = enemy; 221 break; 222 } 223 } 224 225 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 226 { 227 spawn = &enemies[enemyCount++]; 228 } 229 230 if (spawn) 231 { 232 spawn->currentX = currentX; 233 spawn->currentY = currentY; 234 spawn->nextX = currentX;
235 spawn->nextY = currentY; 236 spawn->simPosition = (Vector2){currentX, currentY}; 237 spawn->simVelocity = (Vector2){0, 0};
238 spawn->enemyType = enemyType; 239 spawn->startMovingTime = gameTime.time; 240 spawn->damage = 0.0f; 241 spawn->futureDamage = 0.0f; 242 spawn->generation++; 243 } 244 245 return spawn; 246 } 247 248 int EnemyAddDamage(Enemy *enemy, float damage) 249 { 250 enemy->damage += damage; 251 if (enemy->damage >= EnemyGetMaxHealth(enemy)) 252 { 253 enemy->enemyType = ENEMY_TYPE_NONE; 254 return 1; 255 } 256 257 return 0; 258 } 259 260 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 261 { 262 int16_t castleX = 0; 263 int16_t castleY = 0; 264 Enemy* closest = 0; 265 int16_t closestDistance = 0; 266 float range2 = range * range; 267 for (int i = 0; i < enemyCount; i++) 268 { 269 Enemy* enemy = &enemies[i]; 270 if (enemy->enemyType == ENEMY_TYPE_NONE) 271 { 272 continue; 273 } 274 float maxHealth = EnemyGetMaxHealth(enemy); 275 if (enemy->futureDamage >= maxHealth) 276 { 277 // ignore enemies that will die soon 278 continue; 279 } 280 int16_t dx = castleX - enemy->currentX; 281 int16_t dy = castleY - enemy->currentY; 282 int16_t distance = abs(dx) + abs(dy); 283 if (!closest || distance < closestDistance) 284 { 285 float tdx = towerX - enemy->currentX; 286 float tdy = towerY - enemy->currentY; 287 float tdistance2 = tdx * tdx + tdy * tdy; 288 if (tdistance2 <= range2) 289 { 290 closest = enemy; 291 closestDistance = distance; 292 } 293 } 294 }
295 return closest; 296 } 297 298 int EnemyCount() 299 { 300 int count = 0; 301 for (int i = 0; i < enemyCount; i++) 302 { 303 if (enemies[i].enemyType != ENEMY_TYPE_NONE) 304 { 305 count++; 306 } 307 } 308 return count;
309 } 310 311 //# Projectiles 312 #define PROJECTILE_MAX_COUNT 1200 313 #define PROJECTILE_TYPE_NONE 0 314 #define PROJECTILE_TYPE_BULLET 1 315 316 typedef struct Projectile 317 { 318 uint8_t projectileType; 319 float shootTime; 320 float arrivalTime; 321 float damage; 322 Vector2 position; 323 Vector2 target; 324 Vector2 directionNormal; 325 EnemyId targetEnemy; 326 } Projectile; 327 328 Projectile projectiles[PROJECTILE_MAX_COUNT]; 329 int projectileCount = 0; 330 331 void ProjectileInit() 332 { 333 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 334 { 335 projectiles[i] = (Projectile){0}; 336 } 337 } 338 339 void ProjectileDraw() 340 { 341 for (int i = 0; i < projectileCount; i++) 342 { 343 Projectile projectile = projectiles[i]; 344 if (projectile.projectileType == PROJECTILE_TYPE_NONE) 345 { 346 continue; 347 } 348 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime); 349 if (transition >= 1.0f) 350 { 351 continue; 352 } 353 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition); 354 float x = position.x; 355 float y = position.y; 356 float dx = projectile.directionNormal.x; 357 float dy = projectile.directionNormal.y; 358 for (float d = 1.0f; d > 0.0f; d -= 0.25f) 359 { 360 x -= dx * 0.1f; 361 y -= dy * 0.1f; 362 float size = 0.1f * d; 363 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED); 364 } 365 } 366 } 367 368 void ProjectileUpdate() 369 { 370 for (int i = 0; i < projectileCount; i++) 371 { 372 Projectile *projectile = &projectiles[i]; 373 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 374 { 375 continue; 376 } 377 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime); 378 if (transition >= 1.0f) 379 { 380 projectile->projectileType = PROJECTILE_TYPE_NONE; 381 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy); 382 if (enemy) 383 { 384 EnemyAddDamage(enemy, projectile->damage); 385 } 386 continue; 387 } 388 } 389 } 390 391 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage) 392 { 393 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 394 { 395 Projectile *projectile = &projectiles[i]; 396 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 397 { 398 projectile->projectileType = projectileType; 399 projectile->shootTime = gameTime.time; 400 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed; 401 projectile->damage = damage; 402 projectile->position = position; 403 projectile->target = target; 404 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position)); 405 projectile->targetEnemy = EnemyGetId(enemy); 406 projectileCount = projectileCount <= i ? i + 1 : projectileCount; 407 return projectile; 408 } 409 } 410 return 0; 411 } 412 413 //# Towers 414 415 #define TOWER_MAX_COUNT 400 416 #define TOWER_TYPE_NONE 0 417 #define TOWER_TYPE_BASE 1 418 #define TOWER_TYPE_GUN 2 419 420 typedef struct Tower 421 { 422 int16_t x, y; 423 uint8_t towerType; 424 float cooldown; 425 } Tower; 426 427 Tower towers[TOWER_MAX_COUNT]; 428 int towerCount = 0; 429 430 void TowerInit() 431 { 432 for (int i = 0; i < TOWER_MAX_COUNT; i++) 433 { 434 towers[i] = (Tower){0}; 435 } 436 towerCount = 0; 437 } 438 439 Tower *TowerGetAt(int16_t x, int16_t y) 440 { 441 for (int i = 0; i < towerCount; i++) 442 { 443 if (towers[i].x == x && towers[i].y == y) 444 { 445 return &towers[i]; 446 } 447 } 448 return 0; 449 } 450 451 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 452 { 453 if (towerCount >= TOWER_MAX_COUNT) 454 { 455 return 0; 456 } 457 458 Tower *tower = TowerGetAt(x, y); 459 if (tower) 460 { 461 return 0; 462 } 463 464 tower = &towers[towerCount++]; 465 tower->x = x; 466 tower->y = y; 467 tower->towerType = towerType; 468 return tower; 469 } 470 471 void TowerDraw() 472 { 473 for (int i = 0; i < towerCount; i++) 474 { 475 Tower tower = towers[i]; 476 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 477 switch (tower.towerType) 478 { 479 case TOWER_TYPE_BASE: 480 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 481 break; 482 case TOWER_TYPE_GUN: 483 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 484 break; 485 } 486 } 487 } 488 489 void TowerGunUpdate(Tower *tower) 490 { 491 if (tower->cooldown <= 0) 492 { 493 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 494 if (enemy) 495 { 496 tower->cooldown = 0.25f; 497 // shoot the enemy; determine future position of the enemy 498 float bulletSpeed = 1.0f; 499 float bulletDamage = 3.0f;
500 Vector2 velocity = enemy->simVelocity; 501 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
502 Vector2 towerPosition = {tower->x, tower->y}; 503 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 504 for (int i = 0; i < 8; i++) {
505 velocity = enemy->simVelocity; 506 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
507 float distance = Vector2Distance(towerPosition, futurePosition); 508 float eta2 = distance / bulletSpeed; 509 if (fabs(eta - eta2) < 0.01f) { 510 break; 511 } 512 eta = (eta2 + eta) * 0.5f; 513 } 514 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition, 515 bulletSpeed, bulletDamage); 516 enemy->futureDamage += bulletDamage; 517 } 518 } 519 else 520 { 521 tower->cooldown -= gameTime.deltaTime; 522 } 523 } 524 525 void TowerUpdate() 526 { 527 for (int i = 0; i < towerCount; i++) 528 { 529 Tower *tower = &towers[i]; 530 switch (tower->towerType) 531 { 532 case TOWER_TYPE_GUN: 533 TowerGunUpdate(tower); 534 break; 535 } 536 } 537 } 538 539 //# Game 540 541 float nextSpawnTime = 0.0f; 542 543 void InitGame() 544 { 545 TowerInit(); 546 EnemyInit(); 547 ProjectileInit(); 548 549 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 550 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 551 TowerTryAdd(TOWER_TYPE_GUN, -2, 0); 552 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4); 553 } 554 555 void GameUpdate() 556 { 557 float dt = GetFrameTime(); 558 // cap maximum delta time to 0.1 seconds to prevent large time steps 559 if (dt > 0.1f) dt = 0.1f; 560 gameTime.time += dt; 561 gameTime.deltaTime = dt; 562 EnemyUpdate(); 563 TowerUpdate(); 564 ProjectileUpdate(); 565 566 // spawn a new enemy every second
567 if (gameTime.time >= nextSpawnTime && EnemyCount() < 1)
568 { 569 nextSpawnTime = gameTime.time + 1.0f; 570 // add a new enemy at the boundary of the map 571 int randValue = GetRandomValue(-5, 5); 572 int randSide = GetRandomValue(0, 3); 573 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue; 574 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue; 575 EnemyTryAdd(ENEMY_TYPE_MINION, x, y); 576 } 577 } 578 579 int main(void) 580 { 581 int screenWidth, screenHeight; 582 GetPreferredSize(&screenWidth, &screenHeight); 583 InitWindow(screenWidth, screenHeight, "Tower defense"); 584 SetTargetFPS(30); 585 586 Camera3D camera = {0}; 587 camera.position = (Vector3){0.0f, 10.0f, 5.0f}; 588 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 589 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 590 camera.fovy = 45.0f; 591 camera.projection = CAMERA_PERSPECTIVE; 592 593 InitGame(); 594 595 while (!WindowShouldClose()) 596 { 597 if (IsPaused()) { 598 // canvas is not visible in browser - do nothing 599 continue; 600 } 601 BeginDrawing(); 602 ClearBackground(DARKBLUE); 603 604 BeginMode3D(camera); 605 DrawGrid(10, 1.0f); 606 TowerDraw(); 607 EnemyDraw(); 608 ProjectileDraw(); 609 GameUpdate(); 610 EndMode3D(); 611 612 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 613 EndDrawing(); 614 } 615 616 CloseWindow(); 617 618 return 0; 619 }
  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 #endif
  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

The behavior we're observing is not quite working: The enemy circles the next waypoint and is never reaching it. This is because while the acceleration is pointing towards the waypoint, our current forward velocity is keeping us on a tangent. We need to take this into account when we calculate the acceleration.

There are all kinds of ways how this can be solved, but the most simple way I have found is to extrapolate the position based on the current velocity and use that position for calculating the new acceleration vector. If our enemy moves on a tangent, this will automatically introduce a deceleration that counters the forward velocity:

Graphical depiction of vectors when using the current position vs the predicted position for calculating the acceleration

When we'll take a future point for calculating the acceleration, how far should we project the future point? In this case, I found out by experimentation that taking the current speed and using this as the time factor for the future point works quite well. My thinking is that the greater the speed in comparison to the maximum acceleration is, the further we'll have to project the future point - since we'll need more time to counteract the current velocity.

Let's see how this'll look:

  • 💾
  1 #include "td-tut-2-main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 typedef struct GameTime
  7 {
  8   float time;
  9   float deltaTime;
 10 } GameTime;
 11 
 12 GameTime gameTime = {0};
 13 
 14 //# Enemies
 15 
 16 #define ENEMY_MAX_COUNT 400
 17 #define ENEMY_TYPE_NONE 0
 18 #define ENEMY_TYPE_MINION 1
 19 
 20 typedef struct EnemyId
 21 {
 22   uint16_t index;
 23   uint16_t generation;
 24 } EnemyId;
 25 
 26 typedef struct EnemyClassConfig
 27 {
 28   float speed;
 29   float health;
 30   float radius;
 31   float maxAcceleration;
 32 } EnemyClassConfig;
 33 
 34 typedef struct Enemy
 35 {
 36   int16_t currentX, currentY;
 37   int16_t nextX, nextY;
 38   Vector2 simPosition;
 39   Vector2 simVelocity;
 40   uint16_t generation;
 41   float startMovingTime;
 42   float damage, futureDamage;
 43   uint8_t enemyType;
 44 } Enemy;
 45 
 46 Enemy enemies[ENEMY_MAX_COUNT];
 47 int enemyCount = 0;
 48 
 49 EnemyClassConfig enemyClassConfigs[] = {
 50     [ENEMY_TYPE_MINION] = {.health = 3.0f, .speed = 1.0f, .radius = 0.25f, .maxAcceleration = 1.0f},
 51 };
 52 
 53 void EnemyInit()
 54 {
 55   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 56   {
 57     enemies[i] = (Enemy){0};
 58   }
 59   enemyCount = 0;
 60 }
 61 
 62 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 63 {
 64   return enemyClassConfigs[enemy->enemyType].speed;
 65 }
 66 
 67 float EnemyGetMaxHealth(Enemy *enemy)
 68 {
 69   return enemyClassConfigs[enemy->enemyType].health;
 70 }
 71 
 72 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 73 {
 74   int16_t castleX = 0;
 75   int16_t castleY = 0;
 76   int16_t dx = castleX - currentX;
 77   int16_t dy = castleY - currentY;
 78   if (dx == 0 && dy == 0)
 79   {
 80     *nextX = currentX;
 81     *nextY = currentY;
 82     return 1;
 83   }
 84   if (abs(dx) > abs(dy))
 85   {
 86     *nextX = currentX + (dx > 0 ? 1 : -1);
 87     *nextY = currentY;
 88   }
 89   else
 90   {
 91     *nextX = currentX;
 92     *nextY = currentY + (dy > 0 ? 1 : -1);
 93   }
 94   return 0;
 95 }
 96 
97
98 // this function predicts the movement of the unit for the next deltaT seconds 99 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount) 100 { 101 const float pointReachedDistance = 0.25f; 102 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance; 103 const float maxSimStepTime = 0.015625f; 104 105 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration; 106 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy); 107 int16_t nextX = enemy->nextX; 108 int16_t nextY = enemy->nextY; 109 Vector2 position = enemy->simPosition; 110 int passedCount = 0; 111 for (float t = 0.0f; t < deltaT; t += maxSimStepTime) 112 { 113 float stepTime = fminf(deltaT - t, maxSimStepTime);
114 Vector2 target = (Vector2){nextX, nextY}; 115 float speed = Vector2Length(*velocity);
116 // draw the target position for debugging 117 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
118 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed)); 119 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
120 { 121 // we reached the target position, let's move to the next waypoint 122 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY); 123 target = (Vector2){nextX, nextY}; 124 // track how many waypoints we passed 125 passedCount++; 126 } 127 128 // acceleration towards the target
129 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
130 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime); 131 *velocity = Vector2Add(*velocity, acceleration);
132 133 // limit the speed to the maximum speed
134 if (speed > maxSpeed) 135 { 136 *velocity = Vector2Scale(*velocity, maxSpeed / speed); 137 } 138 139 // move the enemy 140 position = Vector2Add(position, Vector2Scale(*velocity, stepTime)); 141 } 142 143 if (waypointPassedCount) 144 { 145 (*waypointPassedCount) = passedCount; 146 } 147 148 return position; 149 } 150 151 void EnemyDraw() 152 { 153 for (int i = 0; i < enemyCount; i++) 154 { 155 Enemy enemy = enemies[i]; 156 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0); 157 158 switch (enemy.enemyType) 159 { 160 case ENEMY_TYPE_MINION: 161 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN); 162 break; 163 } 164 } 165 } 166 167 void EnemyUpdate() 168 { 169 for (int i = 0; i < enemyCount; i++) 170 { 171 Enemy *enemy = &enemies[i]; 172 if (enemy->enemyType == ENEMY_TYPE_NONE) 173 { 174 continue; 175 } 176 177 int waypointPassedCount = 0; 178 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount); 179 enemy->startMovingTime = gameTime.time; 180 if (waypointPassedCount > 0) 181 { 182 enemy->currentX = enemy->nextX; 183 enemy->currentY = enemy->nextY; 184 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY)) 185 { 186 // enemy reached the castle; remove it 187 enemy->enemyType = ENEMY_TYPE_NONE; 188 continue; 189 } 190 } 191 192 } 193 } 194 195 EnemyId EnemyGetId(Enemy *enemy) 196 { 197 return (EnemyId){enemy - enemies, enemy->generation}; 198 } 199 200 Enemy *EnemyTryResolve(EnemyId enemyId) 201 { 202 if (enemyId.index >= ENEMY_MAX_COUNT) 203 { 204 return 0; 205 } 206 Enemy *enemy = &enemies[enemyId.index]; 207 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE) 208 { 209 return 0; 210 } 211 return enemy; 212 } 213 214 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 215 { 216 Enemy *spawn = 0; 217 for (int i = 0; i < enemyCount; i++) 218 { 219 Enemy *enemy = &enemies[i]; 220 if (enemy->enemyType == ENEMY_TYPE_NONE) 221 { 222 spawn = enemy; 223 break; 224 } 225 } 226 227 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 228 { 229 spawn = &enemies[enemyCount++]; 230 } 231 232 if (spawn) 233 { 234 spawn->currentX = currentX; 235 spawn->currentY = currentY; 236 spawn->nextX = currentX; 237 spawn->nextY = currentY; 238 spawn->simPosition = (Vector2){currentX, currentY}; 239 spawn->simVelocity = (Vector2){0, 0}; 240 spawn->enemyType = enemyType; 241 spawn->startMovingTime = gameTime.time; 242 spawn->damage = 0.0f; 243 spawn->futureDamage = 0.0f; 244 spawn->generation++; 245 } 246 247 return spawn; 248 } 249 250 int EnemyAddDamage(Enemy *enemy, float damage) 251 { 252 enemy->damage += damage; 253 if (enemy->damage >= EnemyGetMaxHealth(enemy)) 254 { 255 enemy->enemyType = ENEMY_TYPE_NONE; 256 return 1; 257 } 258 259 return 0; 260 } 261 262 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 263 { 264 int16_t castleX = 0; 265 int16_t castleY = 0; 266 Enemy* closest = 0; 267 int16_t closestDistance = 0; 268 float range2 = range * range; 269 for (int i = 0; i < enemyCount; i++) 270 { 271 Enemy* enemy = &enemies[i]; 272 if (enemy->enemyType == ENEMY_TYPE_NONE) 273 { 274 continue; 275 } 276 float maxHealth = EnemyGetMaxHealth(enemy); 277 if (enemy->futureDamage >= maxHealth) 278 { 279 // ignore enemies that will die soon 280 continue; 281 } 282 int16_t dx = castleX - enemy->currentX; 283 int16_t dy = castleY - enemy->currentY; 284 int16_t distance = abs(dx) + abs(dy); 285 if (!closest || distance < closestDistance) 286 { 287 float tdx = towerX - enemy->currentX; 288 float tdy = towerY - enemy->currentY; 289 float tdistance2 = tdx * tdx + tdy * tdy; 290 if (tdistance2 <= range2) 291 { 292 closest = enemy; 293 closestDistance = distance; 294 } 295 } 296 } 297 return closest; 298 } 299 300 int EnemyCount() 301 { 302 int count = 0; 303 for (int i = 0; i < enemyCount; i++) 304 { 305 if (enemies[i].enemyType != ENEMY_TYPE_NONE) 306 { 307 count++; 308 } 309 } 310 return count; 311 } 312 313 //# Projectiles 314 #define PROJECTILE_MAX_COUNT 1200 315 #define PROJECTILE_TYPE_NONE 0 316 #define PROJECTILE_TYPE_BULLET 1 317 318 typedef struct Projectile 319 { 320 uint8_t projectileType; 321 float shootTime; 322 float arrivalTime; 323 float damage; 324 Vector2 position; 325 Vector2 target; 326 Vector2 directionNormal; 327 EnemyId targetEnemy; 328 } Projectile; 329 330 Projectile projectiles[PROJECTILE_MAX_COUNT]; 331 int projectileCount = 0; 332 333 void ProjectileInit() 334 { 335 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 336 { 337 projectiles[i] = (Projectile){0}; 338 } 339 } 340 341 void ProjectileDraw() 342 { 343 for (int i = 0; i < projectileCount; i++) 344 { 345 Projectile projectile = projectiles[i]; 346 if (projectile.projectileType == PROJECTILE_TYPE_NONE) 347 { 348 continue; 349 } 350 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime); 351 if (transition >= 1.0f) 352 { 353 continue; 354 } 355 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition); 356 float x = position.x; 357 float y = position.y; 358 float dx = projectile.directionNormal.x; 359 float dy = projectile.directionNormal.y; 360 for (float d = 1.0f; d > 0.0f; d -= 0.25f) 361 { 362 x -= dx * 0.1f; 363 y -= dy * 0.1f; 364 float size = 0.1f * d; 365 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED); 366 } 367 } 368 } 369 370 void ProjectileUpdate() 371 { 372 for (int i = 0; i < projectileCount; i++) 373 { 374 Projectile *projectile = &projectiles[i]; 375 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 376 { 377 continue; 378 } 379 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime); 380 if (transition >= 1.0f) 381 { 382 projectile->projectileType = PROJECTILE_TYPE_NONE; 383 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy); 384 if (enemy) 385 { 386 EnemyAddDamage(enemy, projectile->damage); 387 } 388 continue; 389 } 390 } 391 } 392 393 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage) 394 { 395 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 396 { 397 Projectile *projectile = &projectiles[i]; 398 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 399 { 400 projectile->projectileType = projectileType; 401 projectile->shootTime = gameTime.time; 402 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed; 403 projectile->damage = damage; 404 projectile->position = position; 405 projectile->target = target; 406 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position)); 407 projectile->targetEnemy = EnemyGetId(enemy); 408 projectileCount = projectileCount <= i ? i + 1 : projectileCount; 409 return projectile; 410 } 411 } 412 return 0; 413 } 414 415 //# Towers 416 417 #define TOWER_MAX_COUNT 400 418 #define TOWER_TYPE_NONE 0 419 #define TOWER_TYPE_BASE 1 420 #define TOWER_TYPE_GUN 2 421 422 typedef struct Tower 423 { 424 int16_t x, y; 425 uint8_t towerType; 426 float cooldown; 427 } Tower; 428 429 Tower towers[TOWER_MAX_COUNT]; 430 int towerCount = 0; 431 432 void TowerInit() 433 { 434 for (int i = 0; i < TOWER_MAX_COUNT; i++) 435 { 436 towers[i] = (Tower){0}; 437 } 438 towerCount = 0; 439 } 440 441 Tower *TowerGetAt(int16_t x, int16_t y) 442 { 443 for (int i = 0; i < towerCount; i++) 444 { 445 if (towers[i].x == x && towers[i].y == y) 446 { 447 return &towers[i]; 448 } 449 } 450 return 0; 451 } 452 453 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 454 { 455 if (towerCount >= TOWER_MAX_COUNT) 456 { 457 return 0; 458 } 459 460 Tower *tower = TowerGetAt(x, y); 461 if (tower) 462 { 463 return 0; 464 } 465 466 tower = &towers[towerCount++]; 467 tower->x = x; 468 tower->y = y; 469 tower->towerType = towerType; 470 return tower; 471 } 472 473 void TowerDraw() 474 { 475 for (int i = 0; i < towerCount; i++) 476 { 477 Tower tower = towers[i]; 478 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 479 switch (tower.towerType) 480 { 481 case TOWER_TYPE_BASE: 482 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 483 break; 484 case TOWER_TYPE_GUN: 485 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 486 break; 487 } 488 } 489 } 490 491 void TowerGunUpdate(Tower *tower) 492 { 493 if (tower->cooldown <= 0) 494 { 495 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 496 if (enemy) 497 { 498 tower->cooldown = 0.25f; 499 // shoot the enemy; determine future position of the enemy 500 float bulletSpeed = 1.0f; 501 float bulletDamage = 3.0f; 502 Vector2 velocity = enemy->simVelocity; 503 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 504 Vector2 towerPosition = {tower->x, tower->y}; 505 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 506 for (int i = 0; i < 8; i++) { 507 velocity = enemy->simVelocity; 508 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 509 float distance = Vector2Distance(towerPosition, futurePosition); 510 float eta2 = distance / bulletSpeed; 511 if (fabs(eta - eta2) < 0.01f) { 512 break; 513 } 514 eta = (eta2 + eta) * 0.5f; 515 } 516 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition, 517 bulletSpeed, bulletDamage); 518 enemy->futureDamage += bulletDamage; 519 } 520 } 521 else 522 { 523 tower->cooldown -= gameTime.deltaTime; 524 } 525 } 526 527 void TowerUpdate() 528 { 529 for (int i = 0; i < towerCount; i++) 530 { 531 Tower *tower = &towers[i]; 532 switch (tower->towerType) 533 { 534 case TOWER_TYPE_GUN: 535 TowerGunUpdate(tower); 536 break; 537 } 538 } 539 } 540 541 //# Game 542 543 float nextSpawnTime = 0.0f; 544 545 void InitGame() 546 { 547 TowerInit(); 548 EnemyInit(); 549 ProjectileInit(); 550 551 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 552 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 553 TowerTryAdd(TOWER_TYPE_GUN, -2, 0); 554 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4); 555 } 556 557 void GameUpdate() 558 { 559 float dt = GetFrameTime(); 560 // cap maximum delta time to 0.1 seconds to prevent large time steps 561 if (dt > 0.1f) dt = 0.1f; 562 gameTime.time += dt; 563 gameTime.deltaTime = dt; 564 EnemyUpdate(); 565 TowerUpdate(); 566 ProjectileUpdate(); 567 568 // spawn a new enemy every second 569 if (gameTime.time >= nextSpawnTime && EnemyCount() < 1) 570 { 571 nextSpawnTime = gameTime.time + 1.0f; 572 // add a new enemy at the boundary of the map 573 int randValue = GetRandomValue(-5, 5); 574 int randSide = GetRandomValue(0, 3); 575 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue; 576 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue; 577 EnemyTryAdd(ENEMY_TYPE_MINION, x, y); 578 } 579 } 580 581 int main(void) 582 { 583 int screenWidth, screenHeight; 584 GetPreferredSize(&screenWidth, &screenHeight); 585 InitWindow(screenWidth, screenHeight, "Tower defense"); 586 SetTargetFPS(30); 587 588 Camera3D camera = {0}; 589 camera.position = (Vector3){0.0f, 10.0f, 5.0f}; 590 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 591 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 592 camera.fovy = 45.0f; 593 camera.projection = CAMERA_PERSPECTIVE; 594 595 InitGame(); 596 597 while (!WindowShouldClose()) 598 { 599 if (IsPaused()) { 600 // canvas is not visible in browser - do nothing 601 continue; 602 } 603 BeginDrawing(); 604 ClearBackground(DARKBLUE); 605 606 BeginMode3D(camera); 607 DrawGrid(10, 1.0f); 608 TowerDraw(); 609 EnemyDraw(); 610 ProjectileDraw(); 611 GameUpdate(); 612 EndMode3D(); 613 614 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 615 EndDrawing(); 616 } 617 618 CloseWindow(); 619 620 return 0; 621 }
  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 #endif
  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

This is working surprisingly well! The enemy is now almost on a straight line when moving diagonally and maintains a steady speed. The projectiles seem to hit the enemy as well, though the camera perspective makes this a little difficult to judge. It makes sense to change the camera perspective to a top-down view and using an orthogonal projection for now. Also, how about drawing the path of the enemy to see how it moves? This might be useful for debugging and maybe some future effects.

For tracking the path, we'll add a fixed number of points to the enemy struct; when the enemy has moved a certain distance, we'll add the current position to the path. 8 points should be enough for our purpose.

  • 💾
  1 #include "td-tut-2-main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 typedef struct GameTime
  7 {
  8   float time;
  9   float deltaTime;
 10 } GameTime;
 11 
 12 GameTime gameTime = {0};
 13 
 14 //# Enemies
 15 
16 #define ENEMY_MAX_PATH_COUNT 8
17 #define ENEMY_MAX_COUNT 400 18 #define ENEMY_TYPE_NONE 0 19 #define ENEMY_TYPE_MINION 1 20 21 typedef struct EnemyId 22 { 23 uint16_t index; 24 uint16_t generation; 25 } EnemyId; 26 27 typedef struct EnemyClassConfig 28 { 29 float speed; 30 float health; 31 float radius; 32 float maxAcceleration; 33 } EnemyClassConfig; 34 35 typedef struct Enemy 36 { 37 int16_t currentX, currentY; 38 int16_t nextX, nextY; 39 Vector2 simPosition; 40 Vector2 simVelocity; 41 uint16_t generation; 42 float startMovingTime; 43 float damage, futureDamage;
44 uint8_t enemyType; 45 uint8_t movePathCount; 46 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
47 } Enemy; 48 49 Enemy enemies[ENEMY_MAX_COUNT]; 50 int enemyCount = 0; 51 52 EnemyClassConfig enemyClassConfigs[] = { 53 [ENEMY_TYPE_MINION] = {.health = 3.0f, .speed = 1.0f, .radius = 0.25f, .maxAcceleration = 1.0f}, 54 }; 55 56 void EnemyInit() 57 { 58 for (int i = 0; i < ENEMY_MAX_COUNT; i++) 59 { 60 enemies[i] = (Enemy){0}; 61 } 62 enemyCount = 0; 63 } 64 65 float EnemyGetCurrentMaxSpeed(Enemy *enemy) 66 { 67 return enemyClassConfigs[enemy->enemyType].speed; 68 } 69 70 float EnemyGetMaxHealth(Enemy *enemy) 71 { 72 return enemyClassConfigs[enemy->enemyType].health; 73 } 74 75 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY) 76 { 77 int16_t castleX = 0; 78 int16_t castleY = 0; 79 int16_t dx = castleX - currentX; 80 int16_t dy = castleY - currentY; 81 if (dx == 0 && dy == 0) 82 { 83 *nextX = currentX; 84 *nextY = currentY; 85 return 1; 86 } 87 if (abs(dx) > abs(dy)) 88 { 89 *nextX = currentX + (dx > 0 ? 1 : -1); 90 *nextY = currentY; 91 } 92 else 93 { 94 *nextX = currentX; 95 *nextY = currentY + (dy > 0 ? 1 : -1); 96 } 97 return 0; 98 } 99 100 101 // this function predicts the movement of the unit for the next deltaT seconds 102 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount) 103 { 104 const float pointReachedDistance = 0.25f; 105 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance; 106 const float maxSimStepTime = 0.015625f; 107 108 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration; 109 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy); 110 int16_t nextX = enemy->nextX; 111 int16_t nextY = enemy->nextY; 112 Vector2 position = enemy->simPosition; 113 int passedCount = 0; 114 for (float t = 0.0f; t < deltaT; t += maxSimStepTime) 115 { 116 float stepTime = fminf(deltaT - t, maxSimStepTime); 117 Vector2 target = (Vector2){nextX, nextY}; 118 float speed = Vector2Length(*velocity); 119 // draw the target position for debugging 120 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED); 121 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed)); 122 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2) 123 { 124 // we reached the target position, let's move to the next waypoint 125 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY); 126 target = (Vector2){nextX, nextY}; 127 // track how many waypoints we passed 128 passedCount++; 129 } 130 131 // acceleration towards the target 132 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos)); 133 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime); 134 *velocity = Vector2Add(*velocity, acceleration); 135 136 // limit the speed to the maximum speed 137 if (speed > maxSpeed) 138 { 139 *velocity = Vector2Scale(*velocity, maxSpeed / speed); 140 } 141 142 // move the enemy 143 position = Vector2Add(position, Vector2Scale(*velocity, stepTime)); 144 } 145 146 if (waypointPassedCount) 147 { 148 (*waypointPassedCount) = passedCount; 149 } 150 151 return position; 152 } 153 154 void EnemyDraw() 155 { 156 for (int i = 0; i < enemyCount; i++) 157 {
158 Enemy enemy = enemies[i]; 159 if (enemy.enemyType == ENEMY_TYPE_NONE) 160 { 161 continue; 162 } 163
164 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
165 166 if (enemy.movePathCount > 0) 167 { 168 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y}; 169 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN); 170 } 171 for (int j = 1; j < enemy.movePathCount; j++) 172 { 173 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y}; 174 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y}; 175 DrawLine3D(p, q, GREEN); 176 } 177
178 switch (enemy.enemyType) 179 { 180 case ENEMY_TYPE_MINION: 181 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN); 182 break; 183 } 184 } 185 } 186 187 void EnemyUpdate()
188 { 189 const float maxPathDistance2 = 0.25f * 0.25f;
190 for (int i = 0; i < enemyCount; i++) 191 { 192 Enemy *enemy = &enemies[i]; 193 if (enemy->enemyType == ENEMY_TYPE_NONE) 194 { 195 continue; 196 } 197 198 int waypointPassedCount = 0; 199 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
200 enemy->startMovingTime = gameTime.time; 201 // track path of unit 202 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2) 203 { 204 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--) 205 { 206 enemy->movePath[j] = enemy->movePath[j - 1]; 207 } 208 enemy->movePath[0] = enemy->simPosition; 209 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT) 210 { 211 enemy->movePathCount = ENEMY_MAX_PATH_COUNT; 212 } 213 } 214
215 if (waypointPassedCount > 0) 216 { 217 enemy->currentX = enemy->nextX; 218 enemy->currentY = enemy->nextY; 219 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY)) 220 { 221 // enemy reached the castle; remove it 222 enemy->enemyType = ENEMY_TYPE_NONE; 223 continue; 224 } 225 } 226 227 } 228 } 229 230 EnemyId EnemyGetId(Enemy *enemy) 231 { 232 return (EnemyId){enemy - enemies, enemy->generation}; 233 } 234 235 Enemy *EnemyTryResolve(EnemyId enemyId) 236 { 237 if (enemyId.index >= ENEMY_MAX_COUNT) 238 { 239 return 0; 240 } 241 Enemy *enemy = &enemies[enemyId.index]; 242 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE) 243 { 244 return 0; 245 } 246 return enemy; 247 } 248 249 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 250 { 251 Enemy *spawn = 0; 252 for (int i = 0; i < enemyCount; i++) 253 { 254 Enemy *enemy = &enemies[i]; 255 if (enemy->enemyType == ENEMY_TYPE_NONE) 256 { 257 spawn = enemy; 258 break; 259 } 260 } 261 262 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 263 { 264 spawn = &enemies[enemyCount++]; 265 } 266 267 if (spawn) 268 { 269 spawn->currentX = currentX; 270 spawn->currentY = currentY; 271 spawn->nextX = currentX; 272 spawn->nextY = currentY; 273 spawn->simPosition = (Vector2){currentX, currentY}; 274 spawn->simVelocity = (Vector2){0, 0}; 275 spawn->enemyType = enemyType; 276 spawn->startMovingTime = gameTime.time; 277 spawn->damage = 0.0f; 278 spawn->futureDamage = 0.0f;
279 spawn->generation++; 280 spawn->movePathCount = 0;
281 } 282 283 return spawn; 284 } 285 286 int EnemyAddDamage(Enemy *enemy, float damage) 287 { 288 enemy->damage += damage; 289 if (enemy->damage >= EnemyGetMaxHealth(enemy)) 290 { 291 enemy->enemyType = ENEMY_TYPE_NONE; 292 return 1; 293 } 294 295 return 0; 296 } 297 298 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 299 { 300 int16_t castleX = 0; 301 int16_t castleY = 0; 302 Enemy* closest = 0; 303 int16_t closestDistance = 0; 304 float range2 = range * range; 305 for (int i = 0; i < enemyCount; i++) 306 { 307 Enemy* enemy = &enemies[i]; 308 if (enemy->enemyType == ENEMY_TYPE_NONE) 309 { 310 continue; 311 } 312 float maxHealth = EnemyGetMaxHealth(enemy); 313 if (enemy->futureDamage >= maxHealth) 314 { 315 // ignore enemies that will die soon 316 continue; 317 } 318 int16_t dx = castleX - enemy->currentX; 319 int16_t dy = castleY - enemy->currentY; 320 int16_t distance = abs(dx) + abs(dy); 321 if (!closest || distance < closestDistance) 322 { 323 float tdx = towerX - enemy->currentX; 324 float tdy = towerY - enemy->currentY; 325 float tdistance2 = tdx * tdx + tdy * tdy; 326 if (tdistance2 <= range2) 327 { 328 closest = enemy; 329 closestDistance = distance; 330 } 331 } 332 } 333 return closest; 334 } 335 336 int EnemyCount() 337 { 338 int count = 0; 339 for (int i = 0; i < enemyCount; i++) 340 { 341 if (enemies[i].enemyType != ENEMY_TYPE_NONE) 342 { 343 count++; 344 } 345 } 346 return count; 347 } 348 349 //# Projectiles 350 #define PROJECTILE_MAX_COUNT 1200 351 #define PROJECTILE_TYPE_NONE 0 352 #define PROJECTILE_TYPE_BULLET 1 353 354 typedef struct Projectile 355 { 356 uint8_t projectileType; 357 float shootTime; 358 float arrivalTime; 359 float damage; 360 Vector2 position; 361 Vector2 target; 362 Vector2 directionNormal; 363 EnemyId targetEnemy; 364 } Projectile; 365 366 Projectile projectiles[PROJECTILE_MAX_COUNT]; 367 int projectileCount = 0; 368 369 void ProjectileInit() 370 { 371 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 372 { 373 projectiles[i] = (Projectile){0}; 374 } 375 } 376 377 void ProjectileDraw() 378 { 379 for (int i = 0; i < projectileCount; i++) 380 { 381 Projectile projectile = projectiles[i]; 382 if (projectile.projectileType == PROJECTILE_TYPE_NONE) 383 { 384 continue; 385 } 386 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime); 387 if (transition >= 1.0f) 388 { 389 continue; 390 } 391 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition); 392 float x = position.x; 393 float y = position.y; 394 float dx = projectile.directionNormal.x; 395 float dy = projectile.directionNormal.y; 396 for (float d = 1.0f; d > 0.0f; d -= 0.25f) 397 { 398 x -= dx * 0.1f; 399 y -= dy * 0.1f; 400 float size = 0.1f * d; 401 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED); 402 } 403 } 404 } 405 406 void ProjectileUpdate() 407 { 408 for (int i = 0; i < projectileCount; i++) 409 { 410 Projectile *projectile = &projectiles[i]; 411 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 412 { 413 continue; 414 } 415 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime); 416 if (transition >= 1.0f) 417 { 418 projectile->projectileType = PROJECTILE_TYPE_NONE; 419 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy); 420 if (enemy) 421 { 422 EnemyAddDamage(enemy, projectile->damage); 423 } 424 continue; 425 } 426 } 427 } 428 429 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage) 430 { 431 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 432 { 433 Projectile *projectile = &projectiles[i]; 434 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 435 { 436 projectile->projectileType = projectileType; 437 projectile->shootTime = gameTime.time; 438 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed; 439 projectile->damage = damage; 440 projectile->position = position; 441 projectile->target = target; 442 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position)); 443 projectile->targetEnemy = EnemyGetId(enemy); 444 projectileCount = projectileCount <= i ? i + 1 : projectileCount; 445 return projectile; 446 } 447 } 448 return 0; 449 } 450 451 //# Towers 452 453 #define TOWER_MAX_COUNT 400 454 #define TOWER_TYPE_NONE 0 455 #define TOWER_TYPE_BASE 1 456 #define TOWER_TYPE_GUN 2 457 458 typedef struct Tower 459 { 460 int16_t x, y; 461 uint8_t towerType; 462 float cooldown; 463 } Tower; 464 465 Tower towers[TOWER_MAX_COUNT]; 466 int towerCount = 0; 467 468 void TowerInit() 469 { 470 for (int i = 0; i < TOWER_MAX_COUNT; i++) 471 { 472 towers[i] = (Tower){0}; 473 } 474 towerCount = 0; 475 } 476 477 Tower *TowerGetAt(int16_t x, int16_t y) 478 { 479 for (int i = 0; i < towerCount; i++) 480 { 481 if (towers[i].x == x && towers[i].y == y) 482 { 483 return &towers[i]; 484 } 485 } 486 return 0; 487 } 488 489 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 490 { 491 if (towerCount >= TOWER_MAX_COUNT) 492 { 493 return 0; 494 } 495 496 Tower *tower = TowerGetAt(x, y); 497 if (tower) 498 { 499 return 0; 500 } 501 502 tower = &towers[towerCount++]; 503 tower->x = x; 504 tower->y = y; 505 tower->towerType = towerType; 506 return tower; 507 } 508 509 void TowerDraw() 510 { 511 for (int i = 0; i < towerCount; i++) 512 { 513 Tower tower = towers[i]; 514 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 515 switch (tower.towerType) 516 { 517 case TOWER_TYPE_BASE: 518 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 519 break; 520 case TOWER_TYPE_GUN: 521 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 522 break; 523 } 524 } 525 } 526 527 void TowerGunUpdate(Tower *tower) 528 { 529 if (tower->cooldown <= 0) 530 { 531 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 532 if (enemy) 533 { 534 tower->cooldown = 0.25f; 535 // shoot the enemy; determine future position of the enemy 536 float bulletSpeed = 1.0f; 537 float bulletDamage = 3.0f; 538 Vector2 velocity = enemy->simVelocity; 539 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 540 Vector2 towerPosition = {tower->x, tower->y}; 541 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 542 for (int i = 0; i < 8; i++) { 543 velocity = enemy->simVelocity; 544 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 545 float distance = Vector2Distance(towerPosition, futurePosition); 546 float eta2 = distance / bulletSpeed; 547 if (fabs(eta - eta2) < 0.01f) { 548 break; 549 } 550 eta = (eta2 + eta) * 0.5f; 551 } 552 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition, 553 bulletSpeed, bulletDamage); 554 enemy->futureDamage += bulletDamage; 555 } 556 } 557 else 558 { 559 tower->cooldown -= gameTime.deltaTime; 560 } 561 } 562 563 void TowerUpdate() 564 { 565 for (int i = 0; i < towerCount; i++) 566 { 567 Tower *tower = &towers[i]; 568 switch (tower->towerType) 569 { 570 case TOWER_TYPE_GUN: 571 TowerGunUpdate(tower); 572 break; 573 } 574 } 575 } 576 577 //# Game 578 579 float nextSpawnTime = 0.0f; 580 581 void InitGame() 582 { 583 TowerInit(); 584 EnemyInit(); 585 ProjectileInit(); 586 587 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 588 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 589 TowerTryAdd(TOWER_TYPE_GUN, -2, 0); 590 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4); 591 } 592 593 void GameUpdate() 594 { 595 float dt = GetFrameTime(); 596 // cap maximum delta time to 0.1 seconds to prevent large time steps 597 if (dt > 0.1f) dt = 0.1f; 598 gameTime.time += dt; 599 gameTime.deltaTime = dt; 600 EnemyUpdate(); 601 TowerUpdate(); 602 ProjectileUpdate(); 603 604 // spawn a new enemy every second
605 if (gameTime.time >= nextSpawnTime && EnemyCount() < 10)
606 { 607 nextSpawnTime = gameTime.time + 1.0f; 608 // add a new enemy at the boundary of the map 609 int randValue = GetRandomValue(-5, 5); 610 int randSide = GetRandomValue(0, 3); 611 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue;
612 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue; 613 static int alternation = 0; 614 alternation += 1; 615 if (alternation % 3 == 0) { 616 EnemyTryAdd(ENEMY_TYPE_MINION, 0, -5); 617 } 618 else if (alternation % 3 == 1) 619 { 620 EnemyTryAdd(ENEMY_TYPE_MINION, 0, 5); 621 }
622 EnemyTryAdd(ENEMY_TYPE_MINION, x, y); 623 } 624 } 625 626 int main(void) 627 { 628 int screenWidth, screenHeight; 629 GetPreferredSize(&screenWidth, &screenHeight); 630 InitWindow(screenWidth, screenHeight, "Tower defense"); 631 SetTargetFPS(30); 632 633 Camera3D camera = {0};
634 camera.position = (Vector3){0.0f, 10.0f, 0.0f};
635 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 636 camera.up = (Vector3){0.0f, 0.0f, -1.0f};
637 camera.fovy = 10.0f; 638 camera.projection = CAMERA_ORTHOGRAPHIC;
639 640 InitGame(); 641 642 while (!WindowShouldClose()) 643 { 644 if (IsPaused()) { 645 // canvas is not visible in browser - do nothing 646 continue; 647 } 648 BeginDrawing(); 649 ClearBackground(DARKBLUE); 650 651 BeginMode3D(camera); 652 DrawGrid(10, 1.0f); 653 TowerDraw(); 654 EnemyDraw(); 655 ProjectileDraw(); 656 GameUpdate(); 657 EndMode3D(); 658 659 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 660 EndDrawing(); 661 } 662 663 CloseWindow(); 664 665 return 0; 666 }
  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 #endif
  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

The path is now visible and we can see how the enemy moves quite well from above. What confused me first, was that when the enemy is moving in a vertical straight line towards the tower, the projectile is not hitting the enemy because the enemy disappears well before that. But the reason is, that the detection if the enemy has reached the castle is now flawed and must be fixed:

  • 💾
  1 #include "td-tut-2-main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 typedef struct GameTime
  7 {
  8   float time;
  9   float deltaTime;
 10 } GameTime;
 11 
 12 GameTime gameTime = {0};
 13 
 14 //# Enemies
 15 
 16 #define ENEMY_MAX_PATH_COUNT 8
 17 #define ENEMY_MAX_COUNT 400
 18 #define ENEMY_TYPE_NONE 0
 19 #define ENEMY_TYPE_MINION 1
 20 
 21 typedef struct EnemyId
 22 {
 23   uint16_t index;
 24   uint16_t generation;
 25 } EnemyId;
 26 
 27 typedef struct EnemyClassConfig
 28 {
 29   float speed;
 30   float health;
 31   float radius;
 32   float maxAcceleration;
 33 } EnemyClassConfig;
 34 
 35 typedef struct Enemy
 36 {
 37   int16_t currentX, currentY;
 38   int16_t nextX, nextY;
 39   Vector2 simPosition;
 40   Vector2 simVelocity;
 41   uint16_t generation;
 42   float startMovingTime;
 43   float damage, futureDamage;
 44   uint8_t enemyType;
 45   uint8_t movePathCount;
 46   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
 47 } Enemy;
 48 
 49 Enemy enemies[ENEMY_MAX_COUNT];
 50 int enemyCount = 0;
 51 
 52 EnemyClassConfig enemyClassConfigs[] = {
 53     [ENEMY_TYPE_MINION] = {.health = 3.0f, .speed = 1.0f, .radius = 0.25f, .maxAcceleration = 1.0f},
 54 };
 55 
 56 void EnemyInit()
 57 {
 58   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 59   {
 60     enemies[i] = (Enemy){0};
 61   }
 62   enemyCount = 0;
 63 }
 64 
 65 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 66 {
 67   return enemyClassConfigs[enemy->enemyType].speed;
 68 }
 69 
 70 float EnemyGetMaxHealth(Enemy *enemy)
 71 {
 72   return enemyClassConfigs[enemy->enemyType].health;
 73 }
 74 
 75 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 76 {
 77   int16_t castleX = 0;
 78   int16_t castleY = 0;
 79   int16_t dx = castleX - currentX;
 80   int16_t dy = castleY - currentY;
 81   if (dx == 0 && dy == 0)
 82   {
 83     *nextX = currentX;
 84     *nextY = currentY;
 85     return 1;
 86   }
 87   if (abs(dx) > abs(dy))
 88   {
 89     *nextX = currentX + (dx > 0 ? 1 : -1);
 90     *nextY = currentY;
 91   }
 92   else
 93   {
 94     *nextX = currentX;
 95     *nextY = currentY + (dy > 0 ? 1 : -1);
 96   }
 97   return 0;
 98 }
 99 
100 
101 // this function predicts the movement of the unit for the next deltaT seconds
102 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
103 {
104   const float pointReachedDistance = 0.25f;
105   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
106   const float maxSimStepTime = 0.015625f;
107   
108   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
109   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
110   int16_t nextX = enemy->nextX;
111   int16_t nextY = enemy->nextY;
112   Vector2 position = enemy->simPosition;
113   int passedCount = 0;
114   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
115   {
116     float stepTime = fminf(deltaT - t, maxSimStepTime);
117     Vector2 target = (Vector2){nextX, nextY};
118     float speed = Vector2Length(*velocity);
119     // draw the target position for debugging
120     DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
121     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
122     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
123     {
124       // we reached the target position, let's move to the next waypoint
125       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
126       target = (Vector2){nextX, nextY};
127       // track how many waypoints we passed
128       passedCount++;
129     }
130     
131     // acceleration towards the target
132     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
133     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
134     *velocity = Vector2Add(*velocity, acceleration);
135 
136     // limit the speed to the maximum speed
137     if (speed > maxSpeed)
138     {
139       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
140     }
141 
142     // move the enemy
143     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
144   }
145 
146   if (waypointPassedCount)
147   {
148     (*waypointPassedCount) = passedCount;
149   }
150 
151   return position;
152 }
153 
154 void EnemyDraw()
155 {
156   for (int i = 0; i < enemyCount; i++)
157   {
158     Enemy enemy = enemies[i];
159     if (enemy.enemyType == ENEMY_TYPE_NONE)
160     {
161       continue;
162     }
163 
164     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
165     
166     if (enemy.movePathCount > 0)
167     {
168       Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
169       DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
170     }
171     for (int j = 1; j < enemy.movePathCount; j++)
172     {
173       Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
174       Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
175       DrawLine3D(p, q, GREEN);
176     }
177 
178     switch (enemy.enemyType)
179     {
180     case ENEMY_TYPE_MINION:
181       DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
182       break;
183     }
184   }
185 }
186 
187 void EnemyUpdate()
188 {
189 const float castleX = 0; 190 const float castleY = 0; 191 const float maxPathDistance2 = 0.25f * 0.25f; 192
193 for (int i = 0; i < enemyCount; i++) 194 { 195 Enemy *enemy = &enemies[i]; 196 if (enemy->enemyType == ENEMY_TYPE_NONE) 197 { 198 continue; 199 } 200 201 int waypointPassedCount = 0; 202 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount); 203 enemy->startMovingTime = gameTime.time; 204 // track path of unit 205 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2) 206 { 207 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--) 208 { 209 enemy->movePath[j] = enemy->movePath[j - 1]; 210 } 211 enemy->movePath[0] = enemy->simPosition; 212 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT) 213 { 214 enemy->movePathCount = ENEMY_MAX_PATH_COUNT; 215 } 216 } 217 218 if (waypointPassedCount > 0) 219 { 220 enemy->currentX = enemy->nextX; 221 enemy->currentY = enemy->nextY;
222 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) && 223 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
224 { 225 // enemy reached the castle; remove it 226 enemy->enemyType = ENEMY_TYPE_NONE; 227 continue; 228 } 229 } 230 231 } 232 } 233 234 EnemyId EnemyGetId(Enemy *enemy) 235 { 236 return (EnemyId){enemy - enemies, enemy->generation}; 237 } 238 239 Enemy *EnemyTryResolve(EnemyId enemyId) 240 { 241 if (enemyId.index >= ENEMY_MAX_COUNT) 242 { 243 return 0; 244 } 245 Enemy *enemy = &enemies[enemyId.index]; 246 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE) 247 { 248 return 0; 249 } 250 return enemy; 251 } 252 253 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 254 { 255 Enemy *spawn = 0; 256 for (int i = 0; i < enemyCount; i++) 257 { 258 Enemy *enemy = &enemies[i]; 259 if (enemy->enemyType == ENEMY_TYPE_NONE) 260 { 261 spawn = enemy; 262 break; 263 } 264 } 265 266 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 267 { 268 spawn = &enemies[enemyCount++]; 269 } 270 271 if (spawn) 272 { 273 spawn->currentX = currentX; 274 spawn->currentY = currentY; 275 spawn->nextX = currentX; 276 spawn->nextY = currentY; 277 spawn->simPosition = (Vector2){currentX, currentY}; 278 spawn->simVelocity = (Vector2){0, 0}; 279 spawn->enemyType = enemyType; 280 spawn->startMovingTime = gameTime.time; 281 spawn->damage = 0.0f; 282 spawn->futureDamage = 0.0f; 283 spawn->generation++; 284 spawn->movePathCount = 0; 285 } 286 287 return spawn; 288 } 289 290 int EnemyAddDamage(Enemy *enemy, float damage) 291 { 292 enemy->damage += damage; 293 if (enemy->damage >= EnemyGetMaxHealth(enemy)) 294 { 295 enemy->enemyType = ENEMY_TYPE_NONE; 296 return 1; 297 } 298 299 return 0; 300 } 301 302 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 303 { 304 int16_t castleX = 0; 305 int16_t castleY = 0; 306 Enemy* closest = 0; 307 int16_t closestDistance = 0; 308 float range2 = range * range; 309 for (int i = 0; i < enemyCount; i++) 310 { 311 Enemy* enemy = &enemies[i]; 312 if (enemy->enemyType == ENEMY_TYPE_NONE) 313 { 314 continue; 315 } 316 float maxHealth = EnemyGetMaxHealth(enemy); 317 if (enemy->futureDamage >= maxHealth) 318 { 319 // ignore enemies that will die soon 320 continue; 321 } 322 int16_t dx = castleX - enemy->currentX; 323 int16_t dy = castleY - enemy->currentY; 324 int16_t distance = abs(dx) + abs(dy); 325 if (!closest || distance < closestDistance) 326 { 327 float tdx = towerX - enemy->currentX; 328 float tdy = towerY - enemy->currentY; 329 float tdistance2 = tdx * tdx + tdy * tdy; 330 if (tdistance2 <= range2) 331 { 332 closest = enemy; 333 closestDistance = distance; 334 } 335 } 336 } 337 return closest; 338 } 339 340 int EnemyCount() 341 { 342 int count = 0; 343 for (int i = 0; i < enemyCount; i++) 344 { 345 if (enemies[i].enemyType != ENEMY_TYPE_NONE) 346 { 347 count++; 348 } 349 } 350 return count; 351 } 352 353 //# Projectiles 354 #define PROJECTILE_MAX_COUNT 1200 355 #define PROJECTILE_TYPE_NONE 0 356 #define PROJECTILE_TYPE_BULLET 1 357 358 typedef struct Projectile 359 { 360 uint8_t projectileType; 361 float shootTime; 362 float arrivalTime; 363 float damage; 364 Vector2 position; 365 Vector2 target; 366 Vector2 directionNormal; 367 EnemyId targetEnemy; 368 } Projectile; 369 370 Projectile projectiles[PROJECTILE_MAX_COUNT]; 371 int projectileCount = 0; 372 373 void ProjectileInit() 374 { 375 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 376 { 377 projectiles[i] = (Projectile){0}; 378 } 379 } 380 381 void ProjectileDraw() 382 { 383 for (int i = 0; i < projectileCount; i++) 384 { 385 Projectile projectile = projectiles[i]; 386 if (projectile.projectileType == PROJECTILE_TYPE_NONE) 387 { 388 continue; 389 } 390 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime); 391 if (transition >= 1.0f) 392 { 393 continue; 394 } 395 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition); 396 float x = position.x; 397 float y = position.y; 398 float dx = projectile.directionNormal.x; 399 float dy = projectile.directionNormal.y; 400 for (float d = 1.0f; d > 0.0f; d -= 0.25f) 401 { 402 x -= dx * 0.1f; 403 y -= dy * 0.1f; 404 float size = 0.1f * d; 405 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED); 406 } 407 } 408 } 409 410 void ProjectileUpdate() 411 { 412 for (int i = 0; i < projectileCount; i++) 413 { 414 Projectile *projectile = &projectiles[i]; 415 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 416 { 417 continue; 418 } 419 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime); 420 if (transition >= 1.0f) 421 { 422 projectile->projectileType = PROJECTILE_TYPE_NONE; 423 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy); 424 if (enemy) 425 { 426 EnemyAddDamage(enemy, projectile->damage); 427 } 428 continue; 429 } 430 } 431 } 432 433 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage) 434 { 435 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 436 { 437 Projectile *projectile = &projectiles[i]; 438 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 439 { 440 projectile->projectileType = projectileType; 441 projectile->shootTime = gameTime.time; 442 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed; 443 projectile->damage = damage; 444 projectile->position = position; 445 projectile->target = target; 446 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position)); 447 projectile->targetEnemy = EnemyGetId(enemy); 448 projectileCount = projectileCount <= i ? i + 1 : projectileCount; 449 return projectile; 450 } 451 } 452 return 0; 453 } 454 455 //# Towers 456 457 #define TOWER_MAX_COUNT 400 458 #define TOWER_TYPE_NONE 0 459 #define TOWER_TYPE_BASE 1 460 #define TOWER_TYPE_GUN 2 461 462 typedef struct Tower 463 { 464 int16_t x, y; 465 uint8_t towerType; 466 float cooldown; 467 } Tower; 468 469 Tower towers[TOWER_MAX_COUNT]; 470 int towerCount = 0; 471 472 void TowerInit() 473 { 474 for (int i = 0; i < TOWER_MAX_COUNT; i++) 475 { 476 towers[i] = (Tower){0}; 477 } 478 towerCount = 0; 479 } 480 481 Tower *TowerGetAt(int16_t x, int16_t y) 482 { 483 for (int i = 0; i < towerCount; i++) 484 { 485 if (towers[i].x == x && towers[i].y == y) 486 { 487 return &towers[i]; 488 } 489 } 490 return 0; 491 } 492 493 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 494 { 495 if (towerCount >= TOWER_MAX_COUNT) 496 { 497 return 0; 498 } 499 500 Tower *tower = TowerGetAt(x, y); 501 if (tower) 502 { 503 return 0; 504 } 505 506 tower = &towers[towerCount++]; 507 tower->x = x; 508 tower->y = y; 509 tower->towerType = towerType; 510 return tower; 511 } 512 513 void TowerDraw() 514 { 515 for (int i = 0; i < towerCount; i++) 516 { 517 Tower tower = towers[i]; 518 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 519 switch (tower.towerType) 520 { 521 case TOWER_TYPE_BASE: 522 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 523 break; 524 case TOWER_TYPE_GUN: 525 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 526 break; 527 } 528 } 529 } 530 531 void TowerGunUpdate(Tower *tower) 532 { 533 if (tower->cooldown <= 0) 534 { 535 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 536 if (enemy) 537 { 538 tower->cooldown = 0.25f; 539 // shoot the enemy; determine future position of the enemy 540 float bulletSpeed = 1.0f; 541 float bulletDamage = 3.0f; 542 Vector2 velocity = enemy->simVelocity; 543 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 544 Vector2 towerPosition = {tower->x, tower->y}; 545 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 546 for (int i = 0; i < 8; i++) { 547 velocity = enemy->simVelocity; 548 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 549 float distance = Vector2Distance(towerPosition, futurePosition); 550 float eta2 = distance / bulletSpeed; 551 if (fabs(eta - eta2) < 0.01f) { 552 break; 553 } 554 eta = (eta2 + eta) * 0.5f; 555 } 556 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition, 557 bulletSpeed, bulletDamage); 558 enemy->futureDamage += bulletDamage; 559 } 560 } 561 else 562 { 563 tower->cooldown -= gameTime.deltaTime; 564 } 565 } 566 567 void TowerUpdate() 568 { 569 for (int i = 0; i < towerCount; i++) 570 { 571 Tower *tower = &towers[i]; 572 switch (tower->towerType) 573 { 574 case TOWER_TYPE_GUN: 575 TowerGunUpdate(tower); 576 break; 577 } 578 } 579 } 580 581 //# Game 582 583 float nextSpawnTime = 0.0f; 584 585 void InitGame() 586 { 587 TowerInit(); 588 EnemyInit(); 589 ProjectileInit(); 590 591 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 592 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 593 TowerTryAdd(TOWER_TYPE_GUN, -2, 0); 594 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4); 595 } 596 597 void GameUpdate() 598 { 599 float dt = GetFrameTime(); 600 // cap maximum delta time to 0.1 seconds to prevent large time steps 601 if (dt > 0.1f) dt = 0.1f; 602 gameTime.time += dt; 603 gameTime.deltaTime = dt; 604 EnemyUpdate(); 605 TowerUpdate(); 606 ProjectileUpdate(); 607 608 // spawn a new enemy every second 609 if (gameTime.time >= nextSpawnTime && EnemyCount() < 10) 610 { 611 nextSpawnTime = gameTime.time + 1.0f; 612 // add a new enemy at the boundary of the map 613 int randValue = GetRandomValue(-5, 5); 614 int randSide = GetRandomValue(0, 3); 615 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue; 616 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue; 617 static int alternation = 0; 618 alternation += 1; 619 if (alternation % 3 == 0) { 620 EnemyTryAdd(ENEMY_TYPE_MINION, 0, -5); 621 } 622 else if (alternation % 3 == 1) 623 { 624 EnemyTryAdd(ENEMY_TYPE_MINION, 0, 5); 625 } 626 EnemyTryAdd(ENEMY_TYPE_MINION, x, y); 627 } 628 } 629 630 int main(void) 631 { 632 int screenWidth, screenHeight; 633 GetPreferredSize(&screenWidth, &screenHeight); 634 InitWindow(screenWidth, screenHeight, "Tower defense"); 635 SetTargetFPS(30); 636 637 Camera3D camera = {0}; 638 camera.position = (Vector3){0.0f, 10.0f, 0.0f}; 639 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 640 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 641 camera.fovy = 10.0f; 642 camera.projection = CAMERA_ORTHOGRAPHIC; 643 644 InitGame(); 645 646 while (!WindowShouldClose()) 647 { 648 if (IsPaused()) { 649 // canvas is not visible in browser - do nothing 650 continue; 651 } 652 BeginDrawing(); 653 ClearBackground(DARKBLUE); 654 655 BeginMode3D(camera); 656 DrawGrid(10, 1.0f); 657 TowerDraw(); 658 EnemyDraw(); 659 ProjectileDraw(); 660 GameUpdate(); 661 EndMode3D(); 662 663 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 664 EndDrawing(); 665 } 666 667 CloseWindow(); 668 669 return 0; 670 }
  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 #endif
  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

Now that the movement is physics based, we can use this to introduce some interesting effects.

The first thing to add is to add enemy collision detection and handling. When two enemy circles overlap, we'll move them apart in a way that they don't overlap (so much) anymore.

Why don't we resolve the collision perfectly? This works well for 2 colliding circles, but when we have 3 circles that are in proximity, this can easily lead to oscillating behavior: A collides with B, so B is moved away. Now B collides with C, so C moves B back again, leading to a collision with A. The more objects are involved in this process, the more jittery it becomes. If we however fix the collision by moving the object 50% of the full way, this is balancing out way smoother. This may be difficult to understand when reading it, but here's how it looks like by comparison:

The left side shows the effect when resolving the collision perfectly. We can see how it permanently jitters because of the chain of collisions resolving steps explained above.

The right side is using the 50% resolution and we can see how the objects are moving towards the center but come to a quite stable rest. The repulsion is in balance with the attraction force towards the center. And even though we correct the error by only 50%, there's not much overlapping. And the 100% collision resolution is not even achieving a perfect result - the objects are still overlapping!

There are many strategies to handle such kind of problems, but as a general rule, you can take away this:

Simulating stiffness is difficult and expensive. Especially when strong forces (=much movement per frame) and many objects are involved.

Back to our code: To see if it is working, we will increase the number of enemies spawned while also increasing the shooting frequency of the towers:

  • 💾
  1 #include "td-tut-2-main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 typedef struct GameTime
  7 {
  8   float time;
  9   float deltaTime;
 10 } GameTime;
 11 
 12 GameTime gameTime = {0};
 13 
 14 //# Enemies
 15 
 16 #define ENEMY_MAX_PATH_COUNT 8
 17 #define ENEMY_MAX_COUNT 400
 18 #define ENEMY_TYPE_NONE 0
 19 #define ENEMY_TYPE_MINION 1
 20 
 21 typedef struct EnemyId
 22 {
 23   uint16_t index;
 24   uint16_t generation;
 25 } EnemyId;
 26 
 27 typedef struct EnemyClassConfig
 28 {
 29   float speed;
 30   float health;
 31   float radius;
 32   float maxAcceleration;
 33 } EnemyClassConfig;
 34 
 35 typedef struct Enemy
 36 {
 37   int16_t currentX, currentY;
 38   int16_t nextX, nextY;
 39   Vector2 simPosition;
 40   Vector2 simVelocity;
 41   uint16_t generation;
 42   float startMovingTime;
 43   float damage, futureDamage;
 44   uint8_t enemyType;
 45   uint8_t movePathCount;
 46   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
 47 } Enemy;
 48 
 49 Enemy enemies[ENEMY_MAX_COUNT];
 50 int enemyCount = 0;
 51 
 52 EnemyClassConfig enemyClassConfigs[] = {
 53     [ENEMY_TYPE_MINION] = {.health = 3.0f, .speed = 1.0f, .radius = 0.25f, .maxAcceleration = 1.0f},
 54 };
 55 
 56 void EnemyInit()
 57 {
 58   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 59   {
 60     enemies[i] = (Enemy){0};
 61   }
 62   enemyCount = 0;
 63 }
 64 
 65 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 66 {
 67   return enemyClassConfigs[enemy->enemyType].speed;
 68 }
 69 
 70 float EnemyGetMaxHealth(Enemy *enemy)
 71 {
 72   return enemyClassConfigs[enemy->enemyType].health;
 73 }
 74 
 75 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 76 {
 77   int16_t castleX = 0;
 78   int16_t castleY = 0;
 79   int16_t dx = castleX - currentX;
 80   int16_t dy = castleY - currentY;
 81   if (dx == 0 && dy == 0)
 82   {
 83     *nextX = currentX;
 84     *nextY = currentY;
 85     return 1;
 86   }
 87   if (abs(dx) > abs(dy))
 88   {
 89     *nextX = currentX + (dx > 0 ? 1 : -1);
 90     *nextY = currentY;
 91   }
 92   else
 93   {
 94     *nextX = currentX;
 95     *nextY = currentY + (dy > 0 ? 1 : -1);
 96   }
 97   return 0;
 98 }
 99 
100 
101 // this function predicts the movement of the unit for the next deltaT seconds
102 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
103 {
104   const float pointReachedDistance = 0.25f;
105   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
106   const float maxSimStepTime = 0.015625f;
107   
108   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
109   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
110   int16_t nextX = enemy->nextX;
111   int16_t nextY = enemy->nextY;
112   Vector2 position = enemy->simPosition;
113   int passedCount = 0;
114   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
115   {
116     float stepTime = fminf(deltaT - t, maxSimStepTime);
117     Vector2 target = (Vector2){nextX, nextY};
118     float speed = Vector2Length(*velocity);
119     // draw the target position for debugging
120     DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
121     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
122     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
123     {
124       // we reached the target position, let's move to the next waypoint
125       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
126       target = (Vector2){nextX, nextY};
127       // track how many waypoints we passed
128       passedCount++;
129     }
130     
131     // acceleration towards the target
132     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
133     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
134     *velocity = Vector2Add(*velocity, acceleration);
135 
136     // limit the speed to the maximum speed
137     if (speed > maxSpeed)
138     {
139       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
140     }
141 
142     // move the enemy
143     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
144   }
145 
146   if (waypointPassedCount)
147   {
148     (*waypointPassedCount) = passedCount;
149   }
150 
151   return position;
152 }
153 
154 void EnemyDraw()
155 {
156   for (int i = 0; i < enemyCount; i++)
157   {
158     Enemy enemy = enemies[i];
159     if (enemy.enemyType == ENEMY_TYPE_NONE)
160     {
161       continue;
162     }
163 
164     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
165     
166     if (enemy.movePathCount > 0)
167     {
168       Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
169       DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
170     }
171     for (int j = 1; j < enemy.movePathCount; j++)
172     {
173       Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
174       Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
175       DrawLine3D(p, q, GREEN);
176     }
177 
178     switch (enemy.enemyType)
179     {
180     case ENEMY_TYPE_MINION:
181       DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
182       break;
183     }
184   }
185 }
186 
187 void EnemyUpdate()
188 {
189   const float castleX = 0;
190   const float castleY = 0;
191   const float maxPathDistance2 = 0.25f * 0.25f;
192   
193   for (int i = 0; i < enemyCount; i++)
194   {
195     Enemy *enemy = &enemies[i];
196     if (enemy->enemyType == ENEMY_TYPE_NONE)
197     {
198       continue;
199     }
200 
201     int waypointPassedCount = 0;
202     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
203     enemy->startMovingTime = gameTime.time;
204     // track path of unit
205     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
206     {
207       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
208       {
209         enemy->movePath[j] = enemy->movePath[j - 1];
210       }
211       enemy->movePath[0] = enemy->simPosition;
212       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
213       {
214         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
215       }
216     }
217 
218     if (waypointPassedCount > 0)
219     {
220       enemy->currentX = enemy->nextX;
221       enemy->currentY = enemy->nextY;
222       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
223         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
224       {
225         // enemy reached the castle; remove it
226         enemy->enemyType = ENEMY_TYPE_NONE;
227         continue;
228       }
229     }
230 } 231 232 // handle collisions 233 for (int i = 0; i < enemyCount - 1; i++) 234 { 235 Enemy *enemyA = &enemies[i]; 236 if (enemyA->enemyType == ENEMY_TYPE_NONE) 237 { 238 continue; 239 } 240 for (int j = i + 1; j < enemyCount; j++) 241 { 242 Enemy *enemyB = &enemies[j]; 243 if (enemyB->enemyType == ENEMY_TYPE_NONE) 244 { 245 continue; 246 } 247 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition); 248 float radiusA = enemyClassConfigs[enemyA->enemyType].radius; 249 float radiusB = enemyClassConfigs[enemyB->enemyType].radius; 250 float radiusSum = radiusA + radiusB; 251 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f) 252 { 253 // collision 254 float distance = sqrtf(distanceSqr); 255 float overlap = radiusSum - distance; 256 // move the enemies apart, but softly; if we have a clog of enemies, 257 // moving them perfectly apart can cause them to jitter 258 float positionCorrection = overlap / 5.0f; 259 Vector2 direction = (Vector2){ 260 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection, 261 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection}; 262 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction); 263 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction); 264 } 265 }
266 } 267 } 268 269 EnemyId EnemyGetId(Enemy *enemy) 270 { 271 return (EnemyId){enemy - enemies, enemy->generation}; 272 } 273 274 Enemy *EnemyTryResolve(EnemyId enemyId) 275 { 276 if (enemyId.index >= ENEMY_MAX_COUNT) 277 { 278 return 0; 279 } 280 Enemy *enemy = &enemies[enemyId.index]; 281 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE) 282 { 283 return 0; 284 } 285 return enemy; 286 } 287 288 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 289 { 290 Enemy *spawn = 0; 291 for (int i = 0; i < enemyCount; i++) 292 { 293 Enemy *enemy = &enemies[i]; 294 if (enemy->enemyType == ENEMY_TYPE_NONE) 295 { 296 spawn = enemy; 297 break; 298 } 299 } 300 301 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 302 { 303 spawn = &enemies[enemyCount++]; 304 } 305 306 if (spawn) 307 { 308 spawn->currentX = currentX; 309 spawn->currentY = currentY; 310 spawn->nextX = currentX; 311 spawn->nextY = currentY; 312 spawn->simPosition = (Vector2){currentX, currentY}; 313 spawn->simVelocity = (Vector2){0, 0}; 314 spawn->enemyType = enemyType; 315 spawn->startMovingTime = gameTime.time; 316 spawn->damage = 0.0f; 317 spawn->futureDamage = 0.0f; 318 spawn->generation++; 319 spawn->movePathCount = 0; 320 } 321 322 return spawn; 323 } 324 325 int EnemyAddDamage(Enemy *enemy, float damage) 326 { 327 enemy->damage += damage; 328 if (enemy->damage >= EnemyGetMaxHealth(enemy)) 329 { 330 enemy->enemyType = ENEMY_TYPE_NONE; 331 return 1; 332 } 333 334 return 0; 335 } 336 337 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 338 { 339 int16_t castleX = 0; 340 int16_t castleY = 0; 341 Enemy* closest = 0; 342 int16_t closestDistance = 0; 343 float range2 = range * range; 344 for (int i = 0; i < enemyCount; i++) 345 { 346 Enemy* enemy = &enemies[i]; 347 if (enemy->enemyType == ENEMY_TYPE_NONE) 348 { 349 continue; 350 } 351 float maxHealth = EnemyGetMaxHealth(enemy); 352 if (enemy->futureDamage >= maxHealth) 353 { 354 // ignore enemies that will die soon 355 continue; 356 } 357 int16_t dx = castleX - enemy->currentX; 358 int16_t dy = castleY - enemy->currentY; 359 int16_t distance = abs(dx) + abs(dy); 360 if (!closest || distance < closestDistance) 361 { 362 float tdx = towerX - enemy->currentX; 363 float tdy = towerY - enemy->currentY; 364 float tdistance2 = tdx * tdx + tdy * tdy; 365 if (tdistance2 <= range2) 366 { 367 closest = enemy; 368 closestDistance = distance; 369 } 370 } 371 } 372 return closest; 373 } 374 375 int EnemyCount() 376 { 377 int count = 0; 378 for (int i = 0; i < enemyCount; i++) 379 { 380 if (enemies[i].enemyType != ENEMY_TYPE_NONE) 381 { 382 count++; 383 } 384 } 385 return count; 386 } 387 388 //# Projectiles 389 #define PROJECTILE_MAX_COUNT 1200 390 #define PROJECTILE_TYPE_NONE 0 391 #define PROJECTILE_TYPE_BULLET 1 392 393 typedef struct Projectile 394 { 395 uint8_t projectileType; 396 float shootTime; 397 float arrivalTime; 398 float damage; 399 Vector2 position; 400 Vector2 target; 401 Vector2 directionNormal; 402 EnemyId targetEnemy; 403 } Projectile; 404 405 Projectile projectiles[PROJECTILE_MAX_COUNT]; 406 int projectileCount = 0; 407 408 void ProjectileInit() 409 { 410 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 411 { 412 projectiles[i] = (Projectile){0}; 413 } 414 } 415 416 void ProjectileDraw() 417 { 418 for (int i = 0; i < projectileCount; i++) 419 { 420 Projectile projectile = projectiles[i]; 421 if (projectile.projectileType == PROJECTILE_TYPE_NONE) 422 { 423 continue; 424 } 425 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime); 426 if (transition >= 1.0f) 427 { 428 continue; 429 } 430 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition); 431 float x = position.x; 432 float y = position.y; 433 float dx = projectile.directionNormal.x; 434 float dy = projectile.directionNormal.y; 435 for (float d = 1.0f; d > 0.0f; d -= 0.25f) 436 { 437 x -= dx * 0.1f; 438 y -= dy * 0.1f; 439 float size = 0.1f * d; 440 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED); 441 } 442 } 443 } 444 445 void ProjectileUpdate() 446 { 447 for (int i = 0; i < projectileCount; i++) 448 { 449 Projectile *projectile = &projectiles[i]; 450 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 451 { 452 continue; 453 } 454 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime); 455 if (transition >= 1.0f) 456 { 457 projectile->projectileType = PROJECTILE_TYPE_NONE; 458 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy); 459 if (enemy) 460 { 461 EnemyAddDamage(enemy, projectile->damage); 462 } 463 continue; 464 } 465 } 466 } 467 468 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage) 469 { 470 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 471 { 472 Projectile *projectile = &projectiles[i]; 473 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 474 { 475 projectile->projectileType = projectileType; 476 projectile->shootTime = gameTime.time; 477 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed; 478 projectile->damage = damage; 479 projectile->position = position; 480 projectile->target = target; 481 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position)); 482 projectile->targetEnemy = EnemyGetId(enemy); 483 projectileCount = projectileCount <= i ? i + 1 : projectileCount; 484 return projectile; 485 } 486 } 487 return 0; 488 } 489 490 //# Towers 491 492 #define TOWER_MAX_COUNT 400 493 #define TOWER_TYPE_NONE 0 494 #define TOWER_TYPE_BASE 1 495 #define TOWER_TYPE_GUN 2 496 497 typedef struct Tower 498 { 499 int16_t x, y; 500 uint8_t towerType; 501 float cooldown; 502 } Tower; 503 504 Tower towers[TOWER_MAX_COUNT]; 505 int towerCount = 0; 506 507 void TowerInit() 508 { 509 for (int i = 0; i < TOWER_MAX_COUNT; i++) 510 { 511 towers[i] = (Tower){0}; 512 } 513 towerCount = 0; 514 } 515 516 Tower *TowerGetAt(int16_t x, int16_t y) 517 { 518 for (int i = 0; i < towerCount; i++) 519 { 520 if (towers[i].x == x && towers[i].y == y) 521 { 522 return &towers[i]; 523 } 524 } 525 return 0; 526 } 527 528 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 529 { 530 if (towerCount >= TOWER_MAX_COUNT) 531 { 532 return 0; 533 } 534 535 Tower *tower = TowerGetAt(x, y); 536 if (tower) 537 { 538 return 0; 539 } 540 541 tower = &towers[towerCount++]; 542 tower->x = x; 543 tower->y = y; 544 tower->towerType = towerType; 545 return tower; 546 } 547 548 void TowerDraw() 549 { 550 for (int i = 0; i < towerCount; i++) 551 { 552 Tower tower = towers[i]; 553 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 554 switch (tower.towerType) 555 { 556 case TOWER_TYPE_BASE: 557 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 558 break; 559 case TOWER_TYPE_GUN: 560 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 561 break; 562 } 563 } 564 } 565 566 void TowerGunUpdate(Tower *tower) 567 { 568 if (tower->cooldown <= 0) 569 { 570 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 571 if (enemy) 572 {
573 tower->cooldown = 0.125f;
574 // shoot the enemy; determine future position of the enemy 575 float bulletSpeed = 1.0f; 576 float bulletDamage = 3.0f; 577 Vector2 velocity = enemy->simVelocity; 578 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 579 Vector2 towerPosition = {tower->x, tower->y}; 580 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 581 for (int i = 0; i < 8; i++) { 582 velocity = enemy->simVelocity; 583 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 584 float distance = Vector2Distance(towerPosition, futurePosition); 585 float eta2 = distance / bulletSpeed; 586 if (fabs(eta - eta2) < 0.01f) { 587 break; 588 } 589 eta = (eta2 + eta) * 0.5f; 590 } 591 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition, 592 bulletSpeed, bulletDamage); 593 enemy->futureDamage += bulletDamage; 594 } 595 } 596 else 597 { 598 tower->cooldown -= gameTime.deltaTime; 599 } 600 } 601 602 void TowerUpdate() 603 { 604 for (int i = 0; i < towerCount; i++) 605 { 606 Tower *tower = &towers[i]; 607 switch (tower->towerType) 608 { 609 case TOWER_TYPE_GUN: 610 TowerGunUpdate(tower); 611 break; 612 } 613 } 614 } 615 616 //# Game 617 618 float nextSpawnTime = 0.0f; 619 620 void InitGame() 621 { 622 TowerInit(); 623 EnemyInit(); 624 ProjectileInit(); 625 626 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 627 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 628 TowerTryAdd(TOWER_TYPE_GUN, -2, 0); 629 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4); 630 } 631 632 void GameUpdate() 633 { 634 float dt = GetFrameTime(); 635 // cap maximum delta time to 0.1 seconds to prevent large time steps 636 if (dt > 0.1f) dt = 0.1f; 637 gameTime.time += dt; 638 gameTime.deltaTime = dt; 639 EnemyUpdate(); 640 TowerUpdate(); 641 ProjectileUpdate(); 642 643 // spawn a new enemy every second
644 if (gameTime.time >= nextSpawnTime && EnemyCount() < 50)
645 {
646 nextSpawnTime = gameTime.time + 0.2f;
647 // add a new enemy at the boundary of the map 648 int randValue = GetRandomValue(-5, 5); 649 int randSide = GetRandomValue(0, 3); 650 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue; 651 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue; 652 static int alternation = 0; 653 alternation += 1; 654 if (alternation % 3 == 0) { 655 EnemyTryAdd(ENEMY_TYPE_MINION, 0, -5); 656 } 657 else if (alternation % 3 == 1) 658 { 659 EnemyTryAdd(ENEMY_TYPE_MINION, 0, 5); 660 } 661 EnemyTryAdd(ENEMY_TYPE_MINION, x, y); 662 } 663 } 664 665 int main(void) 666 { 667 int screenWidth, screenHeight; 668 GetPreferredSize(&screenWidth, &screenHeight); 669 InitWindow(screenWidth, screenHeight, "Tower defense"); 670 SetTargetFPS(30); 671 672 Camera3D camera = {0};
673 camera.position = (Vector3){0.0f, 10.0f, -0.5f}; 674 camera.target = (Vector3){0.0f, 0.0f, -0.5f};
675 camera.up = (Vector3){0.0f, 0.0f, -1.0f};
676 camera.fovy = 12.0f;
677 camera.projection = CAMERA_ORTHOGRAPHIC; 678 679 InitGame(); 680 681 while (!WindowShouldClose()) 682 { 683 if (IsPaused()) { 684 // canvas is not visible in browser - do nothing 685 continue; 686 } 687 BeginDrawing(); 688 ClearBackground(DARKBLUE); 689 690 BeginMode3D(camera); 691 DrawGrid(10, 1.0f); 692 TowerDraw(); 693 EnemyDraw(); 694 ProjectileDraw(); 695 GameUpdate(); 696 EndMode3D(); 697
698 const char *title = "Tower defense tutorial"; 699 int titleWidth = MeasureText(title, 20); 700 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f + 2, 5 + 2, 20, BLACK); 701 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f, 5, 20, WHITE);
702 EndDrawing(); 703 } 704 705 CloseWindow(); 706 707 return 0; 708 }
  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 #endif
  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

Wrapup

In this part, we've improved the shooting algorithm to predict enemy positions. The enemies have now health and the movement is now physics based. Moreover, the enemies are now colliding with each other and we can see the path they are taking.

In the next part, we will add path finding for the enemies. I will publish it on the 29th of November.

🍪