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:
- Handling health and damage
- Refining the shooting algorithm to predict enemy positions
- Refining the enemy movement to be more smooth
- Adding collision detection between enemies
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:
- Make initial guess for enemy position at projectile impact time (use t=0)
- Calculate estimated projectile time of arrival (eta) for that position
- Calculate enemy position for given projectile eta
- The 2 eta times will now differ; create the average, use it as new eta and repeat for a few times
- We can stop after a fixed amount of iterations or when the difference becomes small
Note: When the enemy is moving away at significant speed compared to the bullet (or even faster), this approach will not work well. The same is true for complex enemy movements - but as long as we don't intend to have these in our game, this should be fine.
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:
- Smooth movement around corners
- Acceleration and deceleration
- Collision between enemies
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:
- Change the velocity so we will move towards the next waypoint
- Limit the velocity to the maximum speed of the unit
- Add the velocity to the current position
- Check if we reached the next waypoint and if we reached it, move on to the next one
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:
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.