Simple tower defense tutorial, part 4
As like with the previous parts, let's first have a look at the current state of the game:
To summarize the current state:
- The enemies spawn and seek their way to the castle
- The towers shoot at the enemies
- Wall towers (and any other tower) blocks the enemies
What we want to add next is that enemies attack buildings and that we can build new buildings via a simple interface. We will start with the first part: Enemies attacking buildings.
Enemies attacking buildings
A simple take on the problem could be to let enemies "explode" when they touch buildings, causing damage. That sort of invalidates the wall collisions we implemented in the last part, but maybe other units will be able reuse it.
So next we'll add building health and exploding enemies. When a building's damage reaches the health, the building is destroyed (line 472 + 634):
1 #include "td-tut-2-main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Declarations
7
8 #define TOWER_MAX_COUNT 400
9 #define TOWER_TYPE_NONE 0
10 #define TOWER_TYPE_BASE 1
11 #define TOWER_TYPE_GUN 2
12 #define TOWER_TYPE_WALL 3
13
14 typedef struct Tower
15 {
16 int16_t x, y;
17 uint8_t towerType;
18 float cooldown;
19 float damage;
20 } Tower;
21
22 typedef struct GameTime
23 {
24 float time;
25 float deltaTime;
26 } GameTime;
27
28 GameTime gameTime = {0};
29
30 Tower towers[TOWER_MAX_COUNT];
31 int towerCount = 0;
32
33 float TowerGetMaxHealth(Tower *tower);
34
35 //# Pathfinding map
36 typedef struct DeltaSrc
37 {
38 char x, y;
39 } DeltaSrc;
40
41 typedef struct PathfindingMap
42 {
43 int width, height;
44 float scale;
45 float *distances;
46 long *towerIndex;
47 DeltaSrc *deltaSrc;
48 float maxDistance;
49 Matrix toMapSpace;
50 Matrix toWorldSpace;
51 } PathfindingMap;
52
53 // when we execute the pathfinding algorithm, we need to store the active nodes
54 // in a queue. Each node has a position, a distance from the start, and the
55 // position of the node that we came from.
56 typedef struct PathfindingNode
57 {
58 int16_t x, y, fromX, fromY;
59 float distance;
60 } PathfindingNode;
61
62 // The queue is a simple array of nodes, we add nodes to the end and remove
63 // nodes from the front. We keep the array around to avoid unnecessary allocations
64 static PathfindingNode *pathfindingNodeQueue = 0;
65 static int pathfindingNodeQueueCount = 0;
66 static int pathfindingNodeQueueCapacity = 0;
67
68 // The pathfinding map stores the distances from the castle to each cell in the map.
69 PathfindingMap pathfindingMap = {0};
70
71 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
72 {
73 // transforming between map space and world space allows us to adapt
74 // position and scale of the map without changing the pathfinding data
75 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
76 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
77 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
78 pathfindingMap.width = width;
79 pathfindingMap.height = height;
80 pathfindingMap.scale = scale;
81 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
82 for (int i = 0; i < width * height; i++)
83 {
84 pathfindingMap.distances[i] = -1.0f;
85 }
86
87 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
88 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
89 }
90
91 float PathFindingGetDistance(int mapX, int mapY)
92 {
93 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
94 {
95 // when outside the map, we return the manhattan distance to the castle (0,0)
96 return fabsf((float)mapX) + fabsf((float)mapY);
97 }
98
99 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
100 }
101
102 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
103 {
104 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
105 {
106 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
107 // we use MemAlloc/MemRealloc to allocate memory for the queue
108 // I am not entirely sure if MemRealloc allows passing a null pointer
109 // so we check if the pointer is null and use MemAlloc in that case
110 if (pathfindingNodeQueue == 0)
111 {
112 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
113 }
114 else
115 {
116 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
117 }
118 }
119
120 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
121 node->x = x;
122 node->y = y;
123 node->fromX = fromX;
124 node->fromY = fromY;
125 node->distance = distance;
126 }
127
128 PathfindingNode *PathFindingNodePop()
129 {
130 if (pathfindingNodeQueueCount == 0)
131 {
132 return 0;
133 }
134 // we return the first node in the queue; we want to return a pointer to the node
135 // so we can return 0 if the queue is empty.
136 // We should _not_ return a pointer to the element in the list, because the list
137 // may be reallocated and the pointer would become invalid. Or the
138 // popped element is overwritten by the next push operation.
139 // Using static here means that the variable is permanently allocated.
140 static PathfindingNode node;
141 node = pathfindingNodeQueue[0];
142 // we shift all nodes one position to the front
143 for (int i = 1; i < pathfindingNodeQueueCount; i++)
144 {
145 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
146 }
147 --pathfindingNodeQueueCount;
148 return &node;
149 }
150
151 // transform a world position to a map position in the array;
152 // returns true if the position is inside the map
153 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
154 {
155 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
156 *mapX = (int16_t)mapPosition.x;
157 *mapY = (int16_t)mapPosition.z;
158 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
159 }
160
161 void PathFindingMapUpdate()
162 {
163 const int castleX = 0, castleY = 0;
164 int16_t castleMapX, castleMapY;
165 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
166 {
167 return;
168 }
169 int width = pathfindingMap.width, height = pathfindingMap.height;
170
171 // reset the distances to -1
172 for (int i = 0; i < width * height; i++)
173 {
174 pathfindingMap.distances[i] = -1.0f;
175 }
176 // reset the tower indices
177 for (int i = 0; i < width * height; i++)
178 {
179 pathfindingMap.towerIndex[i] = -1;
180 }
181 // reset the delta src
182 for (int i = 0; i < width * height; i++)
183 {
184 pathfindingMap.deltaSrc[i].x = 0;
185 pathfindingMap.deltaSrc[i].y = 0;
186 }
187
188 for (int i = 0; i < towerCount; i++)
189 {
190 Tower *tower = &towers[i];
191 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
192 {
193 continue;
194 }
195 int16_t mapX, mapY;
196 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
197 // this would not work correctly and needs to be refined to allow towers covering multiple cells
198 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
199 // one cell. For now.
200 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
201 {
202 continue;
203 }
204 int index = mapY * width + mapX;
205 pathfindingMap.towerIndex[index] = i;
206 }
207
208 // we start at the castle and add the castle to the queue
209 pathfindingMap.maxDistance = 0.0f;
210 pathfindingNodeQueueCount = 0;
211 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
212 PathfindingNode *node = 0;
213 while ((node = PathFindingNodePop()))
214 {
215 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
216 {
217 continue;
218 }
219 int index = node->y * width + node->x;
220 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
221 {
222 continue;
223 }
224
225 int deltaX = node->x - node->fromX;
226 int deltaY = node->y - node->fromY;
227 // even if the cell is blocked by a tower, we still may want to store the direction
228 // (though this might not be needed, IDK right now)
229 pathfindingMap.deltaSrc[index].x = (char) deltaX;
230 pathfindingMap.deltaSrc[index].y = (char) deltaY;
231
232 // we skip nodes that are blocked by towers
233 if (pathfindingMap.towerIndex[index] >= 0)
234 {
235 node->distance += 8.0f;
236 }
237 pathfindingMap.distances[index] = node->distance;
238 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
239 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
240 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
241 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
242 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
243 }
244 }
245
246 void PathFindingMapDraw()
247 {
248 float cellSize = pathfindingMap.scale * 0.9f;
249 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
250 for (int x = 0; x < pathfindingMap.width; x++)
251 {
252 for (int y = 0; y < pathfindingMap.height; y++)
253 {
254 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
255 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
256 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
257 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
258 // animate the distance "wave" to show how the pathfinding algorithm expands
259 // from the castle
260 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
261 {
262 color = BLACK;
263 }
264 DrawCube(position, cellSize, 0.1f, cellSize, color);
265 }
266 }
267 }
268
269 Vector2 PathFindingGetGradient(Vector3 world)
270 {
271 int16_t mapX, mapY;
272 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
273 {
274 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
275 return (Vector2){(float)-delta.x, (float)-delta.y};
276 }
277 // fallback to a simple gradient calculation
278 float n = PathFindingGetDistance(mapX, mapY - 1);
279 float s = PathFindingGetDistance(mapX, mapY + 1);
280 float w = PathFindingGetDistance(mapX - 1, mapY);
281 float e = PathFindingGetDistance(mapX + 1, mapY);
282 return (Vector2){w - e + 0.25f, n - s + 0.125f};
283 }
284
285 //# Enemies
286
287 #define ENEMY_MAX_PATH_COUNT 8
288 #define ENEMY_MAX_COUNT 400
289 #define ENEMY_TYPE_NONE 0
290 #define ENEMY_TYPE_MINION 1
291
292 typedef struct EnemyId
293 {
294 uint16_t index;
295 uint16_t generation;
296 } EnemyId;
297
298 typedef struct EnemyClassConfig
299 {
300 float speed;
301 float health;
302 float radius;
303 float maxAcceleration;
304 float explosionDamage;
305 } EnemyClassConfig;
306
307 typedef struct Enemy
308 {
309 int16_t currentX, currentY;
310 int16_t nextX, nextY;
311 Vector2 simPosition;
312 Vector2 simVelocity;
313 uint16_t generation;
314 float startMovingTime;
315 float damage, futureDamage;
316 uint8_t enemyType;
317 uint8_t movePathCount;
318 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
319 } Enemy;
320
321 Enemy enemies[ENEMY_MAX_COUNT];
322 int enemyCount = 0;
323
324 EnemyClassConfig enemyClassConfigs[] = {
325 [ENEMY_TYPE_MINION] = {
326 .health = 3.0f,
327 .speed = 1.0f,
328 .radius = 0.25f,
329 .maxAcceleration = 1.0f,
330 .explosionDamage = 1.0f,
331 },
332 };
333
334 void EnemyInit()
335 {
336 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
337 {
338 enemies[i] = (Enemy){0};
339 }
340 enemyCount = 0;
341 }
342
343 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
344 {
345 return enemyClassConfigs[enemy->enemyType].speed;
346 }
347
348 float EnemyGetMaxHealth(Enemy *enemy)
349 {
350 return enemyClassConfigs[enemy->enemyType].health;
351 }
352
353 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
354 {
355 int16_t castleX = 0;
356 int16_t castleY = 0;
357 int16_t dx = castleX - currentX;
358 int16_t dy = castleY - currentY;
359 if (dx == 0 && dy == 0)
360 {
361 *nextX = currentX;
362 *nextY = currentY;
363 return 1;
364 }
365 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
366
367 if (gradient.x == 0 && gradient.y == 0)
368 {
369 *nextX = currentX;
370 *nextY = currentY;
371 return 1;
372 }
373
374 if (fabsf(gradient.x) > fabsf(gradient.y))
375 {
376 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
377 *nextY = currentY;
378 return 0;
379 }
380 *nextX = currentX;
381 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
382 return 0;
383 }
384
385
386 // this function predicts the movement of the unit for the next deltaT seconds
387 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
388 {
389 const float pointReachedDistance = 0.25f;
390 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
391 const float maxSimStepTime = 0.015625f;
392
393 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
394 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
395 int16_t nextX = enemy->nextX;
396 int16_t nextY = enemy->nextY;
397 Vector2 position = enemy->simPosition;
398 int passedCount = 0;
399 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
400 {
401 float stepTime = fminf(deltaT - t, maxSimStepTime);
402 Vector2 target = (Vector2){nextX, nextY};
403 float speed = Vector2Length(*velocity);
404 // draw the target position for debugging
405 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
406 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
407 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
408 {
409 // we reached the target position, let's move to the next waypoint
410 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
411 target = (Vector2){nextX, nextY};
412 // track how many waypoints we passed
413 passedCount++;
414 }
415
416 // acceleration towards the target
417 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
418 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
419 *velocity = Vector2Add(*velocity, acceleration);
420
421 // limit the speed to the maximum speed
422 if (speed > maxSpeed)
423 {
424 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
425 }
426
427 // move the enemy
428 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
429 }
430
431 if (waypointPassedCount)
432 {
433 (*waypointPassedCount) = passedCount;
434 }
435
436 return position;
437 }
438
439 void EnemyDraw()
440 {
441 for (int i = 0; i < enemyCount; i++)
442 {
443 Enemy enemy = enemies[i];
444 if (enemy.enemyType == ENEMY_TYPE_NONE)
445 {
446 continue;
447 }
448
449 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
450
451 if (enemy.movePathCount > 0)
452 {
453 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
454 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
455 }
456 for (int j = 1; j < enemy.movePathCount; j++)
457 {
458 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
459 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
460 DrawLine3D(p, q, GREEN);
461 }
462
463 switch (enemy.enemyType)
464 {
465 case ENEMY_TYPE_MINION:
466 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
467 break;
468 }
469 }
470 }
471
472 void EnemyTriggerExplode(Enemy *enemy, Tower *tower)
473 {
474 // damage the tower
475 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
476 // explode the enemy
477 if (tower->damage >= TowerGetMaxHealth(tower))
478 {
479 tower->towerType = TOWER_TYPE_NONE;
480 }
481
482 enemy->enemyType = ENEMY_TYPE_NONE;
483 }
484
485 void EnemyUpdate()
486 {
487 const float castleX = 0;
488 const float castleY = 0;
489 const float maxPathDistance2 = 0.25f * 0.25f;
490
491 for (int i = 0; i < enemyCount; i++)
492 {
493 Enemy *enemy = &enemies[i];
494 if (enemy->enemyType == ENEMY_TYPE_NONE)
495 {
496 continue;
497 }
498
499 int waypointPassedCount = 0;
500 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
501 enemy->startMovingTime = gameTime.time;
502 // track path of unit
503 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
504 {
505 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
506 {
507 enemy->movePath[j] = enemy->movePath[j - 1];
508 }
509 enemy->movePath[0] = enemy->simPosition;
510 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
511 {
512 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
513 }
514 }
515
516 if (waypointPassedCount > 0)
517 {
518 enemy->currentX = enemy->nextX;
519 enemy->currentY = enemy->nextY;
520 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
521 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
522 {
523 // enemy reached the castle; remove it
524 enemy->enemyType = ENEMY_TYPE_NONE;
525 continue;
526 }
527 }
528 }
529
530 // handle collisions between enemies
531 for (int i = 0; i < enemyCount - 1; i++)
532 {
533 Enemy *enemyA = &enemies[i];
534 if (enemyA->enemyType == ENEMY_TYPE_NONE)
535 {
536 continue;
537 }
538 for (int j = i + 1; j < enemyCount; j++)
539 {
540 Enemy *enemyB = &enemies[j];
541 if (enemyB->enemyType == ENEMY_TYPE_NONE)
542 {
543 continue;
544 }
545 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
546 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
547 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
548 float radiusSum = radiusA + radiusB;
549 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
550 {
551 // collision
552 float distance = sqrtf(distanceSqr);
553 float overlap = radiusSum - distance;
554 // move the enemies apart, but softly; if we have a clog of enemies,
555 // moving them perfectly apart can cause them to jitter
556 float positionCorrection = overlap / 5.0f;
557 Vector2 direction = (Vector2){
558 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
559 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
560 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
561 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
562 }
563 }
564 }
565
566 // handle collisions between enemies and towers
567 for (int i = 0; i < enemyCount; i++)
568 {
569 Enemy *enemy = &enemies[i];
570 if (enemy->enemyType == ENEMY_TYPE_NONE)
571 {
572 continue;
573 }
574 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
575 // linear search over towers; could be optimized by using path finding tower map,
576 // but for now, we keep it simple
577 for (int j = 0; j < towerCount; j++)
578 {
579 Tower *tower = &towers[j];
580 if (tower->towerType == TOWER_TYPE_NONE)
581 {
582 continue;
583 }
584 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
585 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
586 if (distanceSqr > combinedRadius * combinedRadius)
587 {
588 continue;
589 }
590 // potential collision; square / circle intersection
591 float dx = tower->x - enemy->simPosition.x;
592 float dy = tower->y - enemy->simPosition.y;
593 float absDx = fabsf(dx);
594 float absDy = fabsf(dy);
595 if (absDx <= 0.5f && absDx <= absDy) {
596 // vertical collision; push the enemy out horizontally
597 float overlap = enemyRadius + 0.5f - absDy;
598 if (overlap < 0.0f)
599 {
600 continue;
601 }
602 float direction = dy > 0.0f ? -1.0f : 1.0f;
603 enemy->simPosition.y += direction * overlap;
604 }
605 else if (absDy <= 0.5f && absDy <= absDx)
606 {
607 // horizontal collision; push the enemy out vertically
608 float overlap = enemyRadius + 0.5f - absDx;
609 if (overlap < 0.0f)
610 {
611 continue;
612 }
613 float direction = dx > 0.0f ? -1.0f : 1.0f;
614 enemy->simPosition.x += direction * overlap;
615 }
616 else
617 {
618 // possible collision with a corner
619 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
620 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
621 float cornerX = tower->x + cornerDX;
622 float cornerY = tower->y + cornerDY;
623 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
624 if (cornerDistanceSqr > enemyRadius * enemyRadius)
625 {
626 continue;
627 }
628 // push the enemy out along the diagonal
629 float cornerDistance = sqrtf(cornerDistanceSqr);
630 float overlap = enemyRadius - cornerDistance;
631 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
632 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
633 enemy->simPosition.x -= directionX * overlap;
634 enemy->simPosition.y -= directionY * overlap;
635 }
636
637 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
638 {
639 EnemyTriggerExplode(enemy, tower);
640 }
641 }
642 }
643 }
644
645 EnemyId EnemyGetId(Enemy *enemy)
646 {
647 return (EnemyId){enemy - enemies, enemy->generation};
648 }
649
650 Enemy *EnemyTryResolve(EnemyId enemyId)
651 {
652 if (enemyId.index >= ENEMY_MAX_COUNT)
653 {
654 return 0;
655 }
656 Enemy *enemy = &enemies[enemyId.index];
657 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
658 {
659 return 0;
660 }
661 return enemy;
662 }
663
664 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
665 {
666 Enemy *spawn = 0;
667 for (int i = 0; i < enemyCount; i++)
668 {
669 Enemy *enemy = &enemies[i];
670 if (enemy->enemyType == ENEMY_TYPE_NONE)
671 {
672 spawn = enemy;
673 break;
674 }
675 }
676
677 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
678 {
679 spawn = &enemies[enemyCount++];
680 }
681
682 if (spawn)
683 {
684 spawn->currentX = currentX;
685 spawn->currentY = currentY;
686 spawn->nextX = currentX;
687 spawn->nextY = currentY;
688 spawn->simPosition = (Vector2){currentX, currentY};
689 spawn->simVelocity = (Vector2){0, 0};
690 spawn->enemyType = enemyType;
691 spawn->startMovingTime = gameTime.time;
692 spawn->damage = 0.0f;
693 spawn->futureDamage = 0.0f;
694 spawn->generation++;
695 spawn->movePathCount = 0;
696 }
697
698 return spawn;
699 }
700
701 int EnemyAddDamage(Enemy *enemy, float damage)
702 {
703 enemy->damage += damage;
704 if (enemy->damage >= EnemyGetMaxHealth(enemy))
705 {
706 enemy->enemyType = ENEMY_TYPE_NONE;
707 return 1;
708 }
709
710 return 0;
711 }
712
713 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
714 {
715 int16_t castleX = 0;
716 int16_t castleY = 0;
717 Enemy* closest = 0;
718 int16_t closestDistance = 0;
719 float range2 = range * range;
720 for (int i = 0; i < enemyCount; i++)
721 {
722 Enemy* enemy = &enemies[i];
723 if (enemy->enemyType == ENEMY_TYPE_NONE)
724 {
725 continue;
726 }
727 float maxHealth = EnemyGetMaxHealth(enemy);
728 if (enemy->futureDamage >= maxHealth)
729 {
730 // ignore enemies that will die soon
731 continue;
732 }
733 int16_t dx = castleX - enemy->currentX;
734 int16_t dy = castleY - enemy->currentY;
735 int16_t distance = abs(dx) + abs(dy);
736 if (!closest || distance < closestDistance)
737 {
738 float tdx = towerX - enemy->currentX;
739 float tdy = towerY - enemy->currentY;
740 float tdistance2 = tdx * tdx + tdy * tdy;
741 if (tdistance2 <= range2)
742 {
743 closest = enemy;
744 closestDistance = distance;
745 }
746 }
747 }
748 return closest;
749 }
750
751 int EnemyCount()
752 {
753 int count = 0;
754 for (int i = 0; i < enemyCount; i++)
755 {
756 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
757 {
758 count++;
759 }
760 }
761 return count;
762 }
763
764 //# Projectiles
765 #define PROJECTILE_MAX_COUNT 1200
766 #define PROJECTILE_TYPE_NONE 0
767 #define PROJECTILE_TYPE_BULLET 1
768
769 typedef struct Projectile
770 {
771 uint8_t projectileType;
772 float shootTime;
773 float arrivalTime;
774 float damage;
775 Vector2 position;
776 Vector2 target;
777 Vector2 directionNormal;
778 EnemyId targetEnemy;
779 } Projectile;
780
781 Projectile projectiles[PROJECTILE_MAX_COUNT];
782 int projectileCount = 0;
783
784 void ProjectileInit()
785 {
786 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
787 {
788 projectiles[i] = (Projectile){0};
789 }
790 }
791
792 void ProjectileDraw()
793 {
794 for (int i = 0; i < projectileCount; i++)
795 {
796 Projectile projectile = projectiles[i];
797 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
798 {
799 continue;
800 }
801 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
802 if (transition >= 1.0f)
803 {
804 continue;
805 }
806 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
807 float x = position.x;
808 float y = position.y;
809 float dx = projectile.directionNormal.x;
810 float dy = projectile.directionNormal.y;
811 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
812 {
813 x -= dx * 0.1f;
814 y -= dy * 0.1f;
815 float size = 0.1f * d;
816 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
817 }
818 }
819 }
820
821 void ProjectileUpdate()
822 {
823 for (int i = 0; i < projectileCount; i++)
824 {
825 Projectile *projectile = &projectiles[i];
826 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
827 {
828 continue;
829 }
830 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
831 if (transition >= 1.0f)
832 {
833 projectile->projectileType = PROJECTILE_TYPE_NONE;
834 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
835 if (enemy)
836 {
837 EnemyAddDamage(enemy, projectile->damage);
838 }
839 continue;
840 }
841 }
842 }
843
844 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
845 {
846 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
847 {
848 Projectile *projectile = &projectiles[i];
849 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
850 {
851 projectile->projectileType = projectileType;
852 projectile->shootTime = gameTime.time;
853 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
854 projectile->damage = damage;
855 projectile->position = position;
856 projectile->target = target;
857 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
858 projectile->targetEnemy = EnemyGetId(enemy);
859 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
860 return projectile;
861 }
862 }
863 return 0;
864 }
865
866 //# Towers
867
868 void TowerInit()
869 {
870 for (int i = 0; i < TOWER_MAX_COUNT; i++)
871 {
872 towers[i] = (Tower){0};
873 }
874 towerCount = 0;
875 }
876
877 Tower *TowerGetAt(int16_t x, int16_t y)
878 {
879 for (int i = 0; i < towerCount; i++)
880 {
881 if (towers[i].x == x && towers[i].y == y)
882 {
883 return &towers[i];
884 }
885 }
886 return 0;
887 }
888
889 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
890 {
891 if (towerCount >= TOWER_MAX_COUNT)
892 {
893 return 0;
894 }
895
896 Tower *tower = TowerGetAt(x, y);
897 if (tower)
898 {
899 return 0;
900 }
901
902 tower = &towers[towerCount++];
903 tower->x = x;
904 tower->y = y;
905 tower->towerType = towerType;
906 tower->cooldown = 0.0f;
907 tower->damage = 0.0f;
908 return tower;
909 }
910
911 float TowerGetMaxHealth(Tower *tower)
912 {
913 switch (tower->towerType)
914 {
915 case TOWER_TYPE_BASE:
916 return 10.0f;
917 case TOWER_TYPE_GUN:
918 return 3.0f;
919 case TOWER_TYPE_WALL:
920 return 5.0f;
921 }
922 return 0.0f;
923 }
924
925 void TowerDraw()
926 {
927 for (int i = 0; i < towerCount; i++)
928 {
929 Tower tower = towers[i];
930 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
931 switch (tower.towerType)
932 {
933 case TOWER_TYPE_BASE:
934 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
935 break;
936 case TOWER_TYPE_GUN:
937 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
938 break;
939 case TOWER_TYPE_WALL:
940 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
941 break;
942 }
943 }
944 }
945
946 void TowerGunUpdate(Tower *tower)
947 {
948 if (tower->cooldown <= 0)
949 {
950 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
951 if (enemy)
952 {
953 tower->cooldown = 0.125f;
954 // shoot the enemy; determine future position of the enemy
955 float bulletSpeed = 1.0f;
956 float bulletDamage = 3.0f;
957 Vector2 velocity = enemy->simVelocity;
958 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
959 Vector2 towerPosition = {tower->x, tower->y};
960 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
961 for (int i = 0; i < 8; i++) {
962 velocity = enemy->simVelocity;
963 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
964 float distance = Vector2Distance(towerPosition, futurePosition);
965 float eta2 = distance / bulletSpeed;
966 if (fabs(eta - eta2) < 0.01f) {
967 break;
968 }
969 eta = (eta2 + eta) * 0.5f;
970 }
971 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
972 bulletSpeed, bulletDamage);
973 enemy->futureDamage += bulletDamage;
974 }
975 }
976 else
977 {
978 tower->cooldown -= gameTime.deltaTime;
979 }
980 }
981
982 void TowerUpdate()
983 {
984 for (int i = 0; i < towerCount; i++)
985 {
986 Tower *tower = &towers[i];
987 switch (tower->towerType)
988 {
989 case TOWER_TYPE_GUN:
990 TowerGunUpdate(tower);
991 break;
992 }
993 }
994 }
995
996 //# Game
997
998 float nextSpawnTime = 0.0f;
999
1000 void InitGame()
1001 {
1002 TowerInit();
1003 EnemyInit();
1004 ProjectileInit();
1005 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1006
1007 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1008 TowerTryAdd(TOWER_TYPE_GUN, 2, 0);
1009
1010 for (int i = -2; i <= 2; i += 1)
1011 {
1012 TowerTryAdd(TOWER_TYPE_WALL, i, 2);
1013 TowerTryAdd(TOWER_TYPE_WALL, i, -2);
1014 TowerTryAdd(TOWER_TYPE_WALL, -2, i);
1015 }
1016
1017 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4);
1018 }
1019
1020 void GameUpdate()
1021 {
1022 float dt = GetFrameTime();
1023 // cap maximum delta time to 0.1 seconds to prevent large time steps
1024 if (dt > 0.1f) dt = 0.1f;
1025 gameTime.time += dt;
1026 gameTime.deltaTime = dt;
1027 PathFindingMapUpdate();
1028 EnemyUpdate();
1029 TowerUpdate();
1030 ProjectileUpdate();
1031
1032 // spawn a new enemy every second
1033 if (gameTime.time >= nextSpawnTime && EnemyCount() < 50)
1034 {
1035 nextSpawnTime = gameTime.time + 0.2f;
1036 // add a new enemy at the boundary of the map
1037 int randValue = GetRandomValue(-5, 5);
1038 int randSide = GetRandomValue(0, 3);
1039 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue;
1040 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue;
1041 static int alternation = 0;
1042 alternation += 1;
1043 if (alternation % 3 == 0) {
1044 EnemyTryAdd(ENEMY_TYPE_MINION, 0, -5);
1045 }
1046 else if (alternation % 3 == 1)
1047 {
1048 EnemyTryAdd(ENEMY_TYPE_MINION, 0, 5);
1049 }
1050 EnemyTryAdd(ENEMY_TYPE_MINION, x, y);
1051 }
1052 }
1053
1054 int main(void)
1055 {
1056 int screenWidth, screenHeight;
1057 GetPreferredSize(&screenWidth, &screenHeight);
1058 InitWindow(screenWidth, screenHeight, "Tower defense");
1059 SetTargetFPS(30);
1060
1061 Camera3D camera = {0};
1062 camera.position = (Vector3){0.0f, 10.0f, -0.5f};
1063 camera.target = (Vector3){0.0f, 0.0f, -0.5f};
1064 camera.up = (Vector3){0.0f, 0.0f, -1.0f};
1065 camera.fovy = 12.0f;
1066 camera.projection = CAMERA_ORTHOGRAPHIC;
1067
1068 InitGame();
1069
1070 while (!WindowShouldClose())
1071 {
1072 if (IsPaused()) {
1073 // canvas is not visible in browser - do nothing
1074 continue;
1075 }
1076 BeginDrawing();
1077 ClearBackground(DARKBLUE);
1078
1079 BeginMode3D(camera);
1080 DrawGrid(10, 1.0f);
1081 TowerDraw();
1082 EnemyDraw();
1083 ProjectileDraw();
1084 PathFindingMapDraw();
1085 GameUpdate();
1086 EndMode3D();
1087
1088 const char *title = "Tower defense tutorial";
1089 int titleWidth = MeasureText(title, 20);
1090 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f + 2, 5 + 2, 20, BLACK);
1091 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f, 5, 20, WHITE);
1092 EndDrawing();
1093 }
1094
1095 CloseWindow();
1096
1097 return 0;
1098 }
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
Our base didn't stand a chance 😶
Ok, let's make it more interesting; an enemy needs to touch the building for a certain amount of time before exploding. Also, when the enemy explodes, it should deal damage to other enemies in the area and pushing them back (line 483, 600 and 673):
1 #include "td-tut-2-main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Declarations
7
8 #define TOWER_MAX_COUNT 400
9 #define TOWER_TYPE_NONE 0
10 #define TOWER_TYPE_BASE 1
11 #define TOWER_TYPE_GUN 2
12 #define TOWER_TYPE_WALL 3
13
14 typedef struct Tower
15 {
16 int16_t x, y;
17 uint8_t towerType;
18 float cooldown;
19 float damage;
20 } Tower;
21
22 typedef struct GameTime
23 {
24 float time;
25 float deltaTime;
26 } GameTime;
27
28 GameTime gameTime = {0};
29
30 Tower towers[TOWER_MAX_COUNT];
31 int towerCount = 0;
32
33 float TowerGetMaxHealth(Tower *tower);
34
35 //# Pathfinding map
36 typedef struct DeltaSrc
37 {
38 char x, y;
39 } DeltaSrc;
40
41 typedef struct PathfindingMap
42 {
43 int width, height;
44 float scale;
45 float *distances;
46 long *towerIndex;
47 DeltaSrc *deltaSrc;
48 float maxDistance;
49 Matrix toMapSpace;
50 Matrix toWorldSpace;
51 } PathfindingMap;
52
53 // when we execute the pathfinding algorithm, we need to store the active nodes
54 // in a queue. Each node has a position, a distance from the start, and the
55 // position of the node that we came from.
56 typedef struct PathfindingNode
57 {
58 int16_t x, y, fromX, fromY;
59 float distance;
60 } PathfindingNode;
61
62 // The queue is a simple array of nodes, we add nodes to the end and remove
63 // nodes from the front. We keep the array around to avoid unnecessary allocations
64 static PathfindingNode *pathfindingNodeQueue = 0;
65 static int pathfindingNodeQueueCount = 0;
66 static int pathfindingNodeQueueCapacity = 0;
67
68 // The pathfinding map stores the distances from the castle to each cell in the map.
69 PathfindingMap pathfindingMap = {0};
70
71 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
72 {
73 // transforming between map space and world space allows us to adapt
74 // position and scale of the map without changing the pathfinding data
75 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
76 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
77 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
78 pathfindingMap.width = width;
79 pathfindingMap.height = height;
80 pathfindingMap.scale = scale;
81 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
82 for (int i = 0; i < width * height; i++)
83 {
84 pathfindingMap.distances[i] = -1.0f;
85 }
86
87 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
88 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
89 }
90
91 float PathFindingGetDistance(int mapX, int mapY)
92 {
93 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
94 {
95 // when outside the map, we return the manhattan distance to the castle (0,0)
96 return fabsf((float)mapX) + fabsf((float)mapY);
97 }
98
99 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
100 }
101
102 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
103 {
104 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
105 {
106 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
107 // we use MemAlloc/MemRealloc to allocate memory for the queue
108 // I am not entirely sure if MemRealloc allows passing a null pointer
109 // so we check if the pointer is null and use MemAlloc in that case
110 if (pathfindingNodeQueue == 0)
111 {
112 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
113 }
114 else
115 {
116 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
117 }
118 }
119
120 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
121 node->x = x;
122 node->y = y;
123 node->fromX = fromX;
124 node->fromY = fromY;
125 node->distance = distance;
126 }
127
128 PathfindingNode *PathFindingNodePop()
129 {
130 if (pathfindingNodeQueueCount == 0)
131 {
132 return 0;
133 }
134 // we return the first node in the queue; we want to return a pointer to the node
135 // so we can return 0 if the queue is empty.
136 // We should _not_ return a pointer to the element in the list, because the list
137 // may be reallocated and the pointer would become invalid. Or the
138 // popped element is overwritten by the next push operation.
139 // Using static here means that the variable is permanently allocated.
140 static PathfindingNode node;
141 node = pathfindingNodeQueue[0];
142 // we shift all nodes one position to the front
143 for (int i = 1; i < pathfindingNodeQueueCount; i++)
144 {
145 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
146 }
147 --pathfindingNodeQueueCount;
148 return &node;
149 }
150
151 // transform a world position to a map position in the array;
152 // returns true if the position is inside the map
153 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
154 {
155 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
156 *mapX = (int16_t)mapPosition.x;
157 *mapY = (int16_t)mapPosition.z;
158 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
159 }
160
161 void PathFindingMapUpdate()
162 {
163 const int castleX = 0, castleY = 0;
164 int16_t castleMapX, castleMapY;
165 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
166 {
167 return;
168 }
169 int width = pathfindingMap.width, height = pathfindingMap.height;
170
171 // reset the distances to -1
172 for (int i = 0; i < width * height; i++)
173 {
174 pathfindingMap.distances[i] = -1.0f;
175 }
176 // reset the tower indices
177 for (int i = 0; i < width * height; i++)
178 {
179 pathfindingMap.towerIndex[i] = -1;
180 }
181 // reset the delta src
182 for (int i = 0; i < width * height; i++)
183 {
184 pathfindingMap.deltaSrc[i].x = 0;
185 pathfindingMap.deltaSrc[i].y = 0;
186 }
187
188 for (int i = 0; i < towerCount; i++)
189 {
190 Tower *tower = &towers[i];
191 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
192 {
193 continue;
194 }
195 int16_t mapX, mapY;
196 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
197 // this would not work correctly and needs to be refined to allow towers covering multiple cells
198 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
199 // one cell. For now.
200 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
201 {
202 continue;
203 }
204 int index = mapY * width + mapX;
205 pathfindingMap.towerIndex[index] = i;
206 }
207
208 // we start at the castle and add the castle to the queue
209 pathfindingMap.maxDistance = 0.0f;
210 pathfindingNodeQueueCount = 0;
211 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
212 PathfindingNode *node = 0;
213 while ((node = PathFindingNodePop()))
214 {
215 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
216 {
217 continue;
218 }
219 int index = node->y * width + node->x;
220 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
221 {
222 continue;
223 }
224
225 int deltaX = node->x - node->fromX;
226 int deltaY = node->y - node->fromY;
227 // even if the cell is blocked by a tower, we still may want to store the direction
228 // (though this might not be needed, IDK right now)
229 pathfindingMap.deltaSrc[index].x = (char) deltaX;
230 pathfindingMap.deltaSrc[index].y = (char) deltaY;
231
232 // we skip nodes that are blocked by towers
233 if (pathfindingMap.towerIndex[index] >= 0)
234 {
235 node->distance += 8.0f;
236 }
237 pathfindingMap.distances[index] = node->distance;
238 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
239 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
240 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
241 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
242 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
243 }
244 }
245
246 void PathFindingMapDraw()
247 {
248 float cellSize = pathfindingMap.scale * 0.9f;
249 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
250 for (int x = 0; x < pathfindingMap.width; x++)
251 {
252 for (int y = 0; y < pathfindingMap.height; y++)
253 {
254 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
255 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
256 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
257 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
258 // animate the distance "wave" to show how the pathfinding algorithm expands
259 // from the castle
260 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
261 {
262 color = BLACK;
263 }
264 DrawCube(position, cellSize, 0.1f, cellSize, color);
265 }
266 }
267 }
268
269 Vector2 PathFindingGetGradient(Vector3 world)
270 {
271 int16_t mapX, mapY;
272 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
273 {
274 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
275 return (Vector2){(float)-delta.x, (float)-delta.y};
276 }
277 // fallback to a simple gradient calculation
278 float n = PathFindingGetDistance(mapX, mapY - 1);
279 float s = PathFindingGetDistance(mapX, mapY + 1);
280 float w = PathFindingGetDistance(mapX - 1, mapY);
281 float e = PathFindingGetDistance(mapX + 1, mapY);
282 return (Vector2){w - e + 0.25f, n - s + 0.125f};
283 }
284
285 //# Enemies
286
287 #define ENEMY_MAX_PATH_COUNT 8
288 #define ENEMY_MAX_COUNT 400
289 #define ENEMY_TYPE_NONE 0
290 #define ENEMY_TYPE_MINION 1
291
292 typedef struct EnemyId
293 {
294 uint16_t index;
295 uint16_t generation;
296 } EnemyId;
297
298 typedef struct EnemyClassConfig
299 {
300 float speed;
301 float health;
302 float radius;
303 float maxAcceleration;
304 float requiredContactTime;
305 float explosionDamage;
306 float explosionRange;
307 float explosionPushbackPower;
308 } EnemyClassConfig;
309
310 typedef struct Enemy
311 {
312 int16_t currentX, currentY;
313 int16_t nextX, nextY;
314 Vector2 simPosition;
315 Vector2 simVelocity;
316 uint16_t generation;
317 float startMovingTime;
318 float damage, futureDamage;
319 float contactTime;
320 uint8_t enemyType;
321 uint8_t movePathCount;
322 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
323 } Enemy;
324
325 Enemy enemies[ENEMY_MAX_COUNT];
326 int enemyCount = 0;
327
328 EnemyClassConfig enemyClassConfigs[] = {
329 [ENEMY_TYPE_MINION] = {
330 .health = 3.0f,
331 .speed = 1.0f,
332 .radius = 0.25f,
333 .maxAcceleration = 1.0f,
334 .explosionDamage = 1.0f,
335 .requiredContactTime = 0.5f,
336 .explosionRange = 1.0f,
337 .explosionPushbackPower = 0.25f,
338 },
339 };
340
341 int EnemyAddDamage(Enemy *enemy, float damage);
342
343 void EnemyInit()
344 {
345 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
346 {
347 enemies[i] = (Enemy){0};
348 }
349 enemyCount = 0;
350 }
351
352 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
353 {
354 return enemyClassConfigs[enemy->enemyType].speed;
355 }
356
357 float EnemyGetMaxHealth(Enemy *enemy)
358 {
359 return enemyClassConfigs[enemy->enemyType].health;
360 }
361
362 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
363 {
364 int16_t castleX = 0;
365 int16_t castleY = 0;
366 int16_t dx = castleX - currentX;
367 int16_t dy = castleY - currentY;
368 if (dx == 0 && dy == 0)
369 {
370 *nextX = currentX;
371 *nextY = currentY;
372 return 1;
373 }
374 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
375
376 if (gradient.x == 0 && gradient.y == 0)
377 {
378 *nextX = currentX;
379 *nextY = currentY;
380 return 1;
381 }
382
383 if (fabsf(gradient.x) > fabsf(gradient.y))
384 {
385 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
386 *nextY = currentY;
387 return 0;
388 }
389 *nextX = currentX;
390 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
391 return 0;
392 }
393
394
395 // this function predicts the movement of the unit for the next deltaT seconds
396 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
397 {
398 const float pointReachedDistance = 0.25f;
399 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
400 const float maxSimStepTime = 0.015625f;
401
402 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
403 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
404 int16_t nextX = enemy->nextX;
405 int16_t nextY = enemy->nextY;
406 Vector2 position = enemy->simPosition;
407 int passedCount = 0;
408 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
409 {
410 float stepTime = fminf(deltaT - t, maxSimStepTime);
411 Vector2 target = (Vector2){nextX, nextY};
412 float speed = Vector2Length(*velocity);
413 // draw the target position for debugging
414 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
415 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
416 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
417 {
418 // we reached the target position, let's move to the next waypoint
419 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
420 target = (Vector2){nextX, nextY};
421 // track how many waypoints we passed
422 passedCount++;
423 }
424
425 // acceleration towards the target
426 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
427 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
428 *velocity = Vector2Add(*velocity, acceleration);
429
430 // limit the speed to the maximum speed
431 if (speed > maxSpeed)
432 {
433 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
434 }
435
436 // move the enemy
437 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
438 }
439
440 if (waypointPassedCount)
441 {
442 (*waypointPassedCount) = passedCount;
443 }
444
445 return position;
446 }
447
448 void EnemyDraw()
449 {
450 for (int i = 0; i < enemyCount; i++)
451 {
452 Enemy enemy = enemies[i];
453 if (enemy.enemyType == ENEMY_TYPE_NONE)
454 {
455 continue;
456 }
457
458 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
459
460 if (enemy.movePathCount > 0)
461 {
462 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
463 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
464 }
465 for (int j = 1; j < enemy.movePathCount; j++)
466 {
467 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
468 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
469 DrawLine3D(p, q, GREEN);
470 }
471
472 switch (enemy.enemyType)
473 {
474 case ENEMY_TYPE_MINION:
475 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
476 break;
477 }
478 }
479 }
480
481 void EnemyTriggerExplode(Enemy *enemy, Tower *tower)
482 {
483 // damage the tower
484 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
485 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
486 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
487 float explosionRange2 = explosionRange * explosionRange;
488 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
489 // explode the enemy
490 if (tower->damage >= TowerGetMaxHealth(tower))
491 {
492 tower->towerType = TOWER_TYPE_NONE;
493 }
494
495 enemy->enemyType = ENEMY_TYPE_NONE;
496
497 // push back enemies & dealing damage
498 for (int i = 0; i < enemyCount; i++)
499 {
500 Enemy *other = &enemies[i];
501 if (other->enemyType == ENEMY_TYPE_NONE)
502 {
503 continue;
504 }
505 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
506 if (distanceSqr > 0 && distanceSqr < explosionRange2)
507 {
508 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
509 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
510 EnemyAddDamage(other, explosionDamge);
511 }
512 }
513 }
514
515 void EnemyUpdate()
516 {
517 const float castleX = 0;
518 const float castleY = 0;
519 const float maxPathDistance2 = 0.25f * 0.25f;
520
521 for (int i = 0; i < enemyCount; i++)
522 {
523 Enemy *enemy = &enemies[i];
524 if (enemy->enemyType == ENEMY_TYPE_NONE)
525 {
526 continue;
527 }
528
529 int waypointPassedCount = 0;
530 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
531 enemy->startMovingTime = gameTime.time;
532 // track path of unit
533 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
534 {
535 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
536 {
537 enemy->movePath[j] = enemy->movePath[j - 1];
538 }
539 enemy->movePath[0] = enemy->simPosition;
540 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
541 {
542 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
543 }
544 }
545
546 if (waypointPassedCount > 0)
547 {
548 enemy->currentX = enemy->nextX;
549 enemy->currentY = enemy->nextY;
550 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
551 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
552 {
553 // enemy reached the castle; remove it
554 enemy->enemyType = ENEMY_TYPE_NONE;
555 continue;
556 }
557 }
558 }
559
560 // handle collisions between enemies
561 for (int i = 0; i < enemyCount - 1; i++)
562 {
563 Enemy *enemyA = &enemies[i];
564 if (enemyA->enemyType == ENEMY_TYPE_NONE)
565 {
566 continue;
567 }
568 for (int j = i + 1; j < enemyCount; j++)
569 {
570 Enemy *enemyB = &enemies[j];
571 if (enemyB->enemyType == ENEMY_TYPE_NONE)
572 {
573 continue;
574 }
575 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
576 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
577 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
578 float radiusSum = radiusA + radiusB;
579 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
580 {
581 // collision
582 float distance = sqrtf(distanceSqr);
583 float overlap = radiusSum - distance;
584 // move the enemies apart, but softly; if we have a clog of enemies,
585 // moving them perfectly apart can cause them to jitter
586 float positionCorrection = overlap / 5.0f;
587 Vector2 direction = (Vector2){
588 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
589 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
590 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
591 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
592 }
593 }
594 }
595
596 // handle collisions between enemies and towers
597 for (int i = 0; i < enemyCount; i++)
598 {
599 Enemy *enemy = &enemies[i];
600 if (enemy->enemyType == ENEMY_TYPE_NONE)
601 {
602 continue;
603 }
604 enemy->contactTime -= gameTime.deltaTime;
605 if (enemy->contactTime < 0.0f)
606 {
607 enemy->contactTime = 0.0f;
608 }
609
610 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
611 // linear search over towers; could be optimized by using path finding tower map,
612 // but for now, we keep it simple
613 for (int j = 0; j < towerCount; j++)
614 {
615 Tower *tower = &towers[j];
616 if (tower->towerType == TOWER_TYPE_NONE)
617 {
618 continue;
619 }
620 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
621 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
622 if (distanceSqr > combinedRadius * combinedRadius)
623 {
624 continue;
625 }
626 // potential collision; square / circle intersection
627 float dx = tower->x - enemy->simPosition.x;
628 float dy = tower->y - enemy->simPosition.y;
629 float absDx = fabsf(dx);
630 float absDy = fabsf(dy);
631 if (absDx <= 0.5f && absDx <= absDy) {
632 // vertical collision; push the enemy out horizontally
633 float overlap = enemyRadius + 0.5f - absDy;
634 if (overlap < 0.0f)
635 {
636 continue;
637 }
638 float direction = dy > 0.0f ? -1.0f : 1.0f;
639 enemy->simPosition.y += direction * overlap;
640 }
641 else if (absDy <= 0.5f && absDy <= absDx)
642 {
643 // horizontal collision; push the enemy out vertically
644 float overlap = enemyRadius + 0.5f - absDx;
645 if (overlap < 0.0f)
646 {
647 continue;
648 }
649 float direction = dx > 0.0f ? -1.0f : 1.0f;
650 enemy->simPosition.x += direction * overlap;
651 }
652 else
653 {
654 // possible collision with a corner
655 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
656 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
657 float cornerX = tower->x + cornerDX;
658 float cornerY = tower->y + cornerDY;
659 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
660 if (cornerDistanceSqr > enemyRadius * enemyRadius)
661 {
662 continue;
663 }
664 // push the enemy out along the diagonal
665 float cornerDistance = sqrtf(cornerDistanceSqr);
666 float overlap = enemyRadius - cornerDistance;
667 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
668 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
669 enemy->simPosition.x -= directionX * overlap;
670 enemy->simPosition.y -= directionY * overlap;
671 }
672
673 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
674 {
675 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
676 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
677 {
678 EnemyTriggerExplode(enemy, tower);
679 }
680 }
681 }
682 }
683 }
684
685 EnemyId EnemyGetId(Enemy *enemy)
686 {
687 return (EnemyId){enemy - enemies, enemy->generation};
688 }
689
690 Enemy *EnemyTryResolve(EnemyId enemyId)
691 {
692 if (enemyId.index >= ENEMY_MAX_COUNT)
693 {
694 return 0;
695 }
696 Enemy *enemy = &enemies[enemyId.index];
697 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
698 {
699 return 0;
700 }
701 return enemy;
702 }
703
704 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
705 {
706 Enemy *spawn = 0;
707 for (int i = 0; i < enemyCount; i++)
708 {
709 Enemy *enemy = &enemies[i];
710 if (enemy->enemyType == ENEMY_TYPE_NONE)
711 {
712 spawn = enemy;
713 break;
714 }
715 }
716
717 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
718 {
719 spawn = &enemies[enemyCount++];
720 }
721
722 if (spawn)
723 {
724 spawn->currentX = currentX;
725 spawn->currentY = currentY;
726 spawn->nextX = currentX;
727 spawn->nextY = currentY;
728 spawn->simPosition = (Vector2){currentX, currentY};
729 spawn->simVelocity = (Vector2){0, 0};
730 spawn->enemyType = enemyType;
731 spawn->startMovingTime = gameTime.time;
732 spawn->damage = 0.0f;
733 spawn->futureDamage = 0.0f;
734 spawn->generation++;
735 spawn->movePathCount = 0;
736 }
737
738 return spawn;
739 }
740
741 int EnemyAddDamage(Enemy *enemy, float damage)
742 {
743 enemy->damage += damage;
744 if (enemy->damage >= EnemyGetMaxHealth(enemy))
745 {
746 enemy->enemyType = ENEMY_TYPE_NONE;
747 return 1;
748 }
749
750 return 0;
751 }
752
753 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
754 {
755 int16_t castleX = 0;
756 int16_t castleY = 0;
757 Enemy* closest = 0;
758 int16_t closestDistance = 0;
759 float range2 = range * range;
760 for (int i = 0; i < enemyCount; i++)
761 {
762 Enemy* enemy = &enemies[i];
763 if (enemy->enemyType == ENEMY_TYPE_NONE)
764 {
765 continue;
766 }
767 float maxHealth = EnemyGetMaxHealth(enemy);
768 if (enemy->futureDamage >= maxHealth)
769 {
770 // ignore enemies that will die soon
771 continue;
772 }
773 int16_t dx = castleX - enemy->currentX;
774 int16_t dy = castleY - enemy->currentY;
775 int16_t distance = abs(dx) + abs(dy);
776 if (!closest || distance < closestDistance)
777 {
778 float tdx = towerX - enemy->currentX;
779 float tdy = towerY - enemy->currentY;
780 float tdistance2 = tdx * tdx + tdy * tdy;
781 if (tdistance2 <= range2)
782 {
783 closest = enemy;
784 closestDistance = distance;
785 }
786 }
787 }
788 return closest;
789 }
790
791 int EnemyCount()
792 {
793 int count = 0;
794 for (int i = 0; i < enemyCount; i++)
795 {
796 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
797 {
798 count++;
799 }
800 }
801 return count;
802 }
803
804 //# Projectiles
805 #define PROJECTILE_MAX_COUNT 1200
806 #define PROJECTILE_TYPE_NONE 0
807 #define PROJECTILE_TYPE_BULLET 1
808
809 typedef struct Projectile
810 {
811 uint8_t projectileType;
812 float shootTime;
813 float arrivalTime;
814 float damage;
815 Vector2 position;
816 Vector2 target;
817 Vector2 directionNormal;
818 EnemyId targetEnemy;
819 } Projectile;
820
821 Projectile projectiles[PROJECTILE_MAX_COUNT];
822 int projectileCount = 0;
823
824 void ProjectileInit()
825 {
826 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
827 {
828 projectiles[i] = (Projectile){0};
829 }
830 }
831
832 void ProjectileDraw()
833 {
834 for (int i = 0; i < projectileCount; i++)
835 {
836 Projectile projectile = projectiles[i];
837 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
838 {
839 continue;
840 }
841 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
842 if (transition >= 1.0f)
843 {
844 continue;
845 }
846 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
847 float x = position.x;
848 float y = position.y;
849 float dx = projectile.directionNormal.x;
850 float dy = projectile.directionNormal.y;
851 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
852 {
853 x -= dx * 0.1f;
854 y -= dy * 0.1f;
855 float size = 0.1f * d;
856 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
857 }
858 }
859 }
860
861 void ProjectileUpdate()
862 {
863 for (int i = 0; i < projectileCount; i++)
864 {
865 Projectile *projectile = &projectiles[i];
866 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
867 {
868 continue;
869 }
870 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
871 if (transition >= 1.0f)
872 {
873 projectile->projectileType = PROJECTILE_TYPE_NONE;
874 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
875 if (enemy)
876 {
877 EnemyAddDamage(enemy, projectile->damage);
878 }
879 continue;
880 }
881 }
882 }
883
884 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
885 {
886 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
887 {
888 Projectile *projectile = &projectiles[i];
889 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
890 {
891 projectile->projectileType = projectileType;
892 projectile->shootTime = gameTime.time;
893 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
894 projectile->damage = damage;
895 projectile->position = position;
896 projectile->target = target;
897 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
898 projectile->targetEnemy = EnemyGetId(enemy);
899 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
900 return projectile;
901 }
902 }
903 return 0;
904 }
905
906 //# Towers
907
908 void TowerInit()
909 {
910 for (int i = 0; i < TOWER_MAX_COUNT; i++)
911 {
912 towers[i] = (Tower){0};
913 }
914 towerCount = 0;
915 }
916
917 Tower *TowerGetAt(int16_t x, int16_t y)
918 {
919 for (int i = 0; i < towerCount; i++)
920 {
921 if (towers[i].x == x && towers[i].y == y)
922 {
923 return &towers[i];
924 }
925 }
926 return 0;
927 }
928
929 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
930 {
931 if (towerCount >= TOWER_MAX_COUNT)
932 {
933 return 0;
934 }
935
936 Tower *tower = TowerGetAt(x, y);
937 if (tower)
938 {
939 return 0;
940 }
941
942 tower = &towers[towerCount++];
943 tower->x = x;
944 tower->y = y;
945 tower->towerType = towerType;
946 tower->cooldown = 0.0f;
947 tower->damage = 0.0f;
948 return tower;
949 }
950
951 float TowerGetMaxHealth(Tower *tower)
952 {
953 switch (tower->towerType)
954 {
955 case TOWER_TYPE_BASE:
956 return 10.0f;
957 case TOWER_TYPE_GUN:
958 return 3.0f;
959 case TOWER_TYPE_WALL:
960 return 5.0f;
961 }
962 return 0.0f;
963 }
964
965 void TowerDraw()
966 {
967 for (int i = 0; i < towerCount; i++)
968 {
969 Tower tower = towers[i];
970 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
971 switch (tower.towerType)
972 {
973 case TOWER_TYPE_BASE:
974 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
975 break;
976 case TOWER_TYPE_GUN:
977 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
978 break;
979 case TOWER_TYPE_WALL:
980 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
981 break;
982 }
983 }
984 }
985
986 void TowerGunUpdate(Tower *tower)
987 {
988 if (tower->cooldown <= 0)
989 {
990 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
991 if (enemy)
992 {
993 tower->cooldown = 0.125f;
994 // shoot the enemy; determine future position of the enemy
995 float bulletSpeed = 1.0f;
996 float bulletDamage = 3.0f;
997 Vector2 velocity = enemy->simVelocity;
998 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
999 Vector2 towerPosition = {tower->x, tower->y};
1000 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
1001 for (int i = 0; i < 8; i++) {
1002 velocity = enemy->simVelocity;
1003 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
1004 float distance = Vector2Distance(towerPosition, futurePosition);
1005 float eta2 = distance / bulletSpeed;
1006 if (fabs(eta - eta2) < 0.01f) {
1007 break;
1008 }
1009 eta = (eta2 + eta) * 0.5f;
1010 }
1011 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
1012 bulletSpeed, bulletDamage);
1013 enemy->futureDamage += bulletDamage;
1014 }
1015 }
1016 else
1017 {
1018 tower->cooldown -= gameTime.deltaTime;
1019 }
1020 }
1021
1022 void TowerUpdate()
1023 {
1024 for (int i = 0; i < towerCount; i++)
1025 {
1026 Tower *tower = &towers[i];
1027 switch (tower->towerType)
1028 {
1029 case TOWER_TYPE_GUN:
1030 TowerGunUpdate(tower);
1031 break;
1032 }
1033 }
1034 }
1035
1036 //# Game
1037
1038 float nextSpawnTime = 0.0f;
1039
1040 void InitGame()
1041 {
1042 TowerInit();
1043 EnemyInit();
1044 ProjectileInit();
1045 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1046
1047 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1048 TowerTryAdd(TOWER_TYPE_GUN, 2, 0);
1049
1050 for (int i = -2; i <= 2; i += 1)
1051 {
1052 TowerTryAdd(TOWER_TYPE_WALL, i, 2);
1053 TowerTryAdd(TOWER_TYPE_WALL, i, -2);
1054 TowerTryAdd(TOWER_TYPE_WALL, -2, i);
1055 }
1056
1057 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4);
1058 }
1059
1060 void GameUpdate()
1061 {
1062 float dt = GetFrameTime();
1063 // cap maximum delta time to 0.1 seconds to prevent large time steps
1064 if (dt > 0.1f) dt = 0.1f;
1065 gameTime.time += dt;
1066 gameTime.deltaTime = dt;
1067 PathFindingMapUpdate();
1068 EnemyUpdate();
1069 TowerUpdate();
1070 ProjectileUpdate();
1071
1072 // spawn a new enemy every second
1073 if (gameTime.time >= nextSpawnTime && EnemyCount() < 50)
1074 {
1075 nextSpawnTime = gameTime.time + 0.2f;
1076 // add a new enemy at the boundary of the map
1077 int randValue = GetRandomValue(-5, 5);
1078 int randSide = GetRandomValue(0, 3);
1079 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue;
1080 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue;
1081 static int alternation = 0;
1082 alternation += 1;
1083 if (alternation % 3 == 0) {
1084 EnemyTryAdd(ENEMY_TYPE_MINION, 0, -5);
1085 }
1086 else if (alternation % 3 == 1)
1087 {
1088 EnemyTryAdd(ENEMY_TYPE_MINION, 0, 5);
1089 }
1090 EnemyTryAdd(ENEMY_TYPE_MINION, x, y);
1091 }
1092 }
1093
1094 int main(void)
1095 {
1096 int screenWidth, screenHeight;
1097 GetPreferredSize(&screenWidth, &screenHeight);
1098 InitWindow(screenWidth, screenHeight, "Tower defense");
1099 SetTargetFPS(30);
1100
1101 Camera3D camera = {0};
1102 camera.position = (Vector3){0.0f, 10.0f, -0.5f};
1103 camera.target = (Vector3){0.0f, 0.0f, -0.5f};
1104 camera.up = (Vector3){0.0f, 0.0f, -1.0f};
1105 camera.fovy = 12.0f;
1106 camera.projection = CAMERA_ORTHOGRAPHIC;
1107
1108 InitGame();
1109
1110 while (!WindowShouldClose())
1111 {
1112 if (IsPaused()) {
1113 // canvas is not visible in browser - do nothing
1114 continue;
1115 }
1116 BeginDrawing();
1117 ClearBackground(DARKBLUE);
1118
1119 BeginMode3D(camera);
1120 DrawGrid(10, 1.0f);
1121 TowerDraw();
1122 EnemyDraw();
1123 ProjectileDraw();
1124 PathFindingMapDraw();
1125 GameUpdate();
1126 EndMode3D();
1127
1128 const char *title = "Tower defense tutorial";
1129 int titleWidth = MeasureText(title, 20);
1130 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f + 2, 5 + 2, 20, BLACK);
1131 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f, 5, 20, WHITE);
1132 EndDrawing();
1133 }
1134
1135 CloseWindow();
1136
1137 return 0;
1138 }
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
Here's how the pushback works:
1 // push back enemies & dealing damage
2 for (int i = 0; i < enemyCount; i++)
3 {
4 Enemy *other = &enemies[i];
5 if (other->enemyType == ENEMY_TYPE_NONE)
6 {
7 continue;
8 }
9 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
10 if (distanceSqr > 0 && distanceSqr < explosionRange2)
11 {
12 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
13 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
14 EnemyAddDamage(other, explosionDamge);
15 }
16 }
The pushback is calculated by the distance between the enemy and the other enemy. If the distance is less than the explosion range, the other enemy is pushed back in the direction of the explosion. Like in other case, we avoid here to calculate the square root of the distance by comparing the squared distance to the squared explosion range. This is a common optimization in games, as the square root is a relatively expensive operation.
Now it would be nice to have some feedback on the damage dealt. A small explosion effect would be nice. A simple particle system would be nice to have as well, so let's add that next. Nothing fancy... just something simple that gets the job done. The definition of the particle system is in line 8 and from line 48 on, the implementation starts. From line 1160 on, The integration starts. There are also a few lines in between to spawn the particles when an enemy explodes.
1 #include "td-tut-2-main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Declarations
7
8 #define PARTICLE_MAX_COUNT 400
9 #define PARTICLE_TYPE_NONE 0
10 #define PARTICLE_TYPE_EXPLOSION 1
11
12 typedef struct Particle
13 {
14 uint8_t particleType;
15 float spawnTime;
16 float lifetime;
17 Vector3 position;
18 Vector3 velocity;
19 } Particle;
20
21 Particle particles[PARTICLE_MAX_COUNT];
22 int particleCount = 0;
23
24 #define TOWER_MAX_COUNT 400
25 #define TOWER_TYPE_NONE 0
26 #define TOWER_TYPE_BASE 1
27 #define TOWER_TYPE_GUN 2
28 #define TOWER_TYPE_WALL 3
29
30 typedef struct Tower
31 {
32 int16_t x, y;
33 uint8_t towerType;
34 float cooldown;
35 float damage;
36 } Tower;
37
38 typedef struct GameTime
39 {
40 float time;
41 float deltaTime;
42 } GameTime;
43
44 GameTime gameTime = {0};
45
46 Tower towers[TOWER_MAX_COUNT];
47 int towerCount = 0;
48
49 float TowerGetMaxHealth(Tower *tower);
50
51 //# Particle system
52
53 void ParticleInit()
54 {
55 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
56 {
57 particles[i] = (Particle){0};
58 }
59 particleCount = 0;
60 }
61
62 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
63 {
64 if (particleCount >= PARTICLE_MAX_COUNT)
65 {
66 return;
67 }
68
69 int index = -1;
70 for (int i = 0; i < particleCount; i++)
71 {
72 if (particles[i].particleType == PARTICLE_TYPE_NONE)
73 {
74 index = i;
75 break;
76 }
77 }
78
79 if (index == -1)
80 {
81 index = particleCount++;
82 }
83
84 Particle *particle = &particles[index];
85 particle->particleType = particleType;
86 particle->spawnTime = gameTime.time;
87 particle->lifetime = lifetime;
88 particle->position = position;
89 particle->velocity = velocity;
90 }
91
92 void ParticleUpdate()
93 {
94 for (int i = 0; i < particleCount; i++)
95 {
96 Particle *particle = &particles[i];
97 if (particle->particleType == PARTICLE_TYPE_NONE)
98 {
99 continue;
100 }
101
102 float age = gameTime.time - particle->spawnTime;
103
104 if (particle->lifetime > age)
105 {
106 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
107 }
108 else {
109 particle->particleType = PARTICLE_TYPE_NONE;
110 }
111 }
112 }
113
114 void DrawExplosionParticle(Particle *particle, float transition)
115 {
116 float size = 1.2f * (1.0f - transition);
117 Color startColor = WHITE;
118 Color endColor = RED;
119 Color color = ColorLerp(startColor, endColor, transition);
120 DrawCube(particle->position, size, size, size, color);
121 }
122
123 void ParticleDraw()
124 {
125 for (int i = 0; i < particleCount; i++)
126 {
127 Particle particle = particles[i];
128 if (particle.particleType == PARTICLE_TYPE_NONE)
129 {
130 continue;
131 }
132
133 float age = gameTime.time - particle.spawnTime;
134 float transition = age / particle.lifetime;
135 switch (particle.particleType)
136 {
137 case PARTICLE_TYPE_EXPLOSION:
138 DrawExplosionParticle(&particle, transition);
139 break;
140 default:
141 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
142 break;
143 }
144 }
145 }
146
147 //# Pathfinding map
148 typedef struct DeltaSrc
149 {
150 char x, y;
151 } DeltaSrc;
152
153 typedef struct PathfindingMap
154 {
155 int width, height;
156 float scale;
157 float *distances;
158 long *towerIndex;
159 DeltaSrc *deltaSrc;
160 float maxDistance;
161 Matrix toMapSpace;
162 Matrix toWorldSpace;
163 } PathfindingMap;
164
165 // when we execute the pathfinding algorithm, we need to store the active nodes
166 // in a queue. Each node has a position, a distance from the start, and the
167 // position of the node that we came from.
168 typedef struct PathfindingNode
169 {
170 int16_t x, y, fromX, fromY;
171 float distance;
172 } PathfindingNode;
173
174 // The queue is a simple array of nodes, we add nodes to the end and remove
175 // nodes from the front. We keep the array around to avoid unnecessary allocations
176 static PathfindingNode *pathfindingNodeQueue = 0;
177 static int pathfindingNodeQueueCount = 0;
178 static int pathfindingNodeQueueCapacity = 0;
179
180 // The pathfinding map stores the distances from the castle to each cell in the map.
181 PathfindingMap pathfindingMap = {0};
182
183 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
184 {
185 // transforming between map space and world space allows us to adapt
186 // position and scale of the map without changing the pathfinding data
187 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
188 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
189 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
190 pathfindingMap.width = width;
191 pathfindingMap.height = height;
192 pathfindingMap.scale = scale;
193 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
194 for (int i = 0; i < width * height; i++)
195 {
196 pathfindingMap.distances[i] = -1.0f;
197 }
198
199 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
200 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
201 }
202
203 float PathFindingGetDistance(int mapX, int mapY)
204 {
205 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
206 {
207 // when outside the map, we return the manhattan distance to the castle (0,0)
208 return fabsf((float)mapX) + fabsf((float)mapY);
209 }
210
211 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
212 }
213
214 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
215 {
216 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
217 {
218 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
219 // we use MemAlloc/MemRealloc to allocate memory for the queue
220 // I am not entirely sure if MemRealloc allows passing a null pointer
221 // so we check if the pointer is null and use MemAlloc in that case
222 if (pathfindingNodeQueue == 0)
223 {
224 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
225 }
226 else
227 {
228 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
229 }
230 }
231
232 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
233 node->x = x;
234 node->y = y;
235 node->fromX = fromX;
236 node->fromY = fromY;
237 node->distance = distance;
238 }
239
240 PathfindingNode *PathFindingNodePop()
241 {
242 if (pathfindingNodeQueueCount == 0)
243 {
244 return 0;
245 }
246 // we return the first node in the queue; we want to return a pointer to the node
247 // so we can return 0 if the queue is empty.
248 // We should _not_ return a pointer to the element in the list, because the list
249 // may be reallocated and the pointer would become invalid. Or the
250 // popped element is overwritten by the next push operation.
251 // Using static here means that the variable is permanently allocated.
252 static PathfindingNode node;
253 node = pathfindingNodeQueue[0];
254 // we shift all nodes one position to the front
255 for (int i = 1; i < pathfindingNodeQueueCount; i++)
256 {
257 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
258 }
259 --pathfindingNodeQueueCount;
260 return &node;
261 }
262
263 // transform a world position to a map position in the array;
264 // returns true if the position is inside the map
265 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
266 {
267 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
268 *mapX = (int16_t)mapPosition.x;
269 *mapY = (int16_t)mapPosition.z;
270 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
271 }
272
273 void PathFindingMapUpdate()
274 {
275 const int castleX = 0, castleY = 0;
276 int16_t castleMapX, castleMapY;
277 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
278 {
279 return;
280 }
281 int width = pathfindingMap.width, height = pathfindingMap.height;
282
283 // reset the distances to -1
284 for (int i = 0; i < width * height; i++)
285 {
286 pathfindingMap.distances[i] = -1.0f;
287 }
288 // reset the tower indices
289 for (int i = 0; i < width * height; i++)
290 {
291 pathfindingMap.towerIndex[i] = -1;
292 }
293 // reset the delta src
294 for (int i = 0; i < width * height; i++)
295 {
296 pathfindingMap.deltaSrc[i].x = 0;
297 pathfindingMap.deltaSrc[i].y = 0;
298 }
299
300 for (int i = 0; i < towerCount; i++)
301 {
302 Tower *tower = &towers[i];
303 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
304 {
305 continue;
306 }
307 int16_t mapX, mapY;
308 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
309 // this would not work correctly and needs to be refined to allow towers covering multiple cells
310 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
311 // one cell. For now.
312 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
313 {
314 continue;
315 }
316 int index = mapY * width + mapX;
317 pathfindingMap.towerIndex[index] = i;
318 }
319
320 // we start at the castle and add the castle to the queue
321 pathfindingMap.maxDistance = 0.0f;
322 pathfindingNodeQueueCount = 0;
323 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
324 PathfindingNode *node = 0;
325 while ((node = PathFindingNodePop()))
326 {
327 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
328 {
329 continue;
330 }
331 int index = node->y * width + node->x;
332 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
333 {
334 continue;
335 }
336
337 int deltaX = node->x - node->fromX;
338 int deltaY = node->y - node->fromY;
339 // even if the cell is blocked by a tower, we still may want to store the direction
340 // (though this might not be needed, IDK right now)
341 pathfindingMap.deltaSrc[index].x = (char) deltaX;
342 pathfindingMap.deltaSrc[index].y = (char) deltaY;
343
344 // we skip nodes that are blocked by towers
345 if (pathfindingMap.towerIndex[index] >= 0)
346 {
347 node->distance += 8.0f;
348 }
349 pathfindingMap.distances[index] = node->distance;
350 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
351 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
352 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
353 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
354 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
355 }
356 }
357
358 void PathFindingMapDraw()
359 {
360 float cellSize = pathfindingMap.scale * 0.9f;
361 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
362 for (int x = 0; x < pathfindingMap.width; x++)
363 {
364 for (int y = 0; y < pathfindingMap.height; y++)
365 {
366 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
367 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
368 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
369 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
370 // animate the distance "wave" to show how the pathfinding algorithm expands
371 // from the castle
372 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
373 {
374 color = BLACK;
375 }
376 DrawCube(position, cellSize, 0.1f, cellSize, color);
377 }
378 }
379 }
380
381 Vector2 PathFindingGetGradient(Vector3 world)
382 {
383 int16_t mapX, mapY;
384 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
385 {
386 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
387 return (Vector2){(float)-delta.x, (float)-delta.y};
388 }
389 // fallback to a simple gradient calculation
390 float n = PathFindingGetDistance(mapX, mapY - 1);
391 float s = PathFindingGetDistance(mapX, mapY + 1);
392 float w = PathFindingGetDistance(mapX - 1, mapY);
393 float e = PathFindingGetDistance(mapX + 1, mapY);
394 return (Vector2){w - e + 0.25f, n - s + 0.125f};
395 }
396
397 //# Enemies
398
399 #define ENEMY_MAX_PATH_COUNT 8
400 #define ENEMY_MAX_COUNT 400
401 #define ENEMY_TYPE_NONE 0
402 #define ENEMY_TYPE_MINION 1
403
404 typedef struct EnemyId
405 {
406 uint16_t index;
407 uint16_t generation;
408 } EnemyId;
409
410 typedef struct EnemyClassConfig
411 {
412 float speed;
413 float health;
414 float radius;
415 float maxAcceleration;
416 float requiredContactTime;
417 float explosionDamage;
418 float explosionRange;
419 float explosionPushbackPower;
420 } EnemyClassConfig;
421
422 typedef struct Enemy
423 {
424 int16_t currentX, currentY;
425 int16_t nextX, nextY;
426 Vector2 simPosition;
427 Vector2 simVelocity;
428 uint16_t generation;
429 float startMovingTime;
430 float damage, futureDamage;
431 float contactTime;
432 uint8_t enemyType;
433 uint8_t movePathCount;
434 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
435 } Enemy;
436
437 Enemy enemies[ENEMY_MAX_COUNT];
438 int enemyCount = 0;
439
440 EnemyClassConfig enemyClassConfigs[] = {
441 [ENEMY_TYPE_MINION] = {
442 .health = 3.0f,
443 .speed = 1.0f,
444 .radius = 0.25f,
445 .maxAcceleration = 1.0f,
446 .explosionDamage = 1.0f,
447 .requiredContactTime = 0.5f,
448 .explosionRange = 1.0f,
449 .explosionPushbackPower = 0.25f,
450 },
451 };
452
453 int EnemyAddDamage(Enemy *enemy, float damage);
454
455 void EnemyInit()
456 {
457 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
458 {
459 enemies[i] = (Enemy){0};
460 }
461 enemyCount = 0;
462 }
463
464 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
465 {
466 return enemyClassConfigs[enemy->enemyType].speed;
467 }
468
469 float EnemyGetMaxHealth(Enemy *enemy)
470 {
471 return enemyClassConfigs[enemy->enemyType].health;
472 }
473
474 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
475 {
476 int16_t castleX = 0;
477 int16_t castleY = 0;
478 int16_t dx = castleX - currentX;
479 int16_t dy = castleY - currentY;
480 if (dx == 0 && dy == 0)
481 {
482 *nextX = currentX;
483 *nextY = currentY;
484 return 1;
485 }
486 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
487
488 if (gradient.x == 0 && gradient.y == 0)
489 {
490 *nextX = currentX;
491 *nextY = currentY;
492 return 1;
493 }
494
495 if (fabsf(gradient.x) > fabsf(gradient.y))
496 {
497 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
498 *nextY = currentY;
499 return 0;
500 }
501 *nextX = currentX;
502 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
503 return 0;
504 }
505
506
507 // this function predicts the movement of the unit for the next deltaT seconds
508 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
509 {
510 const float pointReachedDistance = 0.25f;
511 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
512 const float maxSimStepTime = 0.015625f;
513
514 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
515 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
516 int16_t nextX = enemy->nextX;
517 int16_t nextY = enemy->nextY;
518 Vector2 position = enemy->simPosition;
519 int passedCount = 0;
520 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
521 {
522 float stepTime = fminf(deltaT - t, maxSimStepTime);
523 Vector2 target = (Vector2){nextX, nextY};
524 float speed = Vector2Length(*velocity);
525 // draw the target position for debugging
526 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
527 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
528 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
529 {
530 // we reached the target position, let's move to the next waypoint
531 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
532 target = (Vector2){nextX, nextY};
533 // track how many waypoints we passed
534 passedCount++;
535 }
536
537 // acceleration towards the target
538 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
539 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
540 *velocity = Vector2Add(*velocity, acceleration);
541
542 // limit the speed to the maximum speed
543 if (speed > maxSpeed)
544 {
545 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
546 }
547
548 // move the enemy
549 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
550 }
551
552 if (waypointPassedCount)
553 {
554 (*waypointPassedCount) = passedCount;
555 }
556
557 return position;
558 }
559
560 void EnemyDraw()
561 {
562 for (int i = 0; i < enemyCount; i++)
563 {
564 Enemy enemy = enemies[i];
565 if (enemy.enemyType == ENEMY_TYPE_NONE)
566 {
567 continue;
568 }
569
570 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
571
572 if (enemy.movePathCount > 0)
573 {
574 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
575 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
576 }
577 for (int j = 1; j < enemy.movePathCount; j++)
578 {
579 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
580 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
581 DrawLine3D(p, q, GREEN);
582 }
583
584 switch (enemy.enemyType)
585 {
586 case ENEMY_TYPE_MINION:
587 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
588 break;
589 }
590 }
591 }
592
593 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
594 {
595 // damage the tower
596 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
597 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
598 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
599 float explosionRange2 = explosionRange * explosionRange;
600 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
601 // explode the enemy
602 if (tower->damage >= TowerGetMaxHealth(tower))
603 {
604 tower->towerType = TOWER_TYPE_NONE;
605 }
606
607 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
608 explosionSource,
609 (Vector3){0, 0.1f, 0}, 1.0f);
610
611 enemy->enemyType = ENEMY_TYPE_NONE;
612
613 // push back enemies & dealing damage
614 for (int i = 0; i < enemyCount; i++)
615 {
616 Enemy *other = &enemies[i];
617 if (other->enemyType == ENEMY_TYPE_NONE)
618 {
619 continue;
620 }
621 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
622 if (distanceSqr > 0 && distanceSqr < explosionRange2)
623 {
624 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
625 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
626 EnemyAddDamage(other, explosionDamge);
627 }
628 }
629 }
630
631 void EnemyUpdate()
632 {
633 const float castleX = 0;
634 const float castleY = 0;
635 const float maxPathDistance2 = 0.25f * 0.25f;
636
637 for (int i = 0; i < enemyCount; i++)
638 {
639 Enemy *enemy = &enemies[i];
640 if (enemy->enemyType == ENEMY_TYPE_NONE)
641 {
642 continue;
643 }
644
645 int waypointPassedCount = 0;
646 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
647 enemy->startMovingTime = gameTime.time;
648 // track path of unit
649 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
650 {
651 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
652 {
653 enemy->movePath[j] = enemy->movePath[j - 1];
654 }
655 enemy->movePath[0] = enemy->simPosition;
656 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
657 {
658 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
659 }
660 }
661
662 if (waypointPassedCount > 0)
663 {
664 enemy->currentX = enemy->nextX;
665 enemy->currentY = enemy->nextY;
666 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
667 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
668 {
669 // enemy reached the castle; remove it
670 enemy->enemyType = ENEMY_TYPE_NONE;
671 continue;
672 }
673 }
674 }
675
676 // handle collisions between enemies
677 for (int i = 0; i < enemyCount - 1; i++)
678 {
679 Enemy *enemyA = &enemies[i];
680 if (enemyA->enemyType == ENEMY_TYPE_NONE)
681 {
682 continue;
683 }
684 for (int j = i + 1; j < enemyCount; j++)
685 {
686 Enemy *enemyB = &enemies[j];
687 if (enemyB->enemyType == ENEMY_TYPE_NONE)
688 {
689 continue;
690 }
691 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
692 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
693 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
694 float radiusSum = radiusA + radiusB;
695 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
696 {
697 // collision
698 float distance = sqrtf(distanceSqr);
699 float overlap = radiusSum - distance;
700 // move the enemies apart, but softly; if we have a clog of enemies,
701 // moving them perfectly apart can cause them to jitter
702 float positionCorrection = overlap / 5.0f;
703 Vector2 direction = (Vector2){
704 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
705 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
706 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
707 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
708 }
709 }
710 }
711
712 // handle collisions between enemies and towers
713 for (int i = 0; i < enemyCount; i++)
714 {
715 Enemy *enemy = &enemies[i];
716 if (enemy->enemyType == ENEMY_TYPE_NONE)
717 {
718 continue;
719 }
720 enemy->contactTime -= gameTime.deltaTime;
721 if (enemy->contactTime < 0.0f)
722 {
723 enemy->contactTime = 0.0f;
724 }
725
726 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
727 // linear search over towers; could be optimized by using path finding tower map,
728 // but for now, we keep it simple
729 for (int j = 0; j < towerCount; j++)
730 {
731 Tower *tower = &towers[j];
732 if (tower->towerType == TOWER_TYPE_NONE)
733 {
734 continue;
735 }
736 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
737 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
738 if (distanceSqr > combinedRadius * combinedRadius)
739 {
740 continue;
741 }
742 // potential collision; square / circle intersection
743 float dx = tower->x - enemy->simPosition.x;
744 float dy = tower->y - enemy->simPosition.y;
745 float absDx = fabsf(dx);
746 float absDy = fabsf(dy);
747 Vector3 contactPoint = {0};
748 if (absDx <= 0.5f && absDx <= absDy) {
749 // vertical collision; push the enemy out horizontally
750 float overlap = enemyRadius + 0.5f - absDy;
751 if (overlap < 0.0f)
752 {
753 continue;
754 }
755 float direction = dy > 0.0f ? -1.0f : 1.0f;
756 enemy->simPosition.y += direction * overlap;
757 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
758 }
759 else if (absDy <= 0.5f && absDy <= absDx)
760 {
761 // horizontal collision; push the enemy out vertically
762 float overlap = enemyRadius + 0.5f - absDx;
763 if (overlap < 0.0f)
764 {
765 continue;
766 }
767 float direction = dx > 0.0f ? -1.0f : 1.0f;
768 enemy->simPosition.x += direction * overlap;
769 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
770 }
771 else
772 {
773 // possible collision with a corner
774 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
775 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
776 float cornerX = tower->x + cornerDX;
777 float cornerY = tower->y + cornerDY;
778 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
779 if (cornerDistanceSqr > enemyRadius * enemyRadius)
780 {
781 continue;
782 }
783 // push the enemy out along the diagonal
784 float cornerDistance = sqrtf(cornerDistanceSqr);
785 float overlap = enemyRadius - cornerDistance;
786 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
787 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
788 enemy->simPosition.x -= directionX * overlap;
789 enemy->simPosition.y -= directionY * overlap;
790 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
791 }
792
793 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
794 {
795 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
796 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
797 {
798 EnemyTriggerExplode(enemy, tower, contactPoint);
799 }
800 }
801 }
802 }
803 }
804
805 EnemyId EnemyGetId(Enemy *enemy)
806 {
807 return (EnemyId){enemy - enemies, enemy->generation};
808 }
809
810 Enemy *EnemyTryResolve(EnemyId enemyId)
811 {
812 if (enemyId.index >= ENEMY_MAX_COUNT)
813 {
814 return 0;
815 }
816 Enemy *enemy = &enemies[enemyId.index];
817 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
818 {
819 return 0;
820 }
821 return enemy;
822 }
823
824 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
825 {
826 Enemy *spawn = 0;
827 for (int i = 0; i < enemyCount; i++)
828 {
829 Enemy *enemy = &enemies[i];
830 if (enemy->enemyType == ENEMY_TYPE_NONE)
831 {
832 spawn = enemy;
833 break;
834 }
835 }
836
837 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
838 {
839 spawn = &enemies[enemyCount++];
840 }
841
842 if (spawn)
843 {
844 spawn->currentX = currentX;
845 spawn->currentY = currentY;
846 spawn->nextX = currentX;
847 spawn->nextY = currentY;
848 spawn->simPosition = (Vector2){currentX, currentY};
849 spawn->simVelocity = (Vector2){0, 0};
850 spawn->enemyType = enemyType;
851 spawn->startMovingTime = gameTime.time;
852 spawn->damage = 0.0f;
853 spawn->futureDamage = 0.0f;
854 spawn->generation++;
855 spawn->movePathCount = 0;
856 }
857
858 return spawn;
859 }
860
861 int EnemyAddDamage(Enemy *enemy, float damage)
862 {
863 enemy->damage += damage;
864 if (enemy->damage >= EnemyGetMaxHealth(enemy))
865 {
866 enemy->enemyType = ENEMY_TYPE_NONE;
867 return 1;
868 }
869
870 return 0;
871 }
872
873 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
874 {
875 int16_t castleX = 0;
876 int16_t castleY = 0;
877 Enemy* closest = 0;
878 int16_t closestDistance = 0;
879 float range2 = range * range;
880 for (int i = 0; i < enemyCount; i++)
881 {
882 Enemy* enemy = &enemies[i];
883 if (enemy->enemyType == ENEMY_TYPE_NONE)
884 {
885 continue;
886 }
887 float maxHealth = EnemyGetMaxHealth(enemy);
888 if (enemy->futureDamage >= maxHealth)
889 {
890 // ignore enemies that will die soon
891 continue;
892 }
893 int16_t dx = castleX - enemy->currentX;
894 int16_t dy = castleY - enemy->currentY;
895 int16_t distance = abs(dx) + abs(dy);
896 if (!closest || distance < closestDistance)
897 {
898 float tdx = towerX - enemy->currentX;
899 float tdy = towerY - enemy->currentY;
900 float tdistance2 = tdx * tdx + tdy * tdy;
901 if (tdistance2 <= range2)
902 {
903 closest = enemy;
904 closestDistance = distance;
905 }
906 }
907 }
908 return closest;
909 }
910
911 int EnemyCount()
912 {
913 int count = 0;
914 for (int i = 0; i < enemyCount; i++)
915 {
916 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
917 {
918 count++;
919 }
920 }
921 return count;
922 }
923
924 //# Projectiles
925 #define PROJECTILE_MAX_COUNT 1200
926 #define PROJECTILE_TYPE_NONE 0
927 #define PROJECTILE_TYPE_BULLET 1
928
929 typedef struct Projectile
930 {
931 uint8_t projectileType;
932 float shootTime;
933 float arrivalTime;
934 float damage;
935 Vector2 position;
936 Vector2 target;
937 Vector2 directionNormal;
938 EnemyId targetEnemy;
939 } Projectile;
940
941 Projectile projectiles[PROJECTILE_MAX_COUNT];
942 int projectileCount = 0;
943
944 void ProjectileInit()
945 {
946 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
947 {
948 projectiles[i] = (Projectile){0};
949 }
950 }
951
952 void ProjectileDraw()
953 {
954 for (int i = 0; i < projectileCount; i++)
955 {
956 Projectile projectile = projectiles[i];
957 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
958 {
959 continue;
960 }
961 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
962 if (transition >= 1.0f)
963 {
964 continue;
965 }
966 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
967 float x = position.x;
968 float y = position.y;
969 float dx = projectile.directionNormal.x;
970 float dy = projectile.directionNormal.y;
971 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
972 {
973 x -= dx * 0.1f;
974 y -= dy * 0.1f;
975 float size = 0.1f * d;
976 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
977 }
978 }
979 }
980
981 void ProjectileUpdate()
982 {
983 for (int i = 0; i < projectileCount; i++)
984 {
985 Projectile *projectile = &projectiles[i];
986 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
987 {
988 continue;
989 }
990 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
991 if (transition >= 1.0f)
992 {
993 projectile->projectileType = PROJECTILE_TYPE_NONE;
994 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
995 if (enemy)
996 {
997 EnemyAddDamage(enemy, projectile->damage);
998 }
999 continue;
1000 }
1001 }
1002 }
1003
1004 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
1005 {
1006 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
1007 {
1008 Projectile *projectile = &projectiles[i];
1009 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
1010 {
1011 projectile->projectileType = projectileType;
1012 projectile->shootTime = gameTime.time;
1013 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
1014 projectile->damage = damage;
1015 projectile->position = position;
1016 projectile->target = target;
1017 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
1018 projectile->targetEnemy = EnemyGetId(enemy);
1019 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
1020 return projectile;
1021 }
1022 }
1023 return 0;
1024 }
1025
1026 //# Towers
1027
1028 void TowerInit()
1029 {
1030 for (int i = 0; i < TOWER_MAX_COUNT; i++)
1031 {
1032 towers[i] = (Tower){0};
1033 }
1034 towerCount = 0;
1035 }
1036
1037 Tower *TowerGetAt(int16_t x, int16_t y)
1038 {
1039 for (int i = 0; i < towerCount; i++)
1040 {
1041 if (towers[i].x == x && towers[i].y == y)
1042 {
1043 return &towers[i];
1044 }
1045 }
1046 return 0;
1047 }
1048
1049 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
1050 {
1051 if (towerCount >= TOWER_MAX_COUNT)
1052 {
1053 return 0;
1054 }
1055
1056 Tower *tower = TowerGetAt(x, y);
1057 if (tower)
1058 {
1059 return 0;
1060 }
1061
1062 tower = &towers[towerCount++];
1063 tower->x = x;
1064 tower->y = y;
1065 tower->towerType = towerType;
1066 tower->cooldown = 0.0f;
1067 tower->damage = 0.0f;
1068 return tower;
1069 }
1070
1071 float TowerGetMaxHealth(Tower *tower)
1072 {
1073 switch (tower->towerType)
1074 {
1075 case TOWER_TYPE_BASE:
1076 return 10.0f;
1077 case TOWER_TYPE_GUN:
1078 return 3.0f;
1079 case TOWER_TYPE_WALL:
1080 return 5.0f;
1081 }
1082 return 0.0f;
1083 }
1084
1085 void TowerDraw()
1086 {
1087 for (int i = 0; i < towerCount; i++)
1088 {
1089 Tower tower = towers[i];
1090 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
1091 switch (tower.towerType)
1092 {
1093 case TOWER_TYPE_BASE:
1094 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
1095 break;
1096 case TOWER_TYPE_GUN:
1097 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
1098 break;
1099 case TOWER_TYPE_WALL:
1100 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
1101 break;
1102 }
1103 }
1104 }
1105
1106 void TowerGunUpdate(Tower *tower)
1107 {
1108 if (tower->cooldown <= 0)
1109 {
1110 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
1111 if (enemy)
1112 {
1113 tower->cooldown = 0.125f;
1114 // shoot the enemy; determine future position of the enemy
1115 float bulletSpeed = 1.0f;
1116 float bulletDamage = 3.0f;
1117 Vector2 velocity = enemy->simVelocity;
1118 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
1119 Vector2 towerPosition = {tower->x, tower->y};
1120 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
1121 for (int i = 0; i < 8; i++) {
1122 velocity = enemy->simVelocity;
1123 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
1124 float distance = Vector2Distance(towerPosition, futurePosition);
1125 float eta2 = distance / bulletSpeed;
1126 if (fabs(eta - eta2) < 0.01f) {
1127 break;
1128 }
1129 eta = (eta2 + eta) * 0.5f;
1130 }
1131 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
1132 bulletSpeed, bulletDamage);
1133 enemy->futureDamage += bulletDamage;
1134 }
1135 }
1136 else
1137 {
1138 tower->cooldown -= gameTime.deltaTime;
1139 }
1140 }
1141
1142 void TowerUpdate()
1143 {
1144 for (int i = 0; i < towerCount; i++)
1145 {
1146 Tower *tower = &towers[i];
1147 switch (tower->towerType)
1148 {
1149 case TOWER_TYPE_GUN:
1150 TowerGunUpdate(tower);
1151 break;
1152 }
1153 }
1154 }
1155
1156 //# Game
1157
1158 float nextSpawnTime = 0.0f;
1159
1160 void InitGame()
1161 {
1162 TowerInit();
1163 EnemyInit();
1164 ProjectileInit();
1165 ParticleInit();
1166 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1167
1168 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1169 TowerTryAdd(TOWER_TYPE_GUN, 2, 0);
1170
1171 for (int i = -2; i <= 2; i += 1)
1172 {
1173 TowerTryAdd(TOWER_TYPE_WALL, i, 2);
1174 TowerTryAdd(TOWER_TYPE_WALL, i, -2);
1175 TowerTryAdd(TOWER_TYPE_WALL, -2, i);
1176 }
1177
1178 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4);
1179 }
1180
1181 void GameUpdate()
1182 {
1183 float dt = GetFrameTime();
1184 // cap maximum delta time to 0.1 seconds to prevent large time steps
1185 if (dt > 0.1f) dt = 0.1f;
1186 gameTime.time += dt;
1187 gameTime.deltaTime = dt;
1188 PathFindingMapUpdate();
1189 EnemyUpdate();
1190 TowerUpdate();
1191 ProjectileUpdate();
1192 ParticleUpdate();
1193
1194 // spawn a new enemy every second
1195 if (gameTime.time >= nextSpawnTime && EnemyCount() < 50)
1196 {
1197 nextSpawnTime = gameTime.time + 0.2f;
1198 // add a new enemy at the boundary of the map
1199 int randValue = GetRandomValue(-5, 5);
1200 int randSide = GetRandomValue(0, 3);
1201 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue;
1202 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue;
1203 static int alternation = 0;
1204 alternation += 1;
1205 if (alternation % 3 == 0) {
1206 EnemyTryAdd(ENEMY_TYPE_MINION, 0, -5);
1207 }
1208 else if (alternation % 3 == 1)
1209 {
1210 EnemyTryAdd(ENEMY_TYPE_MINION, 0, 5);
1211 }
1212 EnemyTryAdd(ENEMY_TYPE_MINION, x, y);
1213 }
1214 }
1215
1216 int main(void)
1217 {
1218 int screenWidth, screenHeight;
1219 GetPreferredSize(&screenWidth, &screenHeight);
1220 InitWindow(screenWidth, screenHeight, "Tower defense");
1221 SetTargetFPS(30);
1222
1223 Camera3D camera = {0};
1224 camera.position = (Vector3){0.0f, 10.0f, -0.5f};
1225 camera.target = (Vector3){0.0f, 0.0f, -0.5f};
1226 camera.up = (Vector3){0.0f, 0.0f, -1.0f};
1227 camera.fovy = 12.0f;
1228 camera.projection = CAMERA_ORTHOGRAPHIC;
1229
1230 InitGame();
1231
1232 while (!WindowShouldClose())
1233 {
1234 if (IsPaused()) {
1235 // canvas is not visible in browser - do nothing
1236 continue;
1237 }
1238 BeginDrawing();
1239 ClearBackground(DARKBLUE);
1240
1241 BeginMode3D(camera);
1242 DrawGrid(10, 1.0f);
1243 TowerDraw();
1244 EnemyDraw();
1245 ProjectileDraw();
1246 PathFindingMapDraw();
1247 ParticleDraw();
1248 GameUpdate();
1249 EndMode3D();
1250
1251 const char *title = "Tower defense tutorial";
1252 int titleWidth = MeasureText(title, 20);
1253 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f + 2, 5 + 2, 20, BLACK);
1254 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f, 5, 20, WHITE);
1255 EndDrawing();
1256 }
1257
1258 CloseWindow();
1259
1260 return 0;
1261 }
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 we have a little back of feedback and a simple particle system that we can later extend.
It's time to add an interface to build new buildings. We'll use the approach of immediate GUI for this. In case you are not familiar with the concept, here's a quick explanation: An immediate GUI system describes the interface purely in code; draw instructions and event handling happens at the same time. To illustrate the usage, this is how it looks like (pseudo code):
1 if (Button("Build Archer", 10, 10, 80, 30))
2 {
3 buildingType = BUILDING_TYPE_ARCHER;
4 }
In the above example, the function Button draws a button at the position 10, 10 with a size of 80x30 pixels. If the button is clicked, the function returns true and therefore the building type is set.
The other, more common approach are retained GUI systems, where GUI elements are objects that are created and manipulated. They typically use events to communicate with the application. For example, this is how the same button would look like in a retained GUI system:
1 void InitGUI()
2 {
3 Button *button = new Button("Build Archer", 10, 10, 80, 30);
4 button->onClick = OnBuildArcher;
5 }
6
7 void OnBuildArcher()
8 {
9 buildingType = BUILDING_TYPE_ARCHER;
10 }
Retained GUI systems are more complex to implement and use, but they are often combined with visual authoring tools for creating complex interface layouts. Layouting immediate GUIs usually happens in code, which isn't artist-friendly. Immediate GUIs are often used for development tools where visual appeal is less important than speed and ease of use. A very popuplar immediate GUI system is Dear ImGui, which is used in many game engines and tools. It is quite straight forward to use and has a lot of features. For our game, we'll implement a simple immediate GUI system ourselves, as we only need a few buttons and labels.
To get started with a simple immediate GUI system, I will simply add a button to toggle the camera between two perspectives, just to see how our game is looking from a different angle - and to make sure that when we start interacting with the world, our system works in any camera setup:
1 #include "td-tut-2-main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Declarations
7
8 #define PARTICLE_MAX_COUNT 400
9 #define PARTICLE_TYPE_NONE 0
10 #define PARTICLE_TYPE_EXPLOSION 1
11
12 typedef struct Particle
13 {
14 uint8_t particleType;
15 float spawnTime;
16 float lifetime;
17 Vector3 position;
18 Vector3 velocity;
19 } Particle;
20
21 Particle particles[PARTICLE_MAX_COUNT];
22 int particleCount = 0;
23
24 #define TOWER_MAX_COUNT 400
25 #define TOWER_TYPE_NONE 0
26 #define TOWER_TYPE_BASE 1
27 #define TOWER_TYPE_GUN 2
28 #define TOWER_TYPE_WALL 3
29
30 typedef struct Tower
31 {
32 int16_t x, y;
33 uint8_t towerType;
34 float cooldown;
35 float damage;
36 } Tower;
37
38 typedef struct GameTime
39 {
40 float time;
41 float deltaTime;
42 } GameTime;
43
44 GameTime gameTime = {0};
45
46 Tower towers[TOWER_MAX_COUNT];
47 int towerCount = 0;
48
49 float TowerGetMaxHealth(Tower *tower);
50
51 //# Particle system
52
53 void ParticleInit()
54 {
55 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
56 {
57 particles[i] = (Particle){0};
58 }
59 particleCount = 0;
60 }
61
62 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
63 {
64 if (particleCount >= PARTICLE_MAX_COUNT)
65 {
66 return;
67 }
68
69 int index = -1;
70 for (int i = 0; i < particleCount; i++)
71 {
72 if (particles[i].particleType == PARTICLE_TYPE_NONE)
73 {
74 index = i;
75 break;
76 }
77 }
78
79 if (index == -1)
80 {
81 index = particleCount++;
82 }
83
84 Particle *particle = &particles[index];
85 particle->particleType = particleType;
86 particle->spawnTime = gameTime.time;
87 particle->lifetime = lifetime;
88 particle->position = position;
89 particle->velocity = velocity;
90 }
91
92 void ParticleUpdate()
93 {
94 for (int i = 0; i < particleCount; i++)
95 {
96 Particle *particle = &particles[i];
97 if (particle->particleType == PARTICLE_TYPE_NONE)
98 {
99 continue;
100 }
101
102 float age = gameTime.time - particle->spawnTime;
103
104 if (particle->lifetime > age)
105 {
106 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
107 }
108 else {
109 particle->particleType = PARTICLE_TYPE_NONE;
110 }
111 }
112 }
113
114 void DrawExplosionParticle(Particle *particle, float transition)
115 {
116 float size = 1.2f * (1.0f - transition);
117 Color startColor = WHITE;
118 Color endColor = RED;
119 Color color = ColorLerp(startColor, endColor, transition);
120 DrawCube(particle->position, size, size, size, color);
121 }
122
123 void ParticleDraw()
124 {
125 for (int i = 0; i < particleCount; i++)
126 {
127 Particle particle = particles[i];
128 if (particle.particleType == PARTICLE_TYPE_NONE)
129 {
130 continue;
131 }
132
133 float age = gameTime.time - particle.spawnTime;
134 float transition = age / particle.lifetime;
135 switch (particle.particleType)
136 {
137 case PARTICLE_TYPE_EXPLOSION:
138 DrawExplosionParticle(&particle, transition);
139 break;
140 default:
141 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
142 break;
143 }
144 }
145 }
146
147 //# Pathfinding map
148 typedef struct DeltaSrc
149 {
150 char x, y;
151 } DeltaSrc;
152
153 typedef struct PathfindingMap
154 {
155 int width, height;
156 float scale;
157 float *distances;
158 long *towerIndex;
159 DeltaSrc *deltaSrc;
160 float maxDistance;
161 Matrix toMapSpace;
162 Matrix toWorldSpace;
163 } PathfindingMap;
164
165 // when we execute the pathfinding algorithm, we need to store the active nodes
166 // in a queue. Each node has a position, a distance from the start, and the
167 // position of the node that we came from.
168 typedef struct PathfindingNode
169 {
170 int16_t x, y, fromX, fromY;
171 float distance;
172 } PathfindingNode;
173
174 // The queue is a simple array of nodes, we add nodes to the end and remove
175 // nodes from the front. We keep the array around to avoid unnecessary allocations
176 static PathfindingNode *pathfindingNodeQueue = 0;
177 static int pathfindingNodeQueueCount = 0;
178 static int pathfindingNodeQueueCapacity = 0;
179
180 // The pathfinding map stores the distances from the castle to each cell in the map.
181 PathfindingMap pathfindingMap = {0};
182
183 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
184 {
185 // transforming between map space and world space allows us to adapt
186 // position and scale of the map without changing the pathfinding data
187 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
188 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
189 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
190 pathfindingMap.width = width;
191 pathfindingMap.height = height;
192 pathfindingMap.scale = scale;
193 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
194 for (int i = 0; i < width * height; i++)
195 {
196 pathfindingMap.distances[i] = -1.0f;
197 }
198
199 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
200 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
201 }
202
203 float PathFindingGetDistance(int mapX, int mapY)
204 {
205 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
206 {
207 // when outside the map, we return the manhattan distance to the castle (0,0)
208 return fabsf((float)mapX) + fabsf((float)mapY);
209 }
210
211 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
212 }
213
214 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
215 {
216 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
217 {
218 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
219 // we use MemAlloc/MemRealloc to allocate memory for the queue
220 // I am not entirely sure if MemRealloc allows passing a null pointer
221 // so we check if the pointer is null and use MemAlloc in that case
222 if (pathfindingNodeQueue == 0)
223 {
224 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
225 }
226 else
227 {
228 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
229 }
230 }
231
232 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
233 node->x = x;
234 node->y = y;
235 node->fromX = fromX;
236 node->fromY = fromY;
237 node->distance = distance;
238 }
239
240 PathfindingNode *PathFindingNodePop()
241 {
242 if (pathfindingNodeQueueCount == 0)
243 {
244 return 0;
245 }
246 // we return the first node in the queue; we want to return a pointer to the node
247 // so we can return 0 if the queue is empty.
248 // We should _not_ return a pointer to the element in the list, because the list
249 // may be reallocated and the pointer would become invalid. Or the
250 // popped element is overwritten by the next push operation.
251 // Using static here means that the variable is permanently allocated.
252 static PathfindingNode node;
253 node = pathfindingNodeQueue[0];
254 // we shift all nodes one position to the front
255 for (int i = 1; i < pathfindingNodeQueueCount; i++)
256 {
257 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
258 }
259 --pathfindingNodeQueueCount;
260 return &node;
261 }
262
263 // transform a world position to a map position in the array;
264 // returns true if the position is inside the map
265 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
266 {
267 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
268 *mapX = (int16_t)mapPosition.x;
269 *mapY = (int16_t)mapPosition.z;
270 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
271 }
272
273 void PathFindingMapUpdate()
274 {
275 const int castleX = 0, castleY = 0;
276 int16_t castleMapX, castleMapY;
277 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
278 {
279 return;
280 }
281 int width = pathfindingMap.width, height = pathfindingMap.height;
282
283 // reset the distances to -1
284 for (int i = 0; i < width * height; i++)
285 {
286 pathfindingMap.distances[i] = -1.0f;
287 }
288 // reset the tower indices
289 for (int i = 0; i < width * height; i++)
290 {
291 pathfindingMap.towerIndex[i] = -1;
292 }
293 // reset the delta src
294 for (int i = 0; i < width * height; i++)
295 {
296 pathfindingMap.deltaSrc[i].x = 0;
297 pathfindingMap.deltaSrc[i].y = 0;
298 }
299
300 for (int i = 0; i < towerCount; i++)
301 {
302 Tower *tower = &towers[i];
303 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
304 {
305 continue;
306 }
307 int16_t mapX, mapY;
308 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
309 // this would not work correctly and needs to be refined to allow towers covering multiple cells
310 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
311 // one cell. For now.
312 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
313 {
314 continue;
315 }
316 int index = mapY * width + mapX;
317 pathfindingMap.towerIndex[index] = i;
318 }
319
320 // we start at the castle and add the castle to the queue
321 pathfindingMap.maxDistance = 0.0f;
322 pathfindingNodeQueueCount = 0;
323 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
324 PathfindingNode *node = 0;
325 while ((node = PathFindingNodePop()))
326 {
327 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
328 {
329 continue;
330 }
331 int index = node->y * width + node->x;
332 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
333 {
334 continue;
335 }
336
337 int deltaX = node->x - node->fromX;
338 int deltaY = node->y - node->fromY;
339 // even if the cell is blocked by a tower, we still may want to store the direction
340 // (though this might not be needed, IDK right now)
341 pathfindingMap.deltaSrc[index].x = (char) deltaX;
342 pathfindingMap.deltaSrc[index].y = (char) deltaY;
343
344 // we skip nodes that are blocked by towers
345 if (pathfindingMap.towerIndex[index] >= 0)
346 {
347 node->distance += 8.0f;
348 }
349 pathfindingMap.distances[index] = node->distance;
350 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
351 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
352 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
353 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
354 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
355 }
356 }
357
358 void PathFindingMapDraw()
359 {
360 float cellSize = pathfindingMap.scale * 0.9f;
361 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
362 for (int x = 0; x < pathfindingMap.width; x++)
363 {
364 for (int y = 0; y < pathfindingMap.height; y++)
365 {
366 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
367 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
368 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
369 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
370 // animate the distance "wave" to show how the pathfinding algorithm expands
371 // from the castle
372 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
373 {
374 color = BLACK;
375 }
376 DrawCube(position, cellSize, 0.1f, cellSize, color);
377 }
378 }
379 }
380
381 Vector2 PathFindingGetGradient(Vector3 world)
382 {
383 int16_t mapX, mapY;
384 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
385 {
386 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
387 return (Vector2){(float)-delta.x, (float)-delta.y};
388 }
389 // fallback to a simple gradient calculation
390 float n = PathFindingGetDistance(mapX, mapY - 1);
391 float s = PathFindingGetDistance(mapX, mapY + 1);
392 float w = PathFindingGetDistance(mapX - 1, mapY);
393 float e = PathFindingGetDistance(mapX + 1, mapY);
394 return (Vector2){w - e + 0.25f, n - s + 0.125f};
395 }
396
397 //# Enemies
398
399 #define ENEMY_MAX_PATH_COUNT 8
400 #define ENEMY_MAX_COUNT 400
401 #define ENEMY_TYPE_NONE 0
402 #define ENEMY_TYPE_MINION 1
403
404 typedef struct EnemyId
405 {
406 uint16_t index;
407 uint16_t generation;
408 } EnemyId;
409
410 typedef struct EnemyClassConfig
411 {
412 float speed;
413 float health;
414 float radius;
415 float maxAcceleration;
416 float requiredContactTime;
417 float explosionDamage;
418 float explosionRange;
419 float explosionPushbackPower;
420 } EnemyClassConfig;
421
422 typedef struct Enemy
423 {
424 int16_t currentX, currentY;
425 int16_t nextX, nextY;
426 Vector2 simPosition;
427 Vector2 simVelocity;
428 uint16_t generation;
429 float startMovingTime;
430 float damage, futureDamage;
431 float contactTime;
432 uint8_t enemyType;
433 uint8_t movePathCount;
434 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
435 } Enemy;
436
437 Enemy enemies[ENEMY_MAX_COUNT];
438 int enemyCount = 0;
439
440 EnemyClassConfig enemyClassConfigs[] = {
441 [ENEMY_TYPE_MINION] = {
442 .health = 3.0f,
443 .speed = 1.0f,
444 .radius = 0.25f,
445 .maxAcceleration = 1.0f,
446 .explosionDamage = 1.0f,
447 .requiredContactTime = 0.5f,
448 .explosionRange = 1.0f,
449 .explosionPushbackPower = 0.25f,
450 },
451 };
452
453 int EnemyAddDamage(Enemy *enemy, float damage);
454
455 void EnemyInit()
456 {
457 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
458 {
459 enemies[i] = (Enemy){0};
460 }
461 enemyCount = 0;
462 }
463
464 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
465 {
466 return enemyClassConfigs[enemy->enemyType].speed;
467 }
468
469 float EnemyGetMaxHealth(Enemy *enemy)
470 {
471 return enemyClassConfigs[enemy->enemyType].health;
472 }
473
474 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
475 {
476 int16_t castleX = 0;
477 int16_t castleY = 0;
478 int16_t dx = castleX - currentX;
479 int16_t dy = castleY - currentY;
480 if (dx == 0 && dy == 0)
481 {
482 *nextX = currentX;
483 *nextY = currentY;
484 return 1;
485 }
486 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
487
488 if (gradient.x == 0 && gradient.y == 0)
489 {
490 *nextX = currentX;
491 *nextY = currentY;
492 return 1;
493 }
494
495 if (fabsf(gradient.x) > fabsf(gradient.y))
496 {
497 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
498 *nextY = currentY;
499 return 0;
500 }
501 *nextX = currentX;
502 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
503 return 0;
504 }
505
506
507 // this function predicts the movement of the unit for the next deltaT seconds
508 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
509 {
510 const float pointReachedDistance = 0.25f;
511 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
512 const float maxSimStepTime = 0.015625f;
513
514 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
515 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
516 int16_t nextX = enemy->nextX;
517 int16_t nextY = enemy->nextY;
518 Vector2 position = enemy->simPosition;
519 int passedCount = 0;
520 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
521 {
522 float stepTime = fminf(deltaT - t, maxSimStepTime);
523 Vector2 target = (Vector2){nextX, nextY};
524 float speed = Vector2Length(*velocity);
525 // draw the target position for debugging
526 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
527 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
528 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
529 {
530 // we reached the target position, let's move to the next waypoint
531 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
532 target = (Vector2){nextX, nextY};
533 // track how many waypoints we passed
534 passedCount++;
535 }
536
537 // acceleration towards the target
538 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
539 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
540 *velocity = Vector2Add(*velocity, acceleration);
541
542 // limit the speed to the maximum speed
543 if (speed > maxSpeed)
544 {
545 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
546 }
547
548 // move the enemy
549 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
550 }
551
552 if (waypointPassedCount)
553 {
554 (*waypointPassedCount) = passedCount;
555 }
556
557 return position;
558 }
559
560 void EnemyDraw()
561 {
562 for (int i = 0; i < enemyCount; i++)
563 {
564 Enemy enemy = enemies[i];
565 if (enemy.enemyType == ENEMY_TYPE_NONE)
566 {
567 continue;
568 }
569
570 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
571
572 if (enemy.movePathCount > 0)
573 {
574 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
575 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
576 }
577 for (int j = 1; j < enemy.movePathCount; j++)
578 {
579 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
580 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
581 DrawLine3D(p, q, GREEN);
582 }
583
584 switch (enemy.enemyType)
585 {
586 case ENEMY_TYPE_MINION:
587 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
588 break;
589 }
590 }
591 }
592
593 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
594 {
595 // damage the tower
596 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
597 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
598 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
599 float explosionRange2 = explosionRange * explosionRange;
600 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
601 // explode the enemy
602 if (tower->damage >= TowerGetMaxHealth(tower))
603 {
604 tower->towerType = TOWER_TYPE_NONE;
605 }
606
607 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
608 explosionSource,
609 (Vector3){0, 0.1f, 0}, 1.0f);
610
611 enemy->enemyType = ENEMY_TYPE_NONE;
612
613 // push back enemies & dealing damage
614 for (int i = 0; i < enemyCount; i++)
615 {
616 Enemy *other = &enemies[i];
617 if (other->enemyType == ENEMY_TYPE_NONE)
618 {
619 continue;
620 }
621 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
622 if (distanceSqr > 0 && distanceSqr < explosionRange2)
623 {
624 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
625 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
626 EnemyAddDamage(other, explosionDamge);
627 }
628 }
629 }
630
631 void EnemyUpdate()
632 {
633 const float castleX = 0;
634 const float castleY = 0;
635 const float maxPathDistance2 = 0.25f * 0.25f;
636
637 for (int i = 0; i < enemyCount; i++)
638 {
639 Enemy *enemy = &enemies[i];
640 if (enemy->enemyType == ENEMY_TYPE_NONE)
641 {
642 continue;
643 }
644
645 int waypointPassedCount = 0;
646 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
647 enemy->startMovingTime = gameTime.time;
648 // track path of unit
649 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
650 {
651 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
652 {
653 enemy->movePath[j] = enemy->movePath[j - 1];
654 }
655 enemy->movePath[0] = enemy->simPosition;
656 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
657 {
658 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
659 }
660 }
661
662 if (waypointPassedCount > 0)
663 {
664 enemy->currentX = enemy->nextX;
665 enemy->currentY = enemy->nextY;
666 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
667 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
668 {
669 // enemy reached the castle; remove it
670 enemy->enemyType = ENEMY_TYPE_NONE;
671 continue;
672 }
673 }
674 }
675
676 // handle collisions between enemies
677 for (int i = 0; i < enemyCount - 1; i++)
678 {
679 Enemy *enemyA = &enemies[i];
680 if (enemyA->enemyType == ENEMY_TYPE_NONE)
681 {
682 continue;
683 }
684 for (int j = i + 1; j < enemyCount; j++)
685 {
686 Enemy *enemyB = &enemies[j];
687 if (enemyB->enemyType == ENEMY_TYPE_NONE)
688 {
689 continue;
690 }
691 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
692 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
693 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
694 float radiusSum = radiusA + radiusB;
695 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
696 {
697 // collision
698 float distance = sqrtf(distanceSqr);
699 float overlap = radiusSum - distance;
700 // move the enemies apart, but softly; if we have a clog of enemies,
701 // moving them perfectly apart can cause them to jitter
702 float positionCorrection = overlap / 5.0f;
703 Vector2 direction = (Vector2){
704 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
705 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
706 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
707 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
708 }
709 }
710 }
711
712 // handle collisions between enemies and towers
713 for (int i = 0; i < enemyCount; i++)
714 {
715 Enemy *enemy = &enemies[i];
716 if (enemy->enemyType == ENEMY_TYPE_NONE)
717 {
718 continue;
719 }
720 enemy->contactTime -= gameTime.deltaTime;
721 if (enemy->contactTime < 0.0f)
722 {
723 enemy->contactTime = 0.0f;
724 }
725
726 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
727 // linear search over towers; could be optimized by using path finding tower map,
728 // but for now, we keep it simple
729 for (int j = 0; j < towerCount; j++)
730 {
731 Tower *tower = &towers[j];
732 if (tower->towerType == TOWER_TYPE_NONE)
733 {
734 continue;
735 }
736 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
737 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
738 if (distanceSqr > combinedRadius * combinedRadius)
739 {
740 continue;
741 }
742 // potential collision; square / circle intersection
743 float dx = tower->x - enemy->simPosition.x;
744 float dy = tower->y - enemy->simPosition.y;
745 float absDx = fabsf(dx);
746 float absDy = fabsf(dy);
747 Vector3 contactPoint = {0};
748 if (absDx <= 0.5f && absDx <= absDy) {
749 // vertical collision; push the enemy out horizontally
750 float overlap = enemyRadius + 0.5f - absDy;
751 if (overlap < 0.0f)
752 {
753 continue;
754 }
755 float direction = dy > 0.0f ? -1.0f : 1.0f;
756 enemy->simPosition.y += direction * overlap;
757 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
758 }
759 else if (absDy <= 0.5f && absDy <= absDx)
760 {
761 // horizontal collision; push the enemy out vertically
762 float overlap = enemyRadius + 0.5f - absDx;
763 if (overlap < 0.0f)
764 {
765 continue;
766 }
767 float direction = dx > 0.0f ? -1.0f : 1.0f;
768 enemy->simPosition.x += direction * overlap;
769 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
770 }
771 else
772 {
773 // possible collision with a corner
774 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
775 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
776 float cornerX = tower->x + cornerDX;
777 float cornerY = tower->y + cornerDY;
778 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
779 if (cornerDistanceSqr > enemyRadius * enemyRadius)
780 {
781 continue;
782 }
783 // push the enemy out along the diagonal
784 float cornerDistance = sqrtf(cornerDistanceSqr);
785 float overlap = enemyRadius - cornerDistance;
786 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
787 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
788 enemy->simPosition.x -= directionX * overlap;
789 enemy->simPosition.y -= directionY * overlap;
790 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
791 }
792
793 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
794 {
795 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
796 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
797 {
798 EnemyTriggerExplode(enemy, tower, contactPoint);
799 }
800 }
801 }
802 }
803 }
804
805 EnemyId EnemyGetId(Enemy *enemy)
806 {
807 return (EnemyId){enemy - enemies, enemy->generation};
808 }
809
810 Enemy *EnemyTryResolve(EnemyId enemyId)
811 {
812 if (enemyId.index >= ENEMY_MAX_COUNT)
813 {
814 return 0;
815 }
816 Enemy *enemy = &enemies[enemyId.index];
817 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
818 {
819 return 0;
820 }
821 return enemy;
822 }
823
824 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
825 {
826 Enemy *spawn = 0;
827 for (int i = 0; i < enemyCount; i++)
828 {
829 Enemy *enemy = &enemies[i];
830 if (enemy->enemyType == ENEMY_TYPE_NONE)
831 {
832 spawn = enemy;
833 break;
834 }
835 }
836
837 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
838 {
839 spawn = &enemies[enemyCount++];
840 }
841
842 if (spawn)
843 {
844 spawn->currentX = currentX;
845 spawn->currentY = currentY;
846 spawn->nextX = currentX;
847 spawn->nextY = currentY;
848 spawn->simPosition = (Vector2){currentX, currentY};
849 spawn->simVelocity = (Vector2){0, 0};
850 spawn->enemyType = enemyType;
851 spawn->startMovingTime = gameTime.time;
852 spawn->damage = 0.0f;
853 spawn->futureDamage = 0.0f;
854 spawn->generation++;
855 spawn->movePathCount = 0;
856 }
857
858 return spawn;
859 }
860
861 int EnemyAddDamage(Enemy *enemy, float damage)
862 {
863 enemy->damage += damage;
864 if (enemy->damage >= EnemyGetMaxHealth(enemy))
865 {
866 enemy->enemyType = ENEMY_TYPE_NONE;
867 return 1;
868 }
869
870 return 0;
871 }
872
873 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
874 {
875 int16_t castleX = 0;
876 int16_t castleY = 0;
877 Enemy* closest = 0;
878 int16_t closestDistance = 0;
879 float range2 = range * range;
880 for (int i = 0; i < enemyCount; i++)
881 {
882 Enemy* enemy = &enemies[i];
883 if (enemy->enemyType == ENEMY_TYPE_NONE)
884 {
885 continue;
886 }
887 float maxHealth = EnemyGetMaxHealth(enemy);
888 if (enemy->futureDamage >= maxHealth)
889 {
890 // ignore enemies that will die soon
891 continue;
892 }
893 int16_t dx = castleX - enemy->currentX;
894 int16_t dy = castleY - enemy->currentY;
895 int16_t distance = abs(dx) + abs(dy);
896 if (!closest || distance < closestDistance)
897 {
898 float tdx = towerX - enemy->currentX;
899 float tdy = towerY - enemy->currentY;
900 float tdistance2 = tdx * tdx + tdy * tdy;
901 if (tdistance2 <= range2)
902 {
903 closest = enemy;
904 closestDistance = distance;
905 }
906 }
907 }
908 return closest;
909 }
910
911 int EnemyCount()
912 {
913 int count = 0;
914 for (int i = 0; i < enemyCount; i++)
915 {
916 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
917 {
918 count++;
919 }
920 }
921 return count;
922 }
923
924 //# Projectiles
925 #define PROJECTILE_MAX_COUNT 1200
926 #define PROJECTILE_TYPE_NONE 0
927 #define PROJECTILE_TYPE_BULLET 1
928
929 typedef struct Projectile
930 {
931 uint8_t projectileType;
932 float shootTime;
933 float arrivalTime;
934 float damage;
935 Vector2 position;
936 Vector2 target;
937 Vector2 directionNormal;
938 EnemyId targetEnemy;
939 } Projectile;
940
941 Projectile projectiles[PROJECTILE_MAX_COUNT];
942 int projectileCount = 0;
943
944 void ProjectileInit()
945 {
946 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
947 {
948 projectiles[i] = (Projectile){0};
949 }
950 }
951
952 void ProjectileDraw()
953 {
954 for (int i = 0; i < projectileCount; i++)
955 {
956 Projectile projectile = projectiles[i];
957 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
958 {
959 continue;
960 }
961 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
962 if (transition >= 1.0f)
963 {
964 continue;
965 }
966 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
967 float x = position.x;
968 float y = position.y;
969 float dx = projectile.directionNormal.x;
970 float dy = projectile.directionNormal.y;
971 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
972 {
973 x -= dx * 0.1f;
974 y -= dy * 0.1f;
975 float size = 0.1f * d;
976 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
977 }
978 }
979 }
980
981 void ProjectileUpdate()
982 {
983 for (int i = 0; i < projectileCount; i++)
984 {
985 Projectile *projectile = &projectiles[i];
986 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
987 {
988 continue;
989 }
990 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
991 if (transition >= 1.0f)
992 {
993 projectile->projectileType = PROJECTILE_TYPE_NONE;
994 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
995 if (enemy)
996 {
997 EnemyAddDamage(enemy, projectile->damage);
998 }
999 continue;
1000 }
1001 }
1002 }
1003
1004 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
1005 {
1006 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
1007 {
1008 Projectile *projectile = &projectiles[i];
1009 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
1010 {
1011 projectile->projectileType = projectileType;
1012 projectile->shootTime = gameTime.time;
1013 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
1014 projectile->damage = damage;
1015 projectile->position = position;
1016 projectile->target = target;
1017 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
1018 projectile->targetEnemy = EnemyGetId(enemy);
1019 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
1020 return projectile;
1021 }
1022 }
1023 return 0;
1024 }
1025
1026 //# Towers
1027
1028 void TowerInit()
1029 {
1030 for (int i = 0; i < TOWER_MAX_COUNT; i++)
1031 {
1032 towers[i] = (Tower){0};
1033 }
1034 towerCount = 0;
1035 }
1036
1037 Tower *TowerGetAt(int16_t x, int16_t y)
1038 {
1039 for (int i = 0; i < towerCount; i++)
1040 {
1041 if (towers[i].x == x && towers[i].y == y)
1042 {
1043 return &towers[i];
1044 }
1045 }
1046 return 0;
1047 }
1048
1049 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
1050 {
1051 if (towerCount >= TOWER_MAX_COUNT)
1052 {
1053 return 0;
1054 }
1055
1056 Tower *tower = TowerGetAt(x, y);
1057 if (tower)
1058 {
1059 return 0;
1060 }
1061
1062 tower = &towers[towerCount++];
1063 tower->x = x;
1064 tower->y = y;
1065 tower->towerType = towerType;
1066 tower->cooldown = 0.0f;
1067 tower->damage = 0.0f;
1068 return tower;
1069 }
1070
1071 float TowerGetMaxHealth(Tower *tower)
1072 {
1073 switch (tower->towerType)
1074 {
1075 case TOWER_TYPE_BASE:
1076 return 10.0f;
1077 case TOWER_TYPE_GUN:
1078 return 3.0f;
1079 case TOWER_TYPE_WALL:
1080 return 5.0f;
1081 }
1082 return 0.0f;
1083 }
1084
1085 void TowerDraw()
1086 {
1087 for (int i = 0; i < towerCount; i++)
1088 {
1089 Tower tower = towers[i];
1090 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
1091 switch (tower.towerType)
1092 {
1093 case TOWER_TYPE_BASE:
1094 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
1095 break;
1096 case TOWER_TYPE_GUN:
1097 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
1098 break;
1099 case TOWER_TYPE_WALL:
1100 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
1101 break;
1102 }
1103 }
1104 }
1105
1106 void TowerGunUpdate(Tower *tower)
1107 {
1108 if (tower->cooldown <= 0)
1109 {
1110 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
1111 if (enemy)
1112 {
1113 tower->cooldown = 0.125f;
1114 // shoot the enemy; determine future position of the enemy
1115 float bulletSpeed = 1.0f;
1116 float bulletDamage = 3.0f;
1117 Vector2 velocity = enemy->simVelocity;
1118 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
1119 Vector2 towerPosition = {tower->x, tower->y};
1120 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
1121 for (int i = 0; i < 8; i++) {
1122 velocity = enemy->simVelocity;
1123 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
1124 float distance = Vector2Distance(towerPosition, futurePosition);
1125 float eta2 = distance / bulletSpeed;
1126 if (fabs(eta - eta2) < 0.01f) {
1127 break;
1128 }
1129 eta = (eta2 + eta) * 0.5f;
1130 }
1131 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
1132 bulletSpeed, bulletDamage);
1133 enemy->futureDamage += bulletDamage;
1134 }
1135 }
1136 else
1137 {
1138 tower->cooldown -= gameTime.deltaTime;
1139 }
1140 }
1141
1142 void TowerUpdate()
1143 {
1144 for (int i = 0; i < towerCount; i++)
1145 {
1146 Tower *tower = &towers[i];
1147 switch (tower->towerType)
1148 {
1149 case TOWER_TYPE_GUN:
1150 TowerGunUpdate(tower);
1151 break;
1152 }
1153 }
1154 }
1155
1156 //# Game
1157
1158 float nextSpawnTime = 0.0f;
1159
1160 void InitGame()
1161 {
1162 TowerInit();
1163 EnemyInit();
1164 ProjectileInit();
1165 ParticleInit();
1166 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1167
1168 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1169 TowerTryAdd(TOWER_TYPE_GUN, 2, 0);
1170
1171 for (int i = -2; i <= 2; i += 1)
1172 {
1173 TowerTryAdd(TOWER_TYPE_WALL, i, 2);
1174 TowerTryAdd(TOWER_TYPE_WALL, i, -2);
1175 TowerTryAdd(TOWER_TYPE_WALL, -2, i);
1176 }
1177
1178 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4);
1179 }
1180
1181 //# Immediate GUI functions
1182
1183 int Button(const char *text, int x, int y, int width, int height)
1184 {
1185 Rectangle bounds = {x, y, width, height};
1186 int isPressed = 0;
1187 if (CheckCollisionPointRec(GetMousePosition(), bounds))
1188 {
1189 DrawRectangle(x, y, width, height, GRAY);
1190 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1191 {
1192 isPressed = 1;
1193 }
1194 }
1195 else
1196 {
1197 DrawRectangle(x, y, width, height, LIGHTGRAY);
1198 }
1199 Font font = GetFontDefault();
1200 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1201 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, BLACK);
1202 return isPressed;
1203 }
1204
1205 //# Main game loop
1206
1207 void GameUpdate()
1208 {
1209 float dt = GetFrameTime();
1210 // cap maximum delta time to 0.1 seconds to prevent large time steps
1211 if (dt > 0.1f) dt = 0.1f;
1212 gameTime.time += dt;
1213 gameTime.deltaTime = dt;
1214 PathFindingMapUpdate();
1215 EnemyUpdate();
1216 TowerUpdate();
1217 ProjectileUpdate();
1218 ParticleUpdate();
1219
1220 // spawn a new enemy every second
1221 if (gameTime.time >= nextSpawnTime && EnemyCount() < 50)
1222 {
1223 nextSpawnTime = gameTime.time + 0.2f;
1224 // add a new enemy at the boundary of the map
1225 int randValue = GetRandomValue(-5, 5);
1226 int randSide = GetRandomValue(0, 3);
1227 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue;
1228 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue;
1229 static int alternation = 0;
1230 alternation += 1;
1231 if (alternation % 3 == 0) {
1232 EnemyTryAdd(ENEMY_TYPE_MINION, 0, -5);
1233 }
1234 else if (alternation % 3 == 1)
1235 {
1236 EnemyTryAdd(ENEMY_TYPE_MINION, 0, 5);
1237 }
1238 EnemyTryAdd(ENEMY_TYPE_MINION, x, y);
1239 }
1240 }
1241
1242 int main(void)
1243 {
1244 int screenWidth, screenHeight;
1245 GetPreferredSize(&screenWidth, &screenHeight);
1246 InitWindow(screenWidth, screenHeight, "Tower defense");
1247 SetTargetFPS(30);
1248
1249 Camera3D camera = {0};
1250
1251 int cameraMode = 0;
1252
1253 InitGame();
1254
1255 while (!WindowShouldClose())
1256 {
1257 if (IsPaused()) {
1258 // canvas is not visible in browser - do nothing
1259 continue;
1260 }
1261
1262 if (cameraMode == 0)
1263 {
1264 camera.position = (Vector3){0.0f, 10.0f, -0.5f};
1265 camera.target = (Vector3){0.0f, 0.0f, -0.5f};
1266 camera.up = (Vector3){0.0f, 0.0f, -1.0f};
1267 camera.fovy = 12.0f;
1268 camera.projection = CAMERA_ORTHOGRAPHIC;
1269 }
1270 else
1271 {
1272 camera.position = (Vector3){1.0f, 12.0f, 6.5f};
1273 camera.target = (Vector3){0.0f, 0.5f, 1.0f};
1274 camera.up = (Vector3){0.0f, 1.0f, 0.0f};
1275 camera.fovy = 45.0f;
1276 camera.projection = CAMERA_PERSPECTIVE;
1277 }
1278
1279 BeginDrawing();
1280 ClearBackground(DARKBLUE);
1281
1282 BeginMode3D(camera);
1283 DrawGrid(10, 1.0f);
1284 TowerDraw();
1285 EnemyDraw();
1286 ProjectileDraw();
1287 PathFindingMapDraw();
1288 ParticleDraw();
1289 GameUpdate();
1290 EndMode3D();
1291
1292 const char *title = "Tower defense tutorial";
1293 int titleWidth = MeasureText(title, 20);
1294 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f + 2, 5 + 2, 20, BLACK);
1295 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f, 5, 20, WHITE);
1296
1297 const char *buttonText = cameraMode == 0 ? "2D" : "3D";
1298 if (Button(buttonText, 10, 10, 80, 30))
1299 {
1300 cameraMode = !cameraMode;
1301 }
1302
1303 EndDrawing();
1304 }
1305
1306 CloseWindow();
1307
1308 return 0;
1309 }
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
If you scroll over the code, you'll see that the function to handle the button drawing and behavior is only 20 lines of code and the usage is also dead simple:
1 const char *buttonText = cameraMode == 0 ? "2D" : "3D";
2 if (Button(buttonText, 10, 10, 80, 30))
3 {
4 cameraMode = !cameraMode;
5 }
The next step we need to do is to figure out how to translate the mouse position to the world grid. All we need to do is to cast a ray from the camera to the mouse position and calculate the x-z intersection with the world grid. The math is relatively simple:
1 #include "td-tut-2-main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Declarations
7
8 #define PARTICLE_MAX_COUNT 400
9 #define PARTICLE_TYPE_NONE 0
10 #define PARTICLE_TYPE_EXPLOSION 1
11
12 typedef struct Particle
13 {
14 uint8_t particleType;
15 float spawnTime;
16 float lifetime;
17 Vector3 position;
18 Vector3 velocity;
19 } Particle;
20
21 Particle particles[PARTICLE_MAX_COUNT];
22 int particleCount = 0;
23
24 #define TOWER_MAX_COUNT 400
25 #define TOWER_TYPE_NONE 0
26 #define TOWER_TYPE_BASE 1
27 #define TOWER_TYPE_GUN 2
28 #define TOWER_TYPE_WALL 3
29
30 typedef struct Tower
31 {
32 int16_t x, y;
33 uint8_t towerType;
34 float cooldown;
35 float damage;
36 } Tower;
37
38 typedef struct GameTime
39 {
40 float time;
41 float deltaTime;
42 } GameTime;
43
44 GameTime gameTime = {0};
45
46 Tower towers[TOWER_MAX_COUNT];
47 int towerCount = 0;
48
49 float TowerGetMaxHealth(Tower *tower);
50
51 //# Particle system
52
53 void ParticleInit()
54 {
55 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
56 {
57 particles[i] = (Particle){0};
58 }
59 particleCount = 0;
60 }
61
62 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
63 {
64 if (particleCount >= PARTICLE_MAX_COUNT)
65 {
66 return;
67 }
68
69 int index = -1;
70 for (int i = 0; i < particleCount; i++)
71 {
72 if (particles[i].particleType == PARTICLE_TYPE_NONE)
73 {
74 index = i;
75 break;
76 }
77 }
78
79 if (index == -1)
80 {
81 index = particleCount++;
82 }
83
84 Particle *particle = &particles[index];
85 particle->particleType = particleType;
86 particle->spawnTime = gameTime.time;
87 particle->lifetime = lifetime;
88 particle->position = position;
89 particle->velocity = velocity;
90 }
91
92 void ParticleUpdate()
93 {
94 for (int i = 0; i < particleCount; i++)
95 {
96 Particle *particle = &particles[i];
97 if (particle->particleType == PARTICLE_TYPE_NONE)
98 {
99 continue;
100 }
101
102 float age = gameTime.time - particle->spawnTime;
103
104 if (particle->lifetime > age)
105 {
106 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
107 }
108 else {
109 particle->particleType = PARTICLE_TYPE_NONE;
110 }
111 }
112 }
113
114 void DrawExplosionParticle(Particle *particle, float transition)
115 {
116 float size = 1.2f * (1.0f - transition);
117 Color startColor = WHITE;
118 Color endColor = RED;
119 Color color = ColorLerp(startColor, endColor, transition);
120 DrawCube(particle->position, size, size, size, color);
121 }
122
123 void ParticleDraw()
124 {
125 for (int i = 0; i < particleCount; i++)
126 {
127 Particle particle = particles[i];
128 if (particle.particleType == PARTICLE_TYPE_NONE)
129 {
130 continue;
131 }
132
133 float age = gameTime.time - particle.spawnTime;
134 float transition = age / particle.lifetime;
135 switch (particle.particleType)
136 {
137 case PARTICLE_TYPE_EXPLOSION:
138 DrawExplosionParticle(&particle, transition);
139 break;
140 default:
141 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
142 break;
143 }
144 }
145 }
146
147 //# Pathfinding map
148 typedef struct DeltaSrc
149 {
150 char x, y;
151 } DeltaSrc;
152
153 typedef struct PathfindingMap
154 {
155 int width, height;
156 float scale;
157 float *distances;
158 long *towerIndex;
159 DeltaSrc *deltaSrc;
160 float maxDistance;
161 Matrix toMapSpace;
162 Matrix toWorldSpace;
163 } PathfindingMap;
164
165 // when we execute the pathfinding algorithm, we need to store the active nodes
166 // in a queue. Each node has a position, a distance from the start, and the
167 // position of the node that we came from.
168 typedef struct PathfindingNode
169 {
170 int16_t x, y, fromX, fromY;
171 float distance;
172 } PathfindingNode;
173
174 // The queue is a simple array of nodes, we add nodes to the end and remove
175 // nodes from the front. We keep the array around to avoid unnecessary allocations
176 static PathfindingNode *pathfindingNodeQueue = 0;
177 static int pathfindingNodeQueueCount = 0;
178 static int pathfindingNodeQueueCapacity = 0;
179
180 // The pathfinding map stores the distances from the castle to each cell in the map.
181 PathfindingMap pathfindingMap = {0};
182
183 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
184 {
185 // transforming between map space and world space allows us to adapt
186 // position and scale of the map without changing the pathfinding data
187 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
188 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
189 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
190 pathfindingMap.width = width;
191 pathfindingMap.height = height;
192 pathfindingMap.scale = scale;
193 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
194 for (int i = 0; i < width * height; i++)
195 {
196 pathfindingMap.distances[i] = -1.0f;
197 }
198
199 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
200 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
201 }
202
203 float PathFindingGetDistance(int mapX, int mapY)
204 {
205 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
206 {
207 // when outside the map, we return the manhattan distance to the castle (0,0)
208 return fabsf((float)mapX) + fabsf((float)mapY);
209 }
210
211 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
212 }
213
214 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
215 {
216 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
217 {
218 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
219 // we use MemAlloc/MemRealloc to allocate memory for the queue
220 // I am not entirely sure if MemRealloc allows passing a null pointer
221 // so we check if the pointer is null and use MemAlloc in that case
222 if (pathfindingNodeQueue == 0)
223 {
224 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
225 }
226 else
227 {
228 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
229 }
230 }
231
232 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
233 node->x = x;
234 node->y = y;
235 node->fromX = fromX;
236 node->fromY = fromY;
237 node->distance = distance;
238 }
239
240 PathfindingNode *PathFindingNodePop()
241 {
242 if (pathfindingNodeQueueCount == 0)
243 {
244 return 0;
245 }
246 // we return the first node in the queue; we want to return a pointer to the node
247 // so we can return 0 if the queue is empty.
248 // We should _not_ return a pointer to the element in the list, because the list
249 // may be reallocated and the pointer would become invalid. Or the
250 // popped element is overwritten by the next push operation.
251 // Using static here means that the variable is permanently allocated.
252 static PathfindingNode node;
253 node = pathfindingNodeQueue[0];
254 // we shift all nodes one position to the front
255 for (int i = 1; i < pathfindingNodeQueueCount; i++)
256 {
257 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
258 }
259 --pathfindingNodeQueueCount;
260 return &node;
261 }
262
263 // transform a world position to a map position in the array;
264 // returns true if the position is inside the map
265 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
266 {
267 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
268 *mapX = (int16_t)mapPosition.x;
269 *mapY = (int16_t)mapPosition.z;
270 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
271 }
272
273 void PathFindingMapUpdate()
274 {
275 const int castleX = 0, castleY = 0;
276 int16_t castleMapX, castleMapY;
277 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
278 {
279 return;
280 }
281 int width = pathfindingMap.width, height = pathfindingMap.height;
282
283 // reset the distances to -1
284 for (int i = 0; i < width * height; i++)
285 {
286 pathfindingMap.distances[i] = -1.0f;
287 }
288 // reset the tower indices
289 for (int i = 0; i < width * height; i++)
290 {
291 pathfindingMap.towerIndex[i] = -1;
292 }
293 // reset the delta src
294 for (int i = 0; i < width * height; i++)
295 {
296 pathfindingMap.deltaSrc[i].x = 0;
297 pathfindingMap.deltaSrc[i].y = 0;
298 }
299
300 for (int i = 0; i < towerCount; i++)
301 {
302 Tower *tower = &towers[i];
303 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
304 {
305 continue;
306 }
307 int16_t mapX, mapY;
308 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
309 // this would not work correctly and needs to be refined to allow towers covering multiple cells
310 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
311 // one cell. For now.
312 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
313 {
314 continue;
315 }
316 int index = mapY * width + mapX;
317 pathfindingMap.towerIndex[index] = i;
318 }
319
320 // we start at the castle and add the castle to the queue
321 pathfindingMap.maxDistance = 0.0f;
322 pathfindingNodeQueueCount = 0;
323 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
324 PathfindingNode *node = 0;
325 while ((node = PathFindingNodePop()))
326 {
327 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
328 {
329 continue;
330 }
331 int index = node->y * width + node->x;
332 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
333 {
334 continue;
335 }
336
337 int deltaX = node->x - node->fromX;
338 int deltaY = node->y - node->fromY;
339 // even if the cell is blocked by a tower, we still may want to store the direction
340 // (though this might not be needed, IDK right now)
341 pathfindingMap.deltaSrc[index].x = (char) deltaX;
342 pathfindingMap.deltaSrc[index].y = (char) deltaY;
343
344 // we skip nodes that are blocked by towers
345 if (pathfindingMap.towerIndex[index] >= 0)
346 {
347 node->distance += 8.0f;
348 }
349 pathfindingMap.distances[index] = node->distance;
350 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
351 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
352 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
353 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
354 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
355 }
356 }
357
358 void PathFindingMapDraw()
359 {
360 float cellSize = pathfindingMap.scale * 0.9f;
361 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
362 for (int x = 0; x < pathfindingMap.width; x++)
363 {
364 for (int y = 0; y < pathfindingMap.height; y++)
365 {
366 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
367 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
368 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
369 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
370 // animate the distance "wave" to show how the pathfinding algorithm expands
371 // from the castle
372 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
373 {
374 color = BLACK;
375 }
376 DrawCube(position, cellSize, 0.1f, cellSize, color);
377 }
378 }
379 }
380
381 Vector2 PathFindingGetGradient(Vector3 world)
382 {
383 int16_t mapX, mapY;
384 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
385 {
386 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
387 return (Vector2){(float)-delta.x, (float)-delta.y};
388 }
389 // fallback to a simple gradient calculation
390 float n = PathFindingGetDistance(mapX, mapY - 1);
391 float s = PathFindingGetDistance(mapX, mapY + 1);
392 float w = PathFindingGetDistance(mapX - 1, mapY);
393 float e = PathFindingGetDistance(mapX + 1, mapY);
394 return (Vector2){w - e + 0.25f, n - s + 0.125f};
395 }
396
397 //# Enemies
398
399 #define ENEMY_MAX_PATH_COUNT 8
400 #define ENEMY_MAX_COUNT 400
401 #define ENEMY_TYPE_NONE 0
402 #define ENEMY_TYPE_MINION 1
403
404 typedef struct EnemyId
405 {
406 uint16_t index;
407 uint16_t generation;
408 } EnemyId;
409
410 typedef struct EnemyClassConfig
411 {
412 float speed;
413 float health;
414 float radius;
415 float maxAcceleration;
416 float requiredContactTime;
417 float explosionDamage;
418 float explosionRange;
419 float explosionPushbackPower;
420 } EnemyClassConfig;
421
422 typedef struct Enemy
423 {
424 int16_t currentX, currentY;
425 int16_t nextX, nextY;
426 Vector2 simPosition;
427 Vector2 simVelocity;
428 uint16_t generation;
429 float startMovingTime;
430 float damage, futureDamage;
431 float contactTime;
432 uint8_t enemyType;
433 uint8_t movePathCount;
434 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
435 } Enemy;
436
437 Enemy enemies[ENEMY_MAX_COUNT];
438 int enemyCount = 0;
439
440 EnemyClassConfig enemyClassConfigs[] = {
441 [ENEMY_TYPE_MINION] = {
442 .health = 3.0f,
443 .speed = 1.0f,
444 .radius = 0.25f,
445 .maxAcceleration = 1.0f,
446 .explosionDamage = 1.0f,
447 .requiredContactTime = 0.5f,
448 .explosionRange = 1.0f,
449 .explosionPushbackPower = 0.25f,
450 },
451 };
452
453 int EnemyAddDamage(Enemy *enemy, float damage);
454
455 void EnemyInit()
456 {
457 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
458 {
459 enemies[i] = (Enemy){0};
460 }
461 enemyCount = 0;
462 }
463
464 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
465 {
466 return enemyClassConfigs[enemy->enemyType].speed;
467 }
468
469 float EnemyGetMaxHealth(Enemy *enemy)
470 {
471 return enemyClassConfigs[enemy->enemyType].health;
472 }
473
474 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
475 {
476 int16_t castleX = 0;
477 int16_t castleY = 0;
478 int16_t dx = castleX - currentX;
479 int16_t dy = castleY - currentY;
480 if (dx == 0 && dy == 0)
481 {
482 *nextX = currentX;
483 *nextY = currentY;
484 return 1;
485 }
486 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
487
488 if (gradient.x == 0 && gradient.y == 0)
489 {
490 *nextX = currentX;
491 *nextY = currentY;
492 return 1;
493 }
494
495 if (fabsf(gradient.x) > fabsf(gradient.y))
496 {
497 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
498 *nextY = currentY;
499 return 0;
500 }
501 *nextX = currentX;
502 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
503 return 0;
504 }
505
506
507 // this function predicts the movement of the unit for the next deltaT seconds
508 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
509 {
510 const float pointReachedDistance = 0.25f;
511 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
512 const float maxSimStepTime = 0.015625f;
513
514 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
515 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
516 int16_t nextX = enemy->nextX;
517 int16_t nextY = enemy->nextY;
518 Vector2 position = enemy->simPosition;
519 int passedCount = 0;
520 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
521 {
522 float stepTime = fminf(deltaT - t, maxSimStepTime);
523 Vector2 target = (Vector2){nextX, nextY};
524 float speed = Vector2Length(*velocity);
525 // draw the target position for debugging
526 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
527 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
528 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
529 {
530 // we reached the target position, let's move to the next waypoint
531 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
532 target = (Vector2){nextX, nextY};
533 // track how many waypoints we passed
534 passedCount++;
535 }
536
537 // acceleration towards the target
538 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
539 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
540 *velocity = Vector2Add(*velocity, acceleration);
541
542 // limit the speed to the maximum speed
543 if (speed > maxSpeed)
544 {
545 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
546 }
547
548 // move the enemy
549 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
550 }
551
552 if (waypointPassedCount)
553 {
554 (*waypointPassedCount) = passedCount;
555 }
556
557 return position;
558 }
559
560 void EnemyDraw()
561 {
562 for (int i = 0; i < enemyCount; i++)
563 {
564 Enemy enemy = enemies[i];
565 if (enemy.enemyType == ENEMY_TYPE_NONE)
566 {
567 continue;
568 }
569
570 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
571
572 if (enemy.movePathCount > 0)
573 {
574 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
575 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
576 }
577 for (int j = 1; j < enemy.movePathCount; j++)
578 {
579 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
580 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
581 DrawLine3D(p, q, GREEN);
582 }
583
584 switch (enemy.enemyType)
585 {
586 case ENEMY_TYPE_MINION:
587 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
588 break;
589 }
590 }
591 }
592
593 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
594 {
595 // damage the tower
596 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
597 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
598 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
599 float explosionRange2 = explosionRange * explosionRange;
600 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
601 // explode the enemy
602 if (tower->damage >= TowerGetMaxHealth(tower))
603 {
604 tower->towerType = TOWER_TYPE_NONE;
605 }
606
607 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
608 explosionSource,
609 (Vector3){0, 0.1f, 0}, 1.0f);
610
611 enemy->enemyType = ENEMY_TYPE_NONE;
612
613 // push back enemies & dealing damage
614 for (int i = 0; i < enemyCount; i++)
615 {
616 Enemy *other = &enemies[i];
617 if (other->enemyType == ENEMY_TYPE_NONE)
618 {
619 continue;
620 }
621 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
622 if (distanceSqr > 0 && distanceSqr < explosionRange2)
623 {
624 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
625 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
626 EnemyAddDamage(other, explosionDamge);
627 }
628 }
629 }
630
631 void EnemyUpdate()
632 {
633 const float castleX = 0;
634 const float castleY = 0;
635 const float maxPathDistance2 = 0.25f * 0.25f;
636
637 for (int i = 0; i < enemyCount; i++)
638 {
639 Enemy *enemy = &enemies[i];
640 if (enemy->enemyType == ENEMY_TYPE_NONE)
641 {
642 continue;
643 }
644
645 int waypointPassedCount = 0;
646 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
647 enemy->startMovingTime = gameTime.time;
648 // track path of unit
649 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
650 {
651 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
652 {
653 enemy->movePath[j] = enemy->movePath[j - 1];
654 }
655 enemy->movePath[0] = enemy->simPosition;
656 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
657 {
658 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
659 }
660 }
661
662 if (waypointPassedCount > 0)
663 {
664 enemy->currentX = enemy->nextX;
665 enemy->currentY = enemy->nextY;
666 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
667 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
668 {
669 // enemy reached the castle; remove it
670 enemy->enemyType = ENEMY_TYPE_NONE;
671 continue;
672 }
673 }
674 }
675
676 // handle collisions between enemies
677 for (int i = 0; i < enemyCount - 1; i++)
678 {
679 Enemy *enemyA = &enemies[i];
680 if (enemyA->enemyType == ENEMY_TYPE_NONE)
681 {
682 continue;
683 }
684 for (int j = i + 1; j < enemyCount; j++)
685 {
686 Enemy *enemyB = &enemies[j];
687 if (enemyB->enemyType == ENEMY_TYPE_NONE)
688 {
689 continue;
690 }
691 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
692 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
693 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
694 float radiusSum = radiusA + radiusB;
695 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
696 {
697 // collision
698 float distance = sqrtf(distanceSqr);
699 float overlap = radiusSum - distance;
700 // move the enemies apart, but softly; if we have a clog of enemies,
701 // moving them perfectly apart can cause them to jitter
702 float positionCorrection = overlap / 5.0f;
703 Vector2 direction = (Vector2){
704 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
705 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
706 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
707 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
708 }
709 }
710 }
711
712 // handle collisions between enemies and towers
713 for (int i = 0; i < enemyCount; i++)
714 {
715 Enemy *enemy = &enemies[i];
716 if (enemy->enemyType == ENEMY_TYPE_NONE)
717 {
718 continue;
719 }
720 enemy->contactTime -= gameTime.deltaTime;
721 if (enemy->contactTime < 0.0f)
722 {
723 enemy->contactTime = 0.0f;
724 }
725
726 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
727 // linear search over towers; could be optimized by using path finding tower map,
728 // but for now, we keep it simple
729 for (int j = 0; j < towerCount; j++)
730 {
731 Tower *tower = &towers[j];
732 if (tower->towerType == TOWER_TYPE_NONE)
733 {
734 continue;
735 }
736 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
737 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
738 if (distanceSqr > combinedRadius * combinedRadius)
739 {
740 continue;
741 }
742 // potential collision; square / circle intersection
743 float dx = tower->x - enemy->simPosition.x;
744 float dy = tower->y - enemy->simPosition.y;
745 float absDx = fabsf(dx);
746 float absDy = fabsf(dy);
747 Vector3 contactPoint = {0};
748 if (absDx <= 0.5f && absDx <= absDy) {
749 // vertical collision; push the enemy out horizontally
750 float overlap = enemyRadius + 0.5f - absDy;
751 if (overlap < 0.0f)
752 {
753 continue;
754 }
755 float direction = dy > 0.0f ? -1.0f : 1.0f;
756 enemy->simPosition.y += direction * overlap;
757 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
758 }
759 else if (absDy <= 0.5f && absDy <= absDx)
760 {
761 // horizontal collision; push the enemy out vertically
762 float overlap = enemyRadius + 0.5f - absDx;
763 if (overlap < 0.0f)
764 {
765 continue;
766 }
767 float direction = dx > 0.0f ? -1.0f : 1.0f;
768 enemy->simPosition.x += direction * overlap;
769 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
770 }
771 else
772 {
773 // possible collision with a corner
774 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
775 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
776 float cornerX = tower->x + cornerDX;
777 float cornerY = tower->y + cornerDY;
778 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
779 if (cornerDistanceSqr > enemyRadius * enemyRadius)
780 {
781 continue;
782 }
783 // push the enemy out along the diagonal
784 float cornerDistance = sqrtf(cornerDistanceSqr);
785 float overlap = enemyRadius - cornerDistance;
786 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
787 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
788 enemy->simPosition.x -= directionX * overlap;
789 enemy->simPosition.y -= directionY * overlap;
790 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
791 }
792
793 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
794 {
795 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
796 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
797 {
798 EnemyTriggerExplode(enemy, tower, contactPoint);
799 }
800 }
801 }
802 }
803 }
804
805 EnemyId EnemyGetId(Enemy *enemy)
806 {
807 return (EnemyId){enemy - enemies, enemy->generation};
808 }
809
810 Enemy *EnemyTryResolve(EnemyId enemyId)
811 {
812 if (enemyId.index >= ENEMY_MAX_COUNT)
813 {
814 return 0;
815 }
816 Enemy *enemy = &enemies[enemyId.index];
817 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
818 {
819 return 0;
820 }
821 return enemy;
822 }
823
824 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
825 {
826 Enemy *spawn = 0;
827 for (int i = 0; i < enemyCount; i++)
828 {
829 Enemy *enemy = &enemies[i];
830 if (enemy->enemyType == ENEMY_TYPE_NONE)
831 {
832 spawn = enemy;
833 break;
834 }
835 }
836
837 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
838 {
839 spawn = &enemies[enemyCount++];
840 }
841
842 if (spawn)
843 {
844 spawn->currentX = currentX;
845 spawn->currentY = currentY;
846 spawn->nextX = currentX;
847 spawn->nextY = currentY;
848 spawn->simPosition = (Vector2){currentX, currentY};
849 spawn->simVelocity = (Vector2){0, 0};
850 spawn->enemyType = enemyType;
851 spawn->startMovingTime = gameTime.time;
852 spawn->damage = 0.0f;
853 spawn->futureDamage = 0.0f;
854 spawn->generation++;
855 spawn->movePathCount = 0;
856 }
857
858 return spawn;
859 }
860
861 int EnemyAddDamage(Enemy *enemy, float damage)
862 {
863 enemy->damage += damage;
864 if (enemy->damage >= EnemyGetMaxHealth(enemy))
865 {
866 enemy->enemyType = ENEMY_TYPE_NONE;
867 return 1;
868 }
869
870 return 0;
871 }
872
873 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
874 {
875 int16_t castleX = 0;
876 int16_t castleY = 0;
877 Enemy* closest = 0;
878 int16_t closestDistance = 0;
879 float range2 = range * range;
880 for (int i = 0; i < enemyCount; i++)
881 {
882 Enemy* enemy = &enemies[i];
883 if (enemy->enemyType == ENEMY_TYPE_NONE)
884 {
885 continue;
886 }
887 float maxHealth = EnemyGetMaxHealth(enemy);
888 if (enemy->futureDamage >= maxHealth)
889 {
890 // ignore enemies that will die soon
891 continue;
892 }
893 int16_t dx = castleX - enemy->currentX;
894 int16_t dy = castleY - enemy->currentY;
895 int16_t distance = abs(dx) + abs(dy);
896 if (!closest || distance < closestDistance)
897 {
898 float tdx = towerX - enemy->currentX;
899 float tdy = towerY - enemy->currentY;
900 float tdistance2 = tdx * tdx + tdy * tdy;
901 if (tdistance2 <= range2)
902 {
903 closest = enemy;
904 closestDistance = distance;
905 }
906 }
907 }
908 return closest;
909 }
910
911 int EnemyCount()
912 {
913 int count = 0;
914 for (int i = 0; i < enemyCount; i++)
915 {
916 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
917 {
918 count++;
919 }
920 }
921 return count;
922 }
923
924 //# Projectiles
925 #define PROJECTILE_MAX_COUNT 1200
926 #define PROJECTILE_TYPE_NONE 0
927 #define PROJECTILE_TYPE_BULLET 1
928
929 typedef struct Projectile
930 {
931 uint8_t projectileType;
932 float shootTime;
933 float arrivalTime;
934 float damage;
935 Vector2 position;
936 Vector2 target;
937 Vector2 directionNormal;
938 EnemyId targetEnemy;
939 } Projectile;
940
941 Projectile projectiles[PROJECTILE_MAX_COUNT];
942 int projectileCount = 0;
943
944 void ProjectileInit()
945 {
946 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
947 {
948 projectiles[i] = (Projectile){0};
949 }
950 }
951
952 void ProjectileDraw()
953 {
954 for (int i = 0; i < projectileCount; i++)
955 {
956 Projectile projectile = projectiles[i];
957 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
958 {
959 continue;
960 }
961 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
962 if (transition >= 1.0f)
963 {
964 continue;
965 }
966 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
967 float x = position.x;
968 float y = position.y;
969 float dx = projectile.directionNormal.x;
970 float dy = projectile.directionNormal.y;
971 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
972 {
973 x -= dx * 0.1f;
974 y -= dy * 0.1f;
975 float size = 0.1f * d;
976 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
977 }
978 }
979 }
980
981 void ProjectileUpdate()
982 {
983 for (int i = 0; i < projectileCount; i++)
984 {
985 Projectile *projectile = &projectiles[i];
986 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
987 {
988 continue;
989 }
990 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
991 if (transition >= 1.0f)
992 {
993 projectile->projectileType = PROJECTILE_TYPE_NONE;
994 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
995 if (enemy)
996 {
997 EnemyAddDamage(enemy, projectile->damage);
998 }
999 continue;
1000 }
1001 }
1002 }
1003
1004 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
1005 {
1006 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
1007 {
1008 Projectile *projectile = &projectiles[i];
1009 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
1010 {
1011 projectile->projectileType = projectileType;
1012 projectile->shootTime = gameTime.time;
1013 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
1014 projectile->damage = damage;
1015 projectile->position = position;
1016 projectile->target = target;
1017 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
1018 projectile->targetEnemy = EnemyGetId(enemy);
1019 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
1020 return projectile;
1021 }
1022 }
1023 return 0;
1024 }
1025
1026 //# Towers
1027
1028 void TowerInit()
1029 {
1030 for (int i = 0; i < TOWER_MAX_COUNT; i++)
1031 {
1032 towers[i] = (Tower){0};
1033 }
1034 towerCount = 0;
1035 }
1036
1037 Tower *TowerGetAt(int16_t x, int16_t y)
1038 {
1039 for (int i = 0; i < towerCount; i++)
1040 {
1041 if (towers[i].x == x && towers[i].y == y)
1042 {
1043 return &towers[i];
1044 }
1045 }
1046 return 0;
1047 }
1048
1049 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
1050 {
1051 if (towerCount >= TOWER_MAX_COUNT)
1052 {
1053 return 0;
1054 }
1055
1056 Tower *tower = TowerGetAt(x, y);
1057 if (tower)
1058 {
1059 return 0;
1060 }
1061
1062 tower = &towers[towerCount++];
1063 tower->x = x;
1064 tower->y = y;
1065 tower->towerType = towerType;
1066 tower->cooldown = 0.0f;
1067 tower->damage = 0.0f;
1068 return tower;
1069 }
1070
1071 float TowerGetMaxHealth(Tower *tower)
1072 {
1073 switch (tower->towerType)
1074 {
1075 case TOWER_TYPE_BASE:
1076 return 10.0f;
1077 case TOWER_TYPE_GUN:
1078 return 3.0f;
1079 case TOWER_TYPE_WALL:
1080 return 5.0f;
1081 }
1082 return 0.0f;
1083 }
1084
1085 void TowerDraw()
1086 {
1087 for (int i = 0; i < towerCount; i++)
1088 {
1089 Tower tower = towers[i];
1090 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
1091 switch (tower.towerType)
1092 {
1093 case TOWER_TYPE_BASE:
1094 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
1095 break;
1096 case TOWER_TYPE_GUN:
1097 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
1098 break;
1099 case TOWER_TYPE_WALL:
1100 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
1101 break;
1102 }
1103 }
1104 }
1105
1106 void TowerGunUpdate(Tower *tower)
1107 {
1108 if (tower->cooldown <= 0)
1109 {
1110 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
1111 if (enemy)
1112 {
1113 tower->cooldown = 0.125f;
1114 // shoot the enemy; determine future position of the enemy
1115 float bulletSpeed = 1.0f;
1116 float bulletDamage = 3.0f;
1117 Vector2 velocity = enemy->simVelocity;
1118 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
1119 Vector2 towerPosition = {tower->x, tower->y};
1120 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
1121 for (int i = 0; i < 8; i++) {
1122 velocity = enemy->simVelocity;
1123 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
1124 float distance = Vector2Distance(towerPosition, futurePosition);
1125 float eta2 = distance / bulletSpeed;
1126 if (fabs(eta - eta2) < 0.01f) {
1127 break;
1128 }
1129 eta = (eta2 + eta) * 0.5f;
1130 }
1131 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
1132 bulletSpeed, bulletDamage);
1133 enemy->futureDamage += bulletDamage;
1134 }
1135 }
1136 else
1137 {
1138 tower->cooldown -= gameTime.deltaTime;
1139 }
1140 }
1141
1142 void TowerUpdate()
1143 {
1144 for (int i = 0; i < towerCount; i++)
1145 {
1146 Tower *tower = &towers[i];
1147 switch (tower->towerType)
1148 {
1149 case TOWER_TYPE_GUN:
1150 TowerGunUpdate(tower);
1151 break;
1152 }
1153 }
1154 }
1155
1156 //# Game
1157
1158 float nextSpawnTime = 0.0f;
1159
1160 void InitGame()
1161 {
1162 TowerInit();
1163 EnemyInit();
1164 ProjectileInit();
1165 ParticleInit();
1166 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1167
1168 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1169 TowerTryAdd(TOWER_TYPE_GUN, 2, 0);
1170
1171 for (int i = -2; i <= 2; i += 1)
1172 {
1173 TowerTryAdd(TOWER_TYPE_WALL, i, 2);
1174 TowerTryAdd(TOWER_TYPE_WALL, i, -2);
1175 TowerTryAdd(TOWER_TYPE_WALL, -2, i);
1176 }
1177
1178 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4);
1179 }
1180
1181 //# Immediate GUI functions
1182
1183 int Button(const char *text, int x, int y, int width, int height)
1184 {
1185 Rectangle bounds = {x, y, width, height};
1186 int isPressed = 0;
1187 if (CheckCollisionPointRec(GetMousePosition(), bounds))
1188 {
1189 DrawRectangle(x, y, width, height, GRAY);
1190 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1191 {
1192 isPressed = 1;
1193 }
1194 }
1195 else
1196 {
1197 DrawRectangle(x, y, width, height, LIGHTGRAY);
1198 }
1199 Font font = GetFontDefault();
1200 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1201 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, BLACK);
1202 return isPressed;
1203 }
1204
1205 //# Main game loop
1206
1207 void GameUpdate()
1208 {
1209 float dt = GetFrameTime();
1210 // cap maximum delta time to 0.1 seconds to prevent large time steps
1211 if (dt > 0.1f) dt = 0.1f;
1212 gameTime.time += dt;
1213 gameTime.deltaTime = dt;
1214 PathFindingMapUpdate();
1215 EnemyUpdate();
1216 TowerUpdate();
1217 ProjectileUpdate();
1218 ParticleUpdate();
1219
1220 // spawn a new enemy every second
1221 if (gameTime.time >= nextSpawnTime && EnemyCount() < 50)
1222 {
1223 nextSpawnTime = gameTime.time + 0.2f;
1224 // add a new enemy at the boundary of the map
1225 int randValue = GetRandomValue(-5, 5);
1226 int randSide = GetRandomValue(0, 3);
1227 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue;
1228 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue;
1229 static int alternation = 0;
1230 alternation += 1;
1231 if (alternation % 3 == 0) {
1232 EnemyTryAdd(ENEMY_TYPE_MINION, 0, -5);
1233 }
1234 else if (alternation % 3 == 1)
1235 {
1236 EnemyTryAdd(ENEMY_TYPE_MINION, 0, 5);
1237 }
1238 EnemyTryAdd(ENEMY_TYPE_MINION, x, y);
1239 }
1240 }
1241
1242 int main(void)
1243 {
1244 int screenWidth, screenHeight;
1245 GetPreferredSize(&screenWidth, &screenHeight);
1246 InitWindow(screenWidth, screenHeight, "Tower defense");
1247 SetTargetFPS(30);
1248
1249 Camera3D camera = {0};
1250
1251 int cameraMode = 0;
1252
1253 InitGame();
1254
1255 while (!WindowShouldClose())
1256 {
1257 if (IsPaused()) {
1258 // canvas is not visible in browser - do nothing
1259 continue;
1260 }
1261
1262 if (cameraMode == 0)
1263 {
1264 camera.position = (Vector3){0.0f, 10.0f, -0.5f};
1265 camera.target = (Vector3){0.0f, 0.0f, -0.5f};
1266 camera.up = (Vector3){0.0f, 0.0f, -1.0f};
1267 camera.fovy = 12.0f;
1268 camera.projection = CAMERA_ORTHOGRAPHIC;
1269 }
1270 else
1271 {
1272 camera.position = (Vector3){1.0f, 12.0f, 6.5f};
1273 camera.target = (Vector3){0.0f, 0.5f, 1.0f};
1274 camera.up = (Vector3){0.0f, 1.0f, 0.0f};
1275 camera.fovy = 45.0f;
1276 camera.projection = CAMERA_PERSPECTIVE;
1277 }
1278
1279 BeginDrawing();
1280 ClearBackground(DARKBLUE);
1281
1282 BeginMode3D(camera);
1283 DrawGrid(10, 1.0f);
1284 TowerDraw();
1285 EnemyDraw();
1286 ProjectileDraw();
1287 // PathFindingMapDraw();
1288 ParticleDraw();
1289 GameUpdate();
1290
1291 Ray ray = GetScreenToWorldRay(GetMousePosition(), camera);
1292 float planeDistance = ray.position.y / -ray.direction.y;
1293 float planeX = ray.direction.x * planeDistance + ray.position.x;
1294 float planeY = ray.direction.z * planeDistance + ray.position.z;
1295 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
1296 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
1297 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
1298
1299 EndMode3D();
1300
1301 const char *title = "Tower defense tutorial";
1302 int titleWidth = MeasureText(title, 20);
1303 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f + 2, 5 + 2, 20, BLACK);
1304 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f, 5, 20, WHITE);
1305
1306 const char *buttonText = cameraMode == 0 ? "2D" : "3D";
1307 if (Button(buttonText, 10, 10, 80, 30))
1308 {
1309 cameraMode = !cameraMode;
1310 }
1311
1312 EndDrawing();
1313 }
1314
1315 CloseWindow();
1316
1317 return 0;
1318 }
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 the first interactbale part of the game: You can now switch between 2D and 3D mode and hover the mouse over the grid to see the projected mouse position.
The relevant code is this piece here, which is a line-plane intersection calculation for the special case of the x-z plane at height 0:
1 Ray ray = GetScreenToWorldRay(GetMousePosition(), camera);
2 float planeDistance = ray.position.y / -ray.direction.y;
3 float planeX = ray.direction.x * planeDistance + ray.position.x;
4 float planeY = ray.direction.z * planeDistance + ray.position.z;
The variables planeX and planeY are the projected mouse coordinates on the x-z plane at the height 0. Here's an explanation how it works: The distance for the projection of the direction vector is obtained by "looking" how often the vertical component of the ray direction fits into the height of the ray's origin. We then multiply the direction vector with this distance and add the origin to get the projected point.
A simple case is, when we look down from the top: Then the y component is 1, but the x and z components are 0 - so the projection is just the y component of the ray's origin.
Here's a visualization of how the calculation works when seen from the side - which shows that it's nothing else than the intercept theorem:
It's time now to add the building placement. On the left side, we'll have a list of buildings we can place. When we click on a building button, we set it to active and when clicking on the map, this building will be placed and the button will be deactivated. Clicking an activated button again will deactivate it:
1 #include "td-tut-2-main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Declarations
7
8 #define PARTICLE_MAX_COUNT 400
9 #define PARTICLE_TYPE_NONE 0
10 #define PARTICLE_TYPE_EXPLOSION 1
11
12 typedef struct Particle
13 {
14 uint8_t particleType;
15 float spawnTime;
16 float lifetime;
17 Vector3 position;
18 Vector3 velocity;
19 } Particle;
20
21 Particle particles[PARTICLE_MAX_COUNT];
22 int particleCount = 0;
23
24 #define TOWER_MAX_COUNT 400
25 #define TOWER_TYPE_NONE 0
26 #define TOWER_TYPE_BASE 1
27 #define TOWER_TYPE_GUN 2
28 #define TOWER_TYPE_WALL 3
29
30 typedef struct Tower
31 {
32 int16_t x, y;
33 uint8_t towerType;
34 float cooldown;
35 float damage;
36 } Tower;
37
38 typedef struct GameTime
39 {
40 float time;
41 float deltaTime;
42 } GameTime;
43
44 GameTime gameTime = {0};
45
46 Tower towers[TOWER_MAX_COUNT];
47 int towerCount = 0;
48
49 float TowerGetMaxHealth(Tower *tower);
50
51 //# Particle system
52
53 void ParticleInit()
54 {
55 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
56 {
57 particles[i] = (Particle){0};
58 }
59 particleCount = 0;
60 }
61
62 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
63 {
64 if (particleCount >= PARTICLE_MAX_COUNT)
65 {
66 return;
67 }
68
69 int index = -1;
70 for (int i = 0; i < particleCount; i++)
71 {
72 if (particles[i].particleType == PARTICLE_TYPE_NONE)
73 {
74 index = i;
75 break;
76 }
77 }
78
79 if (index == -1)
80 {
81 index = particleCount++;
82 }
83
84 Particle *particle = &particles[index];
85 particle->particleType = particleType;
86 particle->spawnTime = gameTime.time;
87 particle->lifetime = lifetime;
88 particle->position = position;
89 particle->velocity = velocity;
90 }
91
92 void ParticleUpdate()
93 {
94 for (int i = 0; i < particleCount; i++)
95 {
96 Particle *particle = &particles[i];
97 if (particle->particleType == PARTICLE_TYPE_NONE)
98 {
99 continue;
100 }
101
102 float age = gameTime.time - particle->spawnTime;
103
104 if (particle->lifetime > age)
105 {
106 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
107 }
108 else {
109 particle->particleType = PARTICLE_TYPE_NONE;
110 }
111 }
112 }
113
114 void DrawExplosionParticle(Particle *particle, float transition)
115 {
116 float size = 1.2f * (1.0f - transition);
117 Color startColor = WHITE;
118 Color endColor = RED;
119 Color color = ColorLerp(startColor, endColor, transition);
120 DrawCube(particle->position, size, size, size, color);
121 }
122
123 void ParticleDraw()
124 {
125 for (int i = 0; i < particleCount; i++)
126 {
127 Particle particle = particles[i];
128 if (particle.particleType == PARTICLE_TYPE_NONE)
129 {
130 continue;
131 }
132
133 float age = gameTime.time - particle.spawnTime;
134 float transition = age / particle.lifetime;
135 switch (particle.particleType)
136 {
137 case PARTICLE_TYPE_EXPLOSION:
138 DrawExplosionParticle(&particle, transition);
139 break;
140 default:
141 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
142 break;
143 }
144 }
145 }
146
147 //# Pathfinding map
148 typedef struct DeltaSrc
149 {
150 char x, y;
151 } DeltaSrc;
152
153 typedef struct PathfindingMap
154 {
155 int width, height;
156 float scale;
157 float *distances;
158 long *towerIndex;
159 DeltaSrc *deltaSrc;
160 float maxDistance;
161 Matrix toMapSpace;
162 Matrix toWorldSpace;
163 } PathfindingMap;
164
165 // when we execute the pathfinding algorithm, we need to store the active nodes
166 // in a queue. Each node has a position, a distance from the start, and the
167 // position of the node that we came from.
168 typedef struct PathfindingNode
169 {
170 int16_t x, y, fromX, fromY;
171 float distance;
172 } PathfindingNode;
173
174 // The queue is a simple array of nodes, we add nodes to the end and remove
175 // nodes from the front. We keep the array around to avoid unnecessary allocations
176 static PathfindingNode *pathfindingNodeQueue = 0;
177 static int pathfindingNodeQueueCount = 0;
178 static int pathfindingNodeQueueCapacity = 0;
179
180 // The pathfinding map stores the distances from the castle to each cell in the map.
181 PathfindingMap pathfindingMap = {0};
182
183 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
184 {
185 // transforming between map space and world space allows us to adapt
186 // position and scale of the map without changing the pathfinding data
187 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
188 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
189 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
190 pathfindingMap.width = width;
191 pathfindingMap.height = height;
192 pathfindingMap.scale = scale;
193 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
194 for (int i = 0; i < width * height; i++)
195 {
196 pathfindingMap.distances[i] = -1.0f;
197 }
198
199 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
200 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
201 }
202
203 float PathFindingGetDistance(int mapX, int mapY)
204 {
205 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
206 {
207 // when outside the map, we return the manhattan distance to the castle (0,0)
208 return fabsf((float)mapX) + fabsf((float)mapY);
209 }
210
211 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
212 }
213
214 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
215 {
216 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
217 {
218 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
219 // we use MemAlloc/MemRealloc to allocate memory for the queue
220 // I am not entirely sure if MemRealloc allows passing a null pointer
221 // so we check if the pointer is null and use MemAlloc in that case
222 if (pathfindingNodeQueue == 0)
223 {
224 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
225 }
226 else
227 {
228 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
229 }
230 }
231
232 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
233 node->x = x;
234 node->y = y;
235 node->fromX = fromX;
236 node->fromY = fromY;
237 node->distance = distance;
238 }
239
240 PathfindingNode *PathFindingNodePop()
241 {
242 if (pathfindingNodeQueueCount == 0)
243 {
244 return 0;
245 }
246 // we return the first node in the queue; we want to return a pointer to the node
247 // so we can return 0 if the queue is empty.
248 // We should _not_ return a pointer to the element in the list, because the list
249 // may be reallocated and the pointer would become invalid. Or the
250 // popped element is overwritten by the next push operation.
251 // Using static here means that the variable is permanently allocated.
252 static PathfindingNode node;
253 node = pathfindingNodeQueue[0];
254 // we shift all nodes one position to the front
255 for (int i = 1; i < pathfindingNodeQueueCount; i++)
256 {
257 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
258 }
259 --pathfindingNodeQueueCount;
260 return &node;
261 }
262
263 // transform a world position to a map position in the array;
264 // returns true if the position is inside the map
265 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
266 {
267 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
268 *mapX = (int16_t)mapPosition.x;
269 *mapY = (int16_t)mapPosition.z;
270 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
271 }
272
273 void PathFindingMapUpdate()
274 {
275 const int castleX = 0, castleY = 0;
276 int16_t castleMapX, castleMapY;
277 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
278 {
279 return;
280 }
281 int width = pathfindingMap.width, height = pathfindingMap.height;
282
283 // reset the distances to -1
284 for (int i = 0; i < width * height; i++)
285 {
286 pathfindingMap.distances[i] = -1.0f;
287 }
288 // reset the tower indices
289 for (int i = 0; i < width * height; i++)
290 {
291 pathfindingMap.towerIndex[i] = -1;
292 }
293 // reset the delta src
294 for (int i = 0; i < width * height; i++)
295 {
296 pathfindingMap.deltaSrc[i].x = 0;
297 pathfindingMap.deltaSrc[i].y = 0;
298 }
299
300 for (int i = 0; i < towerCount; i++)
301 {
302 Tower *tower = &towers[i];
303 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
304 {
305 continue;
306 }
307 int16_t mapX, mapY;
308 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
309 // this would not work correctly and needs to be refined to allow towers covering multiple cells
310 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
311 // one cell. For now.
312 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
313 {
314 continue;
315 }
316 int index = mapY * width + mapX;
317 pathfindingMap.towerIndex[index] = i;
318 }
319
320 // we start at the castle and add the castle to the queue
321 pathfindingMap.maxDistance = 0.0f;
322 pathfindingNodeQueueCount = 0;
323 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
324 PathfindingNode *node = 0;
325 while ((node = PathFindingNodePop()))
326 {
327 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
328 {
329 continue;
330 }
331 int index = node->y * width + node->x;
332 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
333 {
334 continue;
335 }
336
337 int deltaX = node->x - node->fromX;
338 int deltaY = node->y - node->fromY;
339 // even if the cell is blocked by a tower, we still may want to store the direction
340 // (though this might not be needed, IDK right now)
341 pathfindingMap.deltaSrc[index].x = (char) deltaX;
342 pathfindingMap.deltaSrc[index].y = (char) deltaY;
343
344 // we skip nodes that are blocked by towers
345 if (pathfindingMap.towerIndex[index] >= 0)
346 {
347 node->distance += 8.0f;
348 }
349 pathfindingMap.distances[index] = node->distance;
350 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
351 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
352 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
353 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
354 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
355 }
356 }
357
358 void PathFindingMapDraw()
359 {
360 float cellSize = pathfindingMap.scale * 0.9f;
361 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
362 for (int x = 0; x < pathfindingMap.width; x++)
363 {
364 for (int y = 0; y < pathfindingMap.height; y++)
365 {
366 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
367 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
368 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
369 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
370 // animate the distance "wave" to show how the pathfinding algorithm expands
371 // from the castle
372 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
373 {
374 color = BLACK;
375 }
376 DrawCube(position, cellSize, 0.1f, cellSize, color);
377 }
378 }
379 }
380
381 Vector2 PathFindingGetGradient(Vector3 world)
382 {
383 int16_t mapX, mapY;
384 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
385 {
386 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
387 return (Vector2){(float)-delta.x, (float)-delta.y};
388 }
389 // fallback to a simple gradient calculation
390 float n = PathFindingGetDistance(mapX, mapY - 1);
391 float s = PathFindingGetDistance(mapX, mapY + 1);
392 float w = PathFindingGetDistance(mapX - 1, mapY);
393 float e = PathFindingGetDistance(mapX + 1, mapY);
394 return (Vector2){w - e + 0.25f, n - s + 0.125f};
395 }
396
397 //# Enemies
398
399 #define ENEMY_MAX_PATH_COUNT 8
400 #define ENEMY_MAX_COUNT 400
401 #define ENEMY_TYPE_NONE 0
402 #define ENEMY_TYPE_MINION 1
403
404 typedef struct EnemyId
405 {
406 uint16_t index;
407 uint16_t generation;
408 } EnemyId;
409
410 typedef struct EnemyClassConfig
411 {
412 float speed;
413 float health;
414 float radius;
415 float maxAcceleration;
416 float requiredContactTime;
417 float explosionDamage;
418 float explosionRange;
419 float explosionPushbackPower;
420 } EnemyClassConfig;
421
422 typedef struct Enemy
423 {
424 int16_t currentX, currentY;
425 int16_t nextX, nextY;
426 Vector2 simPosition;
427 Vector2 simVelocity;
428 uint16_t generation;
429 float startMovingTime;
430 float damage, futureDamage;
431 float contactTime;
432 uint8_t enemyType;
433 uint8_t movePathCount;
434 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
435 } Enemy;
436
437 Enemy enemies[ENEMY_MAX_COUNT];
438 int enemyCount = 0;
439
440 EnemyClassConfig enemyClassConfigs[] = {
441 [ENEMY_TYPE_MINION] = {
442 .health = 3.0f,
443 .speed = 1.0f,
444 .radius = 0.25f,
445 .maxAcceleration = 1.0f,
446 .explosionDamage = 1.0f,
447 .requiredContactTime = 0.5f,
448 .explosionRange = 1.0f,
449 .explosionPushbackPower = 0.25f,
450 },
451 };
452
453 int EnemyAddDamage(Enemy *enemy, float damage);
454
455 void EnemyInit()
456 {
457 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
458 {
459 enemies[i] = (Enemy){0};
460 }
461 enemyCount = 0;
462 }
463
464 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
465 {
466 return enemyClassConfigs[enemy->enemyType].speed;
467 }
468
469 float EnemyGetMaxHealth(Enemy *enemy)
470 {
471 return enemyClassConfigs[enemy->enemyType].health;
472 }
473
474 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
475 {
476 int16_t castleX = 0;
477 int16_t castleY = 0;
478 int16_t dx = castleX - currentX;
479 int16_t dy = castleY - currentY;
480 if (dx == 0 && dy == 0)
481 {
482 *nextX = currentX;
483 *nextY = currentY;
484 return 1;
485 }
486 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
487
488 if (gradient.x == 0 && gradient.y == 0)
489 {
490 *nextX = currentX;
491 *nextY = currentY;
492 return 1;
493 }
494
495 if (fabsf(gradient.x) > fabsf(gradient.y))
496 {
497 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
498 *nextY = currentY;
499 return 0;
500 }
501 *nextX = currentX;
502 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
503 return 0;
504 }
505
506
507 // this function predicts the movement of the unit for the next deltaT seconds
508 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
509 {
510 const float pointReachedDistance = 0.25f;
511 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
512 const float maxSimStepTime = 0.015625f;
513
514 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
515 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
516 int16_t nextX = enemy->nextX;
517 int16_t nextY = enemy->nextY;
518 Vector2 position = enemy->simPosition;
519 int passedCount = 0;
520 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
521 {
522 float stepTime = fminf(deltaT - t, maxSimStepTime);
523 Vector2 target = (Vector2){nextX, nextY};
524 float speed = Vector2Length(*velocity);
525 // draw the target position for debugging
526 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
527 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
528 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
529 {
530 // we reached the target position, let's move to the next waypoint
531 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
532 target = (Vector2){nextX, nextY};
533 // track how many waypoints we passed
534 passedCount++;
535 }
536
537 // acceleration towards the target
538 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
539 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
540 *velocity = Vector2Add(*velocity, acceleration);
541
542 // limit the speed to the maximum speed
543 if (speed > maxSpeed)
544 {
545 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
546 }
547
548 // move the enemy
549 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
550 }
551
552 if (waypointPassedCount)
553 {
554 (*waypointPassedCount) = passedCount;
555 }
556
557 return position;
558 }
559
560 void EnemyDraw()
561 {
562 for (int i = 0; i < enemyCount; i++)
563 {
564 Enemy enemy = enemies[i];
565 if (enemy.enemyType == ENEMY_TYPE_NONE)
566 {
567 continue;
568 }
569
570 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
571
572 if (enemy.movePathCount > 0)
573 {
574 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
575 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
576 }
577 for (int j = 1; j < enemy.movePathCount; j++)
578 {
579 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
580 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
581 DrawLine3D(p, q, GREEN);
582 }
583
584 switch (enemy.enemyType)
585 {
586 case ENEMY_TYPE_MINION:
587 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
588 break;
589 }
590 }
591 }
592
593 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
594 {
595 // damage the tower
596 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
597 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
598 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
599 float explosionRange2 = explosionRange * explosionRange;
600 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
601 // explode the enemy
602 if (tower->damage >= TowerGetMaxHealth(tower))
603 {
604 tower->towerType = TOWER_TYPE_NONE;
605 }
606
607 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
608 explosionSource,
609 (Vector3){0, 0.1f, 0}, 1.0f);
610
611 enemy->enemyType = ENEMY_TYPE_NONE;
612
613 // push back enemies & dealing damage
614 for (int i = 0; i < enemyCount; i++)
615 {
616 Enemy *other = &enemies[i];
617 if (other->enemyType == ENEMY_TYPE_NONE)
618 {
619 continue;
620 }
621 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
622 if (distanceSqr > 0 && distanceSqr < explosionRange2)
623 {
624 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
625 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
626 EnemyAddDamage(other, explosionDamge);
627 }
628 }
629 }
630
631 void EnemyUpdate()
632 {
633 const float castleX = 0;
634 const float castleY = 0;
635 const float maxPathDistance2 = 0.25f * 0.25f;
636
637 for (int i = 0; i < enemyCount; i++)
638 {
639 Enemy *enemy = &enemies[i];
640 if (enemy->enemyType == ENEMY_TYPE_NONE)
641 {
642 continue;
643 }
644
645 int waypointPassedCount = 0;
646 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
647 enemy->startMovingTime = gameTime.time;
648 // track path of unit
649 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
650 {
651 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
652 {
653 enemy->movePath[j] = enemy->movePath[j - 1];
654 }
655 enemy->movePath[0] = enemy->simPosition;
656 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
657 {
658 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
659 }
660 }
661
662 if (waypointPassedCount > 0)
663 {
664 enemy->currentX = enemy->nextX;
665 enemy->currentY = enemy->nextY;
666 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
667 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
668 {
669 // enemy reached the castle; remove it
670 enemy->enemyType = ENEMY_TYPE_NONE;
671 continue;
672 }
673 }
674 }
675
676 // handle collisions between enemies
677 for (int i = 0; i < enemyCount - 1; i++)
678 {
679 Enemy *enemyA = &enemies[i];
680 if (enemyA->enemyType == ENEMY_TYPE_NONE)
681 {
682 continue;
683 }
684 for (int j = i + 1; j < enemyCount; j++)
685 {
686 Enemy *enemyB = &enemies[j];
687 if (enemyB->enemyType == ENEMY_TYPE_NONE)
688 {
689 continue;
690 }
691 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
692 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
693 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
694 float radiusSum = radiusA + radiusB;
695 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
696 {
697 // collision
698 float distance = sqrtf(distanceSqr);
699 float overlap = radiusSum - distance;
700 // move the enemies apart, but softly; if we have a clog of enemies,
701 // moving them perfectly apart can cause them to jitter
702 float positionCorrection = overlap / 5.0f;
703 Vector2 direction = (Vector2){
704 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
705 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
706 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
707 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
708 }
709 }
710 }
711
712 // handle collisions between enemies and towers
713 for (int i = 0; i < enemyCount; i++)
714 {
715 Enemy *enemy = &enemies[i];
716 if (enemy->enemyType == ENEMY_TYPE_NONE)
717 {
718 continue;
719 }
720 enemy->contactTime -= gameTime.deltaTime;
721 if (enemy->contactTime < 0.0f)
722 {
723 enemy->contactTime = 0.0f;
724 }
725
726 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
727 // linear search over towers; could be optimized by using path finding tower map,
728 // but for now, we keep it simple
729 for (int j = 0; j < towerCount; j++)
730 {
731 Tower *tower = &towers[j];
732 if (tower->towerType == TOWER_TYPE_NONE)
733 {
734 continue;
735 }
736 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
737 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
738 if (distanceSqr > combinedRadius * combinedRadius)
739 {
740 continue;
741 }
742 // potential collision; square / circle intersection
743 float dx = tower->x - enemy->simPosition.x;
744 float dy = tower->y - enemy->simPosition.y;
745 float absDx = fabsf(dx);
746 float absDy = fabsf(dy);
747 Vector3 contactPoint = {0};
748 if (absDx <= 0.5f && absDx <= absDy) {
749 // vertical collision; push the enemy out horizontally
750 float overlap = enemyRadius + 0.5f - absDy;
751 if (overlap < 0.0f)
752 {
753 continue;
754 }
755 float direction = dy > 0.0f ? -1.0f : 1.0f;
756 enemy->simPosition.y += direction * overlap;
757 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
758 }
759 else if (absDy <= 0.5f && absDy <= absDx)
760 {
761 // horizontal collision; push the enemy out vertically
762 float overlap = enemyRadius + 0.5f - absDx;
763 if (overlap < 0.0f)
764 {
765 continue;
766 }
767 float direction = dx > 0.0f ? -1.0f : 1.0f;
768 enemy->simPosition.x += direction * overlap;
769 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
770 }
771 else
772 {
773 // possible collision with a corner
774 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
775 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
776 float cornerX = tower->x + cornerDX;
777 float cornerY = tower->y + cornerDY;
778 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
779 if (cornerDistanceSqr > enemyRadius * enemyRadius)
780 {
781 continue;
782 }
783 // push the enemy out along the diagonal
784 float cornerDistance = sqrtf(cornerDistanceSqr);
785 float overlap = enemyRadius - cornerDistance;
786 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
787 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
788 enemy->simPosition.x -= directionX * overlap;
789 enemy->simPosition.y -= directionY * overlap;
790 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
791 }
792
793 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
794 {
795 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
796 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
797 {
798 EnemyTriggerExplode(enemy, tower, contactPoint);
799 }
800 }
801 }
802 }
803 }
804
805 EnemyId EnemyGetId(Enemy *enemy)
806 {
807 return (EnemyId){enemy - enemies, enemy->generation};
808 }
809
810 Enemy *EnemyTryResolve(EnemyId enemyId)
811 {
812 if (enemyId.index >= ENEMY_MAX_COUNT)
813 {
814 return 0;
815 }
816 Enemy *enemy = &enemies[enemyId.index];
817 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
818 {
819 return 0;
820 }
821 return enemy;
822 }
823
824 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
825 {
826 Enemy *spawn = 0;
827 for (int i = 0; i < enemyCount; i++)
828 {
829 Enemy *enemy = &enemies[i];
830 if (enemy->enemyType == ENEMY_TYPE_NONE)
831 {
832 spawn = enemy;
833 break;
834 }
835 }
836
837 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
838 {
839 spawn = &enemies[enemyCount++];
840 }
841
842 if (spawn)
843 {
844 spawn->currentX = currentX;
845 spawn->currentY = currentY;
846 spawn->nextX = currentX;
847 spawn->nextY = currentY;
848 spawn->simPosition = (Vector2){currentX, currentY};
849 spawn->simVelocity = (Vector2){0, 0};
850 spawn->enemyType = enemyType;
851 spawn->startMovingTime = gameTime.time;
852 spawn->damage = 0.0f;
853 spawn->futureDamage = 0.0f;
854 spawn->generation++;
855 spawn->movePathCount = 0;
856 }
857
858 return spawn;
859 }
860
861 int EnemyAddDamage(Enemy *enemy, float damage)
862 {
863 enemy->damage += damage;
864 if (enemy->damage >= EnemyGetMaxHealth(enemy))
865 {
866 enemy->enemyType = ENEMY_TYPE_NONE;
867 return 1;
868 }
869
870 return 0;
871 }
872
873 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
874 {
875 int16_t castleX = 0;
876 int16_t castleY = 0;
877 Enemy* closest = 0;
878 int16_t closestDistance = 0;
879 float range2 = range * range;
880 for (int i = 0; i < enemyCount; i++)
881 {
882 Enemy* enemy = &enemies[i];
883 if (enemy->enemyType == ENEMY_TYPE_NONE)
884 {
885 continue;
886 }
887 float maxHealth = EnemyGetMaxHealth(enemy);
888 if (enemy->futureDamage >= maxHealth)
889 {
890 // ignore enemies that will die soon
891 continue;
892 }
893 int16_t dx = castleX - enemy->currentX;
894 int16_t dy = castleY - enemy->currentY;
895 int16_t distance = abs(dx) + abs(dy);
896 if (!closest || distance < closestDistance)
897 {
898 float tdx = towerX - enemy->currentX;
899 float tdy = towerY - enemy->currentY;
900 float tdistance2 = tdx * tdx + tdy * tdy;
901 if (tdistance2 <= range2)
902 {
903 closest = enemy;
904 closestDistance = distance;
905 }
906 }
907 }
908 return closest;
909 }
910
911 int EnemyCount()
912 {
913 int count = 0;
914 for (int i = 0; i < enemyCount; i++)
915 {
916 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
917 {
918 count++;
919 }
920 }
921 return count;
922 }
923
924 //# Projectiles
925 #define PROJECTILE_MAX_COUNT 1200
926 #define PROJECTILE_TYPE_NONE 0
927 #define PROJECTILE_TYPE_BULLET 1
928
929 typedef struct Projectile
930 {
931 uint8_t projectileType;
932 float shootTime;
933 float arrivalTime;
934 float damage;
935 Vector2 position;
936 Vector2 target;
937 Vector2 directionNormal;
938 EnemyId targetEnemy;
939 } Projectile;
940
941 Projectile projectiles[PROJECTILE_MAX_COUNT];
942 int projectileCount = 0;
943
944 void ProjectileInit()
945 {
946 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
947 {
948 projectiles[i] = (Projectile){0};
949 }
950 }
951
952 void ProjectileDraw()
953 {
954 for (int i = 0; i < projectileCount; i++)
955 {
956 Projectile projectile = projectiles[i];
957 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
958 {
959 continue;
960 }
961 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
962 if (transition >= 1.0f)
963 {
964 continue;
965 }
966 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
967 float x = position.x;
968 float y = position.y;
969 float dx = projectile.directionNormal.x;
970 float dy = projectile.directionNormal.y;
971 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
972 {
973 x -= dx * 0.1f;
974 y -= dy * 0.1f;
975 float size = 0.1f * d;
976 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
977 }
978 }
979 }
980
981 void ProjectileUpdate()
982 {
983 for (int i = 0; i < projectileCount; i++)
984 {
985 Projectile *projectile = &projectiles[i];
986 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
987 {
988 continue;
989 }
990 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
991 if (transition >= 1.0f)
992 {
993 projectile->projectileType = PROJECTILE_TYPE_NONE;
994 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
995 if (enemy)
996 {
997 EnemyAddDamage(enemy, projectile->damage);
998 }
999 continue;
1000 }
1001 }
1002 }
1003
1004 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
1005 {
1006 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
1007 {
1008 Projectile *projectile = &projectiles[i];
1009 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
1010 {
1011 projectile->projectileType = projectileType;
1012 projectile->shootTime = gameTime.time;
1013 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
1014 projectile->damage = damage;
1015 projectile->position = position;
1016 projectile->target = target;
1017 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
1018 projectile->targetEnemy = EnemyGetId(enemy);
1019 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
1020 return projectile;
1021 }
1022 }
1023 return 0;
1024 }
1025
1026 //# Towers
1027
1028 void TowerInit()
1029 {
1030 for (int i = 0; i < TOWER_MAX_COUNT; i++)
1031 {
1032 towers[i] = (Tower){0};
1033 }
1034 towerCount = 0;
1035 }
1036
1037 Tower *TowerGetAt(int16_t x, int16_t y)
1038 {
1039 for (int i = 0; i < towerCount; i++)
1040 {
1041 if (towers[i].x == x && towers[i].y == y)
1042 {
1043 return &towers[i];
1044 }
1045 }
1046 return 0;
1047 }
1048
1049 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
1050 {
1051 if (towerCount >= TOWER_MAX_COUNT)
1052 {
1053 return 0;
1054 }
1055
1056 Tower *tower = TowerGetAt(x, y);
1057 if (tower)
1058 {
1059 return 0;
1060 }
1061
1062 tower = &towers[towerCount++];
1063 tower->x = x;
1064 tower->y = y;
1065 tower->towerType = towerType;
1066 tower->cooldown = 0.0f;
1067 tower->damage = 0.0f;
1068 return tower;
1069 }
1070
1071 float TowerGetMaxHealth(Tower *tower)
1072 {
1073 switch (tower->towerType)
1074 {
1075 case TOWER_TYPE_BASE:
1076 return 10.0f;
1077 case TOWER_TYPE_GUN:
1078 return 3.0f;
1079 case TOWER_TYPE_WALL:
1080 return 5.0f;
1081 }
1082 return 0.0f;
1083 }
1084
1085 void TowerDraw()
1086 {
1087 for (int i = 0; i < towerCount; i++)
1088 {
1089 Tower tower = towers[i];
1090 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
1091 switch (tower.towerType)
1092 {
1093 case TOWER_TYPE_BASE:
1094 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
1095 break;
1096 case TOWER_TYPE_GUN:
1097 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
1098 break;
1099 case TOWER_TYPE_WALL:
1100 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
1101 break;
1102 }
1103 }
1104 }
1105
1106 void TowerGunUpdate(Tower *tower)
1107 {
1108 if (tower->cooldown <= 0)
1109 {
1110 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
1111 if (enemy)
1112 {
1113 tower->cooldown = 0.125f;
1114 // shoot the enemy; determine future position of the enemy
1115 float bulletSpeed = 1.0f;
1116 float bulletDamage = 3.0f;
1117 Vector2 velocity = enemy->simVelocity;
1118 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
1119 Vector2 towerPosition = {tower->x, tower->y};
1120 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
1121 for (int i = 0; i < 8; i++) {
1122 velocity = enemy->simVelocity;
1123 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
1124 float distance = Vector2Distance(towerPosition, futurePosition);
1125 float eta2 = distance / bulletSpeed;
1126 if (fabs(eta - eta2) < 0.01f) {
1127 break;
1128 }
1129 eta = (eta2 + eta) * 0.5f;
1130 }
1131 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
1132 bulletSpeed, bulletDamage);
1133 enemy->futureDamage += bulletDamage;
1134 }
1135 }
1136 else
1137 {
1138 tower->cooldown -= gameTime.deltaTime;
1139 }
1140 }
1141
1142 void TowerUpdate()
1143 {
1144 for (int i = 0; i < towerCount; i++)
1145 {
1146 Tower *tower = &towers[i];
1147 switch (tower->towerType)
1148 {
1149 case TOWER_TYPE_GUN:
1150 TowerGunUpdate(tower);
1151 break;
1152 }
1153 }
1154 }
1155
1156 //# Game
1157
1158 float nextSpawnTime = 0.0f;
1159
1160 void InitGame()
1161 {
1162 TowerInit();
1163 EnemyInit();
1164 ProjectileInit();
1165 ParticleInit();
1166 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1167
1168 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1169 TowerTryAdd(TOWER_TYPE_GUN, 2, 0);
1170
1171 for (int i = -2; i <= 2; i += 1)
1172 {
1173 TowerTryAdd(TOWER_TYPE_WALL, i, 2);
1174 TowerTryAdd(TOWER_TYPE_WALL, i, -2);
1175 TowerTryAdd(TOWER_TYPE_WALL, -2, i);
1176 }
1177
1178 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4);
1179 }
1180
1181 //# Immediate GUI functions
1182
1183 typedef struct ButtonState {
1184 char isSelected;
1185 } ButtonState;
1186
1187 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1188 {
1189 Rectangle bounds = {x, y, width, height};
1190 int isPressed = 0;
1191 int isSelected = state && state->isSelected;
1192 if (CheckCollisionPointRec(GetMousePosition(), bounds))
1193 {
1194 Color color = isSelected ? DARKGRAY : GRAY;
1195 DrawRectangle(x, y, width, height, color);
1196 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1197 {
1198 isPressed = 1;
1199 }
1200 }
1201 else
1202 {
1203 Color color = isSelected ? WHITE : LIGHTGRAY;
1204 DrawRectangle(x, y, width, height, color);
1205 }
1206 Font font = GetFontDefault();
1207 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1208 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, BLACK);
1209 return isPressed;
1210 }
1211
1212 //# Main game loop
1213
1214 void GameUpdate()
1215 {
1216 float dt = GetFrameTime();
1217 // cap maximum delta time to 0.1 seconds to prevent large time steps
1218 if (dt > 0.1f) dt = 0.1f;
1219 gameTime.time += dt;
1220 gameTime.deltaTime = dt;
1221 PathFindingMapUpdate();
1222 EnemyUpdate();
1223 TowerUpdate();
1224 ProjectileUpdate();
1225 ParticleUpdate();
1226
1227 // spawn a new enemy every second
1228 if (gameTime.time >= nextSpawnTime && EnemyCount() < 50)
1229 {
1230 nextSpawnTime = gameTime.time + 0.2f;
1231 // add a new enemy at the boundary of the map
1232 int randValue = GetRandomValue(-5, 5);
1233 int randSide = GetRandomValue(0, 3);
1234 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue;
1235 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue;
1236 static int alternation = 0;
1237 alternation += 1;
1238 if (alternation % 3 == 0) {
1239 EnemyTryAdd(ENEMY_TYPE_MINION, 0, -5);
1240 }
1241 else if (alternation % 3 == 1)
1242 {
1243 EnemyTryAdd(ENEMY_TYPE_MINION, 0, 5);
1244 }
1245 EnemyTryAdd(ENEMY_TYPE_MINION, x, y);
1246 }
1247 }
1248
1249 int main(void)
1250 {
1251 int screenWidth, screenHeight;
1252 GetPreferredSize(&screenWidth, &screenHeight);
1253 InitWindow(screenWidth, screenHeight, "Tower defense");
1254 SetTargetFPS(30);
1255
1256 Camera3D camera = {0};
1257
1258 int cameraMode = 0;
1259
1260 InitGame();
1261
1262 while (!WindowShouldClose())
1263 {
1264 if (IsPaused()) {
1265 // canvas is not visible in browser - do nothing
1266 continue;
1267 }
1268
1269 if (cameraMode == 0)
1270 {
1271 camera.position = (Vector3){0.0f, 10.0f, -0.5f};
1272 camera.target = (Vector3){0.0f, 0.0f, -0.5f};
1273 camera.up = (Vector3){0.0f, 0.0f, -1.0f};
1274 camera.fovy = 12.0f;
1275 camera.projection = CAMERA_ORTHOGRAPHIC;
1276 }
1277 else
1278 {
1279 camera.position = (Vector3){1.0f, 12.0f, 6.5f};
1280 camera.target = (Vector3){0.0f, 0.5f, 1.0f};
1281 camera.up = (Vector3){0.0f, 1.0f, 0.0f};
1282 camera.fovy = 45.0f;
1283 camera.projection = CAMERA_PERSPECTIVE;
1284 }
1285
1286 BeginDrawing();
1287 ClearBackground(DARKBLUE);
1288
1289 BeginMode3D(camera);
1290 DrawGrid(10, 1.0f);
1291 TowerDraw();
1292 EnemyDraw();
1293 ProjectileDraw();
1294 // PathFindingMapDraw();
1295 ParticleDraw();
1296 GameUpdate();
1297
1298 static int placementMode = TOWER_TYPE_NONE;
1299
1300 Ray ray = GetScreenToWorldRay(GetMousePosition(), camera);
1301 float planeDistance = ray.position.y / -ray.direction.y;
1302 float planeX = ray.direction.x * planeDistance + ray.position.x;
1303 float planeY = ray.direction.z * planeDistance + ray.position.z;
1304 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
1305 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
1306 if (placementMode)
1307 {
1308 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
1309 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1310 {
1311 TowerTryAdd(placementMode, mapX, mapY);
1312 placementMode = TOWER_TYPE_NONE;
1313 }
1314 }
1315
1316 EndMode3D();
1317
1318 const char *title = "Tower defense tutorial";
1319 int titleWidth = MeasureText(title, 20);
1320 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f + 2, 5 + 2, 20, BLACK);
1321 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f, 5, 20, WHITE);
1322
1323 const char *buttonText = cameraMode == 0 ? "2D" : "3D";
1324 if (Button(buttonText, 10, 10, 80, 30, 0))
1325 {
1326 cameraMode = !cameraMode;
1327 }
1328
1329 static ButtonState buildWallButtonState = {0};
1330 static ButtonState buildGunButtonState = {0};
1331 buildWallButtonState.isSelected = placementMode == TOWER_TYPE_WALL;
1332 buildGunButtonState.isSelected = placementMode == TOWER_TYPE_GUN;
1333
1334 if (Button("Wall", 10, 50, 80, 30, &buildWallButtonState))
1335 {
1336 placementMode = buildWallButtonState.isSelected ? 0 : TOWER_TYPE_WALL;
1337 }
1338 if (Button("Gun", 10, 90, 80, 30, &buildGunButtonState))
1339 {
1340 placementMode = buildGunButtonState.isSelected ? 0 : TOWER_TYPE_GUN;
1341 }
1342
1343 EndDrawing();
1344 }
1345
1346 CloseWindow();
1347
1348 return 0;
1349 }
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
Placing stuff is pretty nice. But right now, the UI is click through! We need to prevent the mouse ray to be active, when the mouse hovers a button. The solution I use here is to introduce a gui state flag that is set when the mouse is over a button:
1 typedef struct GUIState {
2 int isBlocked;
3 } GUIState;
4
5 GUIState guiState = {0};
This flag is reset during the update loop and set when the mouse is over a button. The ray casting is then only done when the flag is not set. This is a simple solution that works well for our case.
Let's also add now a button to reset the game:
1 #include "td-tut-2-main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Declarations
7
8 #define PARTICLE_MAX_COUNT 400
9 #define PARTICLE_TYPE_NONE 0
10 #define PARTICLE_TYPE_EXPLOSION 1
11
12 typedef struct Particle
13 {
14 uint8_t particleType;
15 float spawnTime;
16 float lifetime;
17 Vector3 position;
18 Vector3 velocity;
19 } Particle;
20
21 Particle particles[PARTICLE_MAX_COUNT];
22 int particleCount = 0;
23
24 #define TOWER_MAX_COUNT 400
25 #define TOWER_TYPE_NONE 0
26 #define TOWER_TYPE_BASE 1
27 #define TOWER_TYPE_GUN 2
28 #define TOWER_TYPE_WALL 3
29
30 typedef struct Tower
31 {
32 int16_t x, y;
33 uint8_t towerType;
34 float cooldown;
35 float damage;
36 } Tower;
37
38 typedef struct GameTime
39 {
40 float time;
41 float deltaTime;
42 } GameTime;
43
44 GameTime gameTime = {0};
45
46 Tower towers[TOWER_MAX_COUNT];
47 int towerCount = 0;
48
49 float TowerGetMaxHealth(Tower *tower);
50
51 //# Particle system
52
53 void ParticleInit()
54 {
55 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
56 {
57 particles[i] = (Particle){0};
58 }
59 particleCount = 0;
60 }
61
62 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
63 {
64 if (particleCount >= PARTICLE_MAX_COUNT)
65 {
66 return;
67 }
68
69 int index = -1;
70 for (int i = 0; i < particleCount; i++)
71 {
72 if (particles[i].particleType == PARTICLE_TYPE_NONE)
73 {
74 index = i;
75 break;
76 }
77 }
78
79 if (index == -1)
80 {
81 index = particleCount++;
82 }
83
84 Particle *particle = &particles[index];
85 particle->particleType = particleType;
86 particle->spawnTime = gameTime.time;
87 particle->lifetime = lifetime;
88 particle->position = position;
89 particle->velocity = velocity;
90 }
91
92 void ParticleUpdate()
93 {
94 for (int i = 0; i < particleCount; i++)
95 {
96 Particle *particle = &particles[i];
97 if (particle->particleType == PARTICLE_TYPE_NONE)
98 {
99 continue;
100 }
101
102 float age = gameTime.time - particle->spawnTime;
103
104 if (particle->lifetime > age)
105 {
106 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
107 }
108 else {
109 particle->particleType = PARTICLE_TYPE_NONE;
110 }
111 }
112 }
113
114 void DrawExplosionParticle(Particle *particle, float transition)
115 {
116 float size = 1.2f * (1.0f - transition);
117 Color startColor = WHITE;
118 Color endColor = RED;
119 Color color = ColorLerp(startColor, endColor, transition);
120 DrawCube(particle->position, size, size, size, color);
121 }
122
123 void ParticleDraw()
124 {
125 for (int i = 0; i < particleCount; i++)
126 {
127 Particle particle = particles[i];
128 if (particle.particleType == PARTICLE_TYPE_NONE)
129 {
130 continue;
131 }
132
133 float age = gameTime.time - particle.spawnTime;
134 float transition = age / particle.lifetime;
135 switch (particle.particleType)
136 {
137 case PARTICLE_TYPE_EXPLOSION:
138 DrawExplosionParticle(&particle, transition);
139 break;
140 default:
141 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
142 break;
143 }
144 }
145 }
146
147 //# Pathfinding map
148 typedef struct DeltaSrc
149 {
150 char x, y;
151 } DeltaSrc;
152
153 typedef struct PathfindingMap
154 {
155 int width, height;
156 float scale;
157 float *distances;
158 long *towerIndex;
159 DeltaSrc *deltaSrc;
160 float maxDistance;
161 Matrix toMapSpace;
162 Matrix toWorldSpace;
163 } PathfindingMap;
164
165 // when we execute the pathfinding algorithm, we need to store the active nodes
166 // in a queue. Each node has a position, a distance from the start, and the
167 // position of the node that we came from.
168 typedef struct PathfindingNode
169 {
170 int16_t x, y, fromX, fromY;
171 float distance;
172 } PathfindingNode;
173
174 // The queue is a simple array of nodes, we add nodes to the end and remove
175 // nodes from the front. We keep the array around to avoid unnecessary allocations
176 static PathfindingNode *pathfindingNodeQueue = 0;
177 static int pathfindingNodeQueueCount = 0;
178 static int pathfindingNodeQueueCapacity = 0;
179
180 // The pathfinding map stores the distances from the castle to each cell in the map.
181 PathfindingMap pathfindingMap = {0};
182
183 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
184 {
185 // transforming between map space and world space allows us to adapt
186 // position and scale of the map without changing the pathfinding data
187 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
188 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
189 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
190 pathfindingMap.width = width;
191 pathfindingMap.height = height;
192 pathfindingMap.scale = scale;
193 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
194 for (int i = 0; i < width * height; i++)
195 {
196 pathfindingMap.distances[i] = -1.0f;
197 }
198
199 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
200 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
201 }
202
203 float PathFindingGetDistance(int mapX, int mapY)
204 {
205 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
206 {
207 // when outside the map, we return the manhattan distance to the castle (0,0)
208 return fabsf((float)mapX) + fabsf((float)mapY);
209 }
210
211 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
212 }
213
214 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
215 {
216 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
217 {
218 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
219 // we use MemAlloc/MemRealloc to allocate memory for the queue
220 // I am not entirely sure if MemRealloc allows passing a null pointer
221 // so we check if the pointer is null and use MemAlloc in that case
222 if (pathfindingNodeQueue == 0)
223 {
224 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
225 }
226 else
227 {
228 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
229 }
230 }
231
232 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
233 node->x = x;
234 node->y = y;
235 node->fromX = fromX;
236 node->fromY = fromY;
237 node->distance = distance;
238 }
239
240 PathfindingNode *PathFindingNodePop()
241 {
242 if (pathfindingNodeQueueCount == 0)
243 {
244 return 0;
245 }
246 // we return the first node in the queue; we want to return a pointer to the node
247 // so we can return 0 if the queue is empty.
248 // We should _not_ return a pointer to the element in the list, because the list
249 // may be reallocated and the pointer would become invalid. Or the
250 // popped element is overwritten by the next push operation.
251 // Using static here means that the variable is permanently allocated.
252 static PathfindingNode node;
253 node = pathfindingNodeQueue[0];
254 // we shift all nodes one position to the front
255 for (int i = 1; i < pathfindingNodeQueueCount; i++)
256 {
257 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
258 }
259 --pathfindingNodeQueueCount;
260 return &node;
261 }
262
263 // transform a world position to a map position in the array;
264 // returns true if the position is inside the map
265 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
266 {
267 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
268 *mapX = (int16_t)mapPosition.x;
269 *mapY = (int16_t)mapPosition.z;
270 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
271 }
272
273 void PathFindingMapUpdate()
274 {
275 const int castleX = 0, castleY = 0;
276 int16_t castleMapX, castleMapY;
277 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
278 {
279 return;
280 }
281 int width = pathfindingMap.width, height = pathfindingMap.height;
282
283 // reset the distances to -1
284 for (int i = 0; i < width * height; i++)
285 {
286 pathfindingMap.distances[i] = -1.0f;
287 }
288 // reset the tower indices
289 for (int i = 0; i < width * height; i++)
290 {
291 pathfindingMap.towerIndex[i] = -1;
292 }
293 // reset the delta src
294 for (int i = 0; i < width * height; i++)
295 {
296 pathfindingMap.deltaSrc[i].x = 0;
297 pathfindingMap.deltaSrc[i].y = 0;
298 }
299
300 for (int i = 0; i < towerCount; i++)
301 {
302 Tower *tower = &towers[i];
303 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
304 {
305 continue;
306 }
307 int16_t mapX, mapY;
308 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
309 // this would not work correctly and needs to be refined to allow towers covering multiple cells
310 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
311 // one cell. For now.
312 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
313 {
314 continue;
315 }
316 int index = mapY * width + mapX;
317 pathfindingMap.towerIndex[index] = i;
318 }
319
320 // we start at the castle and add the castle to the queue
321 pathfindingMap.maxDistance = 0.0f;
322 pathfindingNodeQueueCount = 0;
323 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
324 PathfindingNode *node = 0;
325 while ((node = PathFindingNodePop()))
326 {
327 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
328 {
329 continue;
330 }
331 int index = node->y * width + node->x;
332 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
333 {
334 continue;
335 }
336
337 int deltaX = node->x - node->fromX;
338 int deltaY = node->y - node->fromY;
339 // even if the cell is blocked by a tower, we still may want to store the direction
340 // (though this might not be needed, IDK right now)
341 pathfindingMap.deltaSrc[index].x = (char) deltaX;
342 pathfindingMap.deltaSrc[index].y = (char) deltaY;
343
344 // we skip nodes that are blocked by towers
345 if (pathfindingMap.towerIndex[index] >= 0)
346 {
347 node->distance += 8.0f;
348 }
349 pathfindingMap.distances[index] = node->distance;
350 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
351 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
352 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
353 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
354 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
355 }
356 }
357
358 void PathFindingMapDraw()
359 {
360 float cellSize = pathfindingMap.scale * 0.9f;
361 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
362 for (int x = 0; x < pathfindingMap.width; x++)
363 {
364 for (int y = 0; y < pathfindingMap.height; y++)
365 {
366 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
367 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
368 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
369 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
370 // animate the distance "wave" to show how the pathfinding algorithm expands
371 // from the castle
372 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
373 {
374 color = BLACK;
375 }
376 DrawCube(position, cellSize, 0.1f, cellSize, color);
377 }
378 }
379 }
380
381 Vector2 PathFindingGetGradient(Vector3 world)
382 {
383 int16_t mapX, mapY;
384 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
385 {
386 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
387 return (Vector2){(float)-delta.x, (float)-delta.y};
388 }
389 // fallback to a simple gradient calculation
390 float n = PathFindingGetDistance(mapX, mapY - 1);
391 float s = PathFindingGetDistance(mapX, mapY + 1);
392 float w = PathFindingGetDistance(mapX - 1, mapY);
393 float e = PathFindingGetDistance(mapX + 1, mapY);
394 return (Vector2){w - e + 0.25f, n - s + 0.125f};
395 }
396
397 //# Enemies
398
399 #define ENEMY_MAX_PATH_COUNT 8
400 #define ENEMY_MAX_COUNT 400
401 #define ENEMY_TYPE_NONE 0
402 #define ENEMY_TYPE_MINION 1
403
404 typedef struct EnemyId
405 {
406 uint16_t index;
407 uint16_t generation;
408 } EnemyId;
409
410 typedef struct EnemyClassConfig
411 {
412 float speed;
413 float health;
414 float radius;
415 float maxAcceleration;
416 float requiredContactTime;
417 float explosionDamage;
418 float explosionRange;
419 float explosionPushbackPower;
420 } EnemyClassConfig;
421
422 typedef struct Enemy
423 {
424 int16_t currentX, currentY;
425 int16_t nextX, nextY;
426 Vector2 simPosition;
427 Vector2 simVelocity;
428 uint16_t generation;
429 float startMovingTime;
430 float damage, futureDamage;
431 float contactTime;
432 uint8_t enemyType;
433 uint8_t movePathCount;
434 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
435 } Enemy;
436
437 Enemy enemies[ENEMY_MAX_COUNT];
438 int enemyCount = 0;
439
440 EnemyClassConfig enemyClassConfigs[] = {
441 [ENEMY_TYPE_MINION] = {
442 .health = 3.0f,
443 .speed = 1.0f,
444 .radius = 0.25f,
445 .maxAcceleration = 1.0f,
446 .explosionDamage = 1.0f,
447 .requiredContactTime = 0.5f,
448 .explosionRange = 1.0f,
449 .explosionPushbackPower = 0.25f,
450 },
451 };
452
453 int EnemyAddDamage(Enemy *enemy, float damage);
454
455 void EnemyInit()
456 {
457 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
458 {
459 enemies[i] = (Enemy){0};
460 }
461 enemyCount = 0;
462 }
463
464 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
465 {
466 return enemyClassConfigs[enemy->enemyType].speed;
467 }
468
469 float EnemyGetMaxHealth(Enemy *enemy)
470 {
471 return enemyClassConfigs[enemy->enemyType].health;
472 }
473
474 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
475 {
476 int16_t castleX = 0;
477 int16_t castleY = 0;
478 int16_t dx = castleX - currentX;
479 int16_t dy = castleY - currentY;
480 if (dx == 0 && dy == 0)
481 {
482 *nextX = currentX;
483 *nextY = currentY;
484 return 1;
485 }
486 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
487
488 if (gradient.x == 0 && gradient.y == 0)
489 {
490 *nextX = currentX;
491 *nextY = currentY;
492 return 1;
493 }
494
495 if (fabsf(gradient.x) > fabsf(gradient.y))
496 {
497 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
498 *nextY = currentY;
499 return 0;
500 }
501 *nextX = currentX;
502 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
503 return 0;
504 }
505
506
507 // this function predicts the movement of the unit for the next deltaT seconds
508 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
509 {
510 const float pointReachedDistance = 0.25f;
511 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
512 const float maxSimStepTime = 0.015625f;
513
514 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
515 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
516 int16_t nextX = enemy->nextX;
517 int16_t nextY = enemy->nextY;
518 Vector2 position = enemy->simPosition;
519 int passedCount = 0;
520 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
521 {
522 float stepTime = fminf(deltaT - t, maxSimStepTime);
523 Vector2 target = (Vector2){nextX, nextY};
524 float speed = Vector2Length(*velocity);
525 // draw the target position for debugging
526 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
527 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
528 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
529 {
530 // we reached the target position, let's move to the next waypoint
531 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
532 target = (Vector2){nextX, nextY};
533 // track how many waypoints we passed
534 passedCount++;
535 }
536
537 // acceleration towards the target
538 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
539 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
540 *velocity = Vector2Add(*velocity, acceleration);
541
542 // limit the speed to the maximum speed
543 if (speed > maxSpeed)
544 {
545 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
546 }
547
548 // move the enemy
549 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
550 }
551
552 if (waypointPassedCount)
553 {
554 (*waypointPassedCount) = passedCount;
555 }
556
557 return position;
558 }
559
560 void EnemyDraw()
561 {
562 for (int i = 0; i < enemyCount; i++)
563 {
564 Enemy enemy = enemies[i];
565 if (enemy.enemyType == ENEMY_TYPE_NONE)
566 {
567 continue;
568 }
569
570 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
571
572 if (enemy.movePathCount > 0)
573 {
574 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
575 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
576 }
577 for (int j = 1; j < enemy.movePathCount; j++)
578 {
579 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
580 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
581 DrawLine3D(p, q, GREEN);
582 }
583
584 switch (enemy.enemyType)
585 {
586 case ENEMY_TYPE_MINION:
587 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
588 break;
589 }
590 }
591 }
592
593 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
594 {
595 // damage the tower
596 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
597 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
598 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
599 float explosionRange2 = explosionRange * explosionRange;
600 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
601 // explode the enemy
602 if (tower->damage >= TowerGetMaxHealth(tower))
603 {
604 tower->towerType = TOWER_TYPE_NONE;
605 }
606
607 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
608 explosionSource,
609 (Vector3){0, 0.1f, 0}, 1.0f);
610
611 enemy->enemyType = ENEMY_TYPE_NONE;
612
613 // push back enemies & dealing damage
614 for (int i = 0; i < enemyCount; i++)
615 {
616 Enemy *other = &enemies[i];
617 if (other->enemyType == ENEMY_TYPE_NONE)
618 {
619 continue;
620 }
621 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
622 if (distanceSqr > 0 && distanceSqr < explosionRange2)
623 {
624 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
625 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
626 EnemyAddDamage(other, explosionDamge);
627 }
628 }
629 }
630
631 void EnemyUpdate()
632 {
633 const float castleX = 0;
634 const float castleY = 0;
635 const float maxPathDistance2 = 0.25f * 0.25f;
636
637 for (int i = 0; i < enemyCount; i++)
638 {
639 Enemy *enemy = &enemies[i];
640 if (enemy->enemyType == ENEMY_TYPE_NONE)
641 {
642 continue;
643 }
644
645 int waypointPassedCount = 0;
646 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
647 enemy->startMovingTime = gameTime.time;
648 // track path of unit
649 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
650 {
651 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
652 {
653 enemy->movePath[j] = enemy->movePath[j - 1];
654 }
655 enemy->movePath[0] = enemy->simPosition;
656 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
657 {
658 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
659 }
660 }
661
662 if (waypointPassedCount > 0)
663 {
664 enemy->currentX = enemy->nextX;
665 enemy->currentY = enemy->nextY;
666 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
667 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
668 {
669 // enemy reached the castle; remove it
670 enemy->enemyType = ENEMY_TYPE_NONE;
671 continue;
672 }
673 }
674 }
675
676 // handle collisions between enemies
677 for (int i = 0; i < enemyCount - 1; i++)
678 {
679 Enemy *enemyA = &enemies[i];
680 if (enemyA->enemyType == ENEMY_TYPE_NONE)
681 {
682 continue;
683 }
684 for (int j = i + 1; j < enemyCount; j++)
685 {
686 Enemy *enemyB = &enemies[j];
687 if (enemyB->enemyType == ENEMY_TYPE_NONE)
688 {
689 continue;
690 }
691 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
692 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
693 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
694 float radiusSum = radiusA + radiusB;
695 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
696 {
697 // collision
698 float distance = sqrtf(distanceSqr);
699 float overlap = radiusSum - distance;
700 // move the enemies apart, but softly; if we have a clog of enemies,
701 // moving them perfectly apart can cause them to jitter
702 float positionCorrection = overlap / 5.0f;
703 Vector2 direction = (Vector2){
704 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
705 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
706 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
707 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
708 }
709 }
710 }
711
712 // handle collisions between enemies and towers
713 for (int i = 0; i < enemyCount; i++)
714 {
715 Enemy *enemy = &enemies[i];
716 if (enemy->enemyType == ENEMY_TYPE_NONE)
717 {
718 continue;
719 }
720 enemy->contactTime -= gameTime.deltaTime;
721 if (enemy->contactTime < 0.0f)
722 {
723 enemy->contactTime = 0.0f;
724 }
725
726 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
727 // linear search over towers; could be optimized by using path finding tower map,
728 // but for now, we keep it simple
729 for (int j = 0; j < towerCount; j++)
730 {
731 Tower *tower = &towers[j];
732 if (tower->towerType == TOWER_TYPE_NONE)
733 {
734 continue;
735 }
736 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
737 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
738 if (distanceSqr > combinedRadius * combinedRadius)
739 {
740 continue;
741 }
742 // potential collision; square / circle intersection
743 float dx = tower->x - enemy->simPosition.x;
744 float dy = tower->y - enemy->simPosition.y;
745 float absDx = fabsf(dx);
746 float absDy = fabsf(dy);
747 Vector3 contactPoint = {0};
748 if (absDx <= 0.5f && absDx <= absDy) {
749 // vertical collision; push the enemy out horizontally
750 float overlap = enemyRadius + 0.5f - absDy;
751 if (overlap < 0.0f)
752 {
753 continue;
754 }
755 float direction = dy > 0.0f ? -1.0f : 1.0f;
756 enemy->simPosition.y += direction * overlap;
757 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
758 }
759 else if (absDy <= 0.5f && absDy <= absDx)
760 {
761 // horizontal collision; push the enemy out vertically
762 float overlap = enemyRadius + 0.5f - absDx;
763 if (overlap < 0.0f)
764 {
765 continue;
766 }
767 float direction = dx > 0.0f ? -1.0f : 1.0f;
768 enemy->simPosition.x += direction * overlap;
769 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
770 }
771 else
772 {
773 // possible collision with a corner
774 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
775 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
776 float cornerX = tower->x + cornerDX;
777 float cornerY = tower->y + cornerDY;
778 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
779 if (cornerDistanceSqr > enemyRadius * enemyRadius)
780 {
781 continue;
782 }
783 // push the enemy out along the diagonal
784 float cornerDistance = sqrtf(cornerDistanceSqr);
785 float overlap = enemyRadius - cornerDistance;
786 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
787 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
788 enemy->simPosition.x -= directionX * overlap;
789 enemy->simPosition.y -= directionY * overlap;
790 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
791 }
792
793 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
794 {
795 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
796 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
797 {
798 EnemyTriggerExplode(enemy, tower, contactPoint);
799 }
800 }
801 }
802 }
803 }
804
805 EnemyId EnemyGetId(Enemy *enemy)
806 {
807 return (EnemyId){enemy - enemies, enemy->generation};
808 }
809
810 Enemy *EnemyTryResolve(EnemyId enemyId)
811 {
812 if (enemyId.index >= ENEMY_MAX_COUNT)
813 {
814 return 0;
815 }
816 Enemy *enemy = &enemies[enemyId.index];
817 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
818 {
819 return 0;
820 }
821 return enemy;
822 }
823
824 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
825 {
826 Enemy *spawn = 0;
827 for (int i = 0; i < enemyCount; i++)
828 {
829 Enemy *enemy = &enemies[i];
830 if (enemy->enemyType == ENEMY_TYPE_NONE)
831 {
832 spawn = enemy;
833 break;
834 }
835 }
836
837 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
838 {
839 spawn = &enemies[enemyCount++];
840 }
841
842 if (spawn)
843 {
844 spawn->currentX = currentX;
845 spawn->currentY = currentY;
846 spawn->nextX = currentX;
847 spawn->nextY = currentY;
848 spawn->simPosition = (Vector2){currentX, currentY};
849 spawn->simVelocity = (Vector2){0, 0};
850 spawn->enemyType = enemyType;
851 spawn->startMovingTime = gameTime.time;
852 spawn->damage = 0.0f;
853 spawn->futureDamage = 0.0f;
854 spawn->generation++;
855 spawn->movePathCount = 0;
856 }
857
858 return spawn;
859 }
860
861 int EnemyAddDamage(Enemy *enemy, float damage)
862 {
863 enemy->damage += damage;
864 if (enemy->damage >= EnemyGetMaxHealth(enemy))
865 {
866 enemy->enemyType = ENEMY_TYPE_NONE;
867 return 1;
868 }
869
870 return 0;
871 }
872
873 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
874 {
875 int16_t castleX = 0;
876 int16_t castleY = 0;
877 Enemy* closest = 0;
878 int16_t closestDistance = 0;
879 float range2 = range * range;
880 for (int i = 0; i < enemyCount; i++)
881 {
882 Enemy* enemy = &enemies[i];
883 if (enemy->enemyType == ENEMY_TYPE_NONE)
884 {
885 continue;
886 }
887 float maxHealth = EnemyGetMaxHealth(enemy);
888 if (enemy->futureDamage >= maxHealth)
889 {
890 // ignore enemies that will die soon
891 continue;
892 }
893 int16_t dx = castleX - enemy->currentX;
894 int16_t dy = castleY - enemy->currentY;
895 int16_t distance = abs(dx) + abs(dy);
896 if (!closest || distance < closestDistance)
897 {
898 float tdx = towerX - enemy->currentX;
899 float tdy = towerY - enemy->currentY;
900 float tdistance2 = tdx * tdx + tdy * tdy;
901 if (tdistance2 <= range2)
902 {
903 closest = enemy;
904 closestDistance = distance;
905 }
906 }
907 }
908 return closest;
909 }
910
911 int EnemyCount()
912 {
913 int count = 0;
914 for (int i = 0; i < enemyCount; i++)
915 {
916 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
917 {
918 count++;
919 }
920 }
921 return count;
922 }
923
924 //# Projectiles
925 #define PROJECTILE_MAX_COUNT 1200
926 #define PROJECTILE_TYPE_NONE 0
927 #define PROJECTILE_TYPE_BULLET 1
928
929 typedef struct Projectile
930 {
931 uint8_t projectileType;
932 float shootTime;
933 float arrivalTime;
934 float damage;
935 Vector2 position;
936 Vector2 target;
937 Vector2 directionNormal;
938 EnemyId targetEnemy;
939 } Projectile;
940
941 Projectile projectiles[PROJECTILE_MAX_COUNT];
942 int projectileCount = 0;
943
944 void ProjectileInit()
945 {
946 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
947 {
948 projectiles[i] = (Projectile){0};
949 }
950 }
951
952 void ProjectileDraw()
953 {
954 for (int i = 0; i < projectileCount; i++)
955 {
956 Projectile projectile = projectiles[i];
957 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
958 {
959 continue;
960 }
961 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
962 if (transition >= 1.0f)
963 {
964 continue;
965 }
966 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
967 float x = position.x;
968 float y = position.y;
969 float dx = projectile.directionNormal.x;
970 float dy = projectile.directionNormal.y;
971 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
972 {
973 x -= dx * 0.1f;
974 y -= dy * 0.1f;
975 float size = 0.1f * d;
976 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
977 }
978 }
979 }
980
981 void ProjectileUpdate()
982 {
983 for (int i = 0; i < projectileCount; i++)
984 {
985 Projectile *projectile = &projectiles[i];
986 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
987 {
988 continue;
989 }
990 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
991 if (transition >= 1.0f)
992 {
993 projectile->projectileType = PROJECTILE_TYPE_NONE;
994 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
995 if (enemy)
996 {
997 EnemyAddDamage(enemy, projectile->damage);
998 }
999 continue;
1000 }
1001 }
1002 }
1003
1004 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
1005 {
1006 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
1007 {
1008 Projectile *projectile = &projectiles[i];
1009 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
1010 {
1011 projectile->projectileType = projectileType;
1012 projectile->shootTime = gameTime.time;
1013 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
1014 projectile->damage = damage;
1015 projectile->position = position;
1016 projectile->target = target;
1017 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
1018 projectile->targetEnemy = EnemyGetId(enemy);
1019 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
1020 return projectile;
1021 }
1022 }
1023 return 0;
1024 }
1025
1026 //# Towers
1027
1028 void TowerInit()
1029 {
1030 for (int i = 0; i < TOWER_MAX_COUNT; i++)
1031 {
1032 towers[i] = (Tower){0};
1033 }
1034 towerCount = 0;
1035 }
1036
1037 Tower *TowerGetAt(int16_t x, int16_t y)
1038 {
1039 for (int i = 0; i < towerCount; i++)
1040 {
1041 if (towers[i].x == x && towers[i].y == y)
1042 {
1043 return &towers[i];
1044 }
1045 }
1046 return 0;
1047 }
1048
1049 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
1050 {
1051 if (towerCount >= TOWER_MAX_COUNT)
1052 {
1053 return 0;
1054 }
1055
1056 Tower *tower = TowerGetAt(x, y);
1057 if (tower)
1058 {
1059 return 0;
1060 }
1061
1062 tower = &towers[towerCount++];
1063 tower->x = x;
1064 tower->y = y;
1065 tower->towerType = towerType;
1066 tower->cooldown = 0.0f;
1067 tower->damage = 0.0f;
1068 return tower;
1069 }
1070
1071 float TowerGetMaxHealth(Tower *tower)
1072 {
1073 switch (tower->towerType)
1074 {
1075 case TOWER_TYPE_BASE:
1076 return 10.0f;
1077 case TOWER_TYPE_GUN:
1078 return 3.0f;
1079 case TOWER_TYPE_WALL:
1080 return 5.0f;
1081 }
1082 return 0.0f;
1083 }
1084
1085 void TowerDraw()
1086 {
1087 for (int i = 0; i < towerCount; i++)
1088 {
1089 Tower tower = towers[i];
1090 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
1091 switch (tower.towerType)
1092 {
1093 case TOWER_TYPE_BASE:
1094 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
1095 break;
1096 case TOWER_TYPE_GUN:
1097 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
1098 break;
1099 case TOWER_TYPE_WALL:
1100 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
1101 break;
1102 }
1103 }
1104 }
1105
1106 void TowerGunUpdate(Tower *tower)
1107 {
1108 if (tower->cooldown <= 0)
1109 {
1110 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
1111 if (enemy)
1112 {
1113 tower->cooldown = 0.125f;
1114 // shoot the enemy; determine future position of the enemy
1115 float bulletSpeed = 1.0f;
1116 float bulletDamage = 3.0f;
1117 Vector2 velocity = enemy->simVelocity;
1118 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
1119 Vector2 towerPosition = {tower->x, tower->y};
1120 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
1121 for (int i = 0; i < 8; i++) {
1122 velocity = enemy->simVelocity;
1123 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
1124 float distance = Vector2Distance(towerPosition, futurePosition);
1125 float eta2 = distance / bulletSpeed;
1126 if (fabs(eta - eta2) < 0.01f) {
1127 break;
1128 }
1129 eta = (eta2 + eta) * 0.5f;
1130 }
1131 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
1132 bulletSpeed, bulletDamage);
1133 enemy->futureDamage += bulletDamage;
1134 }
1135 }
1136 else
1137 {
1138 tower->cooldown -= gameTime.deltaTime;
1139 }
1140 }
1141
1142 void TowerUpdate()
1143 {
1144 for (int i = 0; i < towerCount; i++)
1145 {
1146 Tower *tower = &towers[i];
1147 switch (tower->towerType)
1148 {
1149 case TOWER_TYPE_GUN:
1150 TowerGunUpdate(tower);
1151 break;
1152 }
1153 }
1154 }
1155
1156 //# Game
1157
1158 float nextSpawnTime = 0.0f;
1159
1160 void ResetGame()
1161 {
1162 TowerInit();
1163 EnemyInit();
1164 ProjectileInit();
1165 ParticleInit();
1166 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1167 }
1168
1169 void InitGame()
1170 {
1171 TowerInit();
1172 EnemyInit();
1173 ProjectileInit();
1174 ParticleInit();
1175 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1176
1177 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1178 TowerTryAdd(TOWER_TYPE_GUN, 2, 0);
1179
1180 for (int i = -2; i <= 2; i += 1)
1181 {
1182 TowerTryAdd(TOWER_TYPE_WALL, i, 2);
1183 TowerTryAdd(TOWER_TYPE_WALL, i, -2);
1184 TowerTryAdd(TOWER_TYPE_WALL, -2, i);
1185 }
1186
1187 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4);
1188 }
1189
1190 //# Immediate GUI functions
1191
1192 typedef struct ButtonState {
1193 char isSelected;
1194 } ButtonState;
1195
1196 typedef struct GUIState {
1197 int isBlocked;
1198 } GUIState;
1199
1200 GUIState guiState = {0};
1201
1202 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1203 {
1204 Rectangle bounds = {x, y, width, height};
1205 int isPressed = 0;
1206 int isSelected = state && state->isSelected;
1207 if (CheckCollisionPointRec(GetMousePosition(), bounds))
1208 {
1209 Color color = isSelected ? DARKGRAY : GRAY;
1210 DrawRectangle(x, y, width, height, color);
1211 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !guiState.isBlocked)
1212 {
1213 isPressed = 1;
1214 }
1215 guiState.isBlocked = 1;
1216 }
1217 else
1218 {
1219 Color color = isSelected ? WHITE : LIGHTGRAY;
1220 DrawRectangle(x, y, width, height, color);
1221 }
1222 Font font = GetFontDefault();
1223 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1224 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, BLACK);
1225 return isPressed;
1226 }
1227
1228 //# Main game loop
1229
1230 void GameUpdate()
1231 {
1232 float dt = GetFrameTime();
1233 // cap maximum delta time to 0.1 seconds to prevent large time steps
1234 if (dt > 0.1f) dt = 0.1f;
1235 gameTime.time += dt;
1236 gameTime.deltaTime = dt;
1237 PathFindingMapUpdate();
1238 EnemyUpdate();
1239 TowerUpdate();
1240 ProjectileUpdate();
1241 ParticleUpdate();
1242
1243 // spawn a new enemy every second
1244 if (gameTime.time >= nextSpawnTime && EnemyCount() < 50)
1245 {
1246 nextSpawnTime = gameTime.time + 0.2f;
1247 // add a new enemy at the boundary of the map
1248 int randValue = GetRandomValue(-5, 5);
1249 int randSide = GetRandomValue(0, 3);
1250 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue;
1251 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue;
1252 static int alternation = 0;
1253 alternation += 1;
1254 if (alternation % 3 == 0) {
1255 EnemyTryAdd(ENEMY_TYPE_MINION, 0, -5);
1256 }
1257 else if (alternation % 3 == 1)
1258 {
1259 EnemyTryAdd(ENEMY_TYPE_MINION, 0, 5);
1260 }
1261 EnemyTryAdd(ENEMY_TYPE_MINION, x, y);
1262 }
1263 }
1264
1265 int main(void)
1266 {
1267 int screenWidth, screenHeight;
1268 GetPreferredSize(&screenWidth, &screenHeight);
1269 InitWindow(screenWidth, screenHeight, "Tower defense");
1270 SetTargetFPS(30);
1271
1272 Camera3D camera = {0};
1273
1274 int cameraMode = 0;
1275
1276 InitGame();
1277
1278 while (!WindowShouldClose())
1279 {
1280 if (IsPaused()) {
1281 // canvas is not visible in browser - do nothing
1282 continue;
1283 }
1284
1285 if (cameraMode == 0)
1286 {
1287 camera.position = (Vector3){0.0f, 10.0f, -0.5f};
1288 camera.target = (Vector3){0.0f, 0.0f, -0.5f};
1289 camera.up = (Vector3){0.0f, 0.0f, -1.0f};
1290 camera.fovy = 12.0f;
1291 camera.projection = CAMERA_ORTHOGRAPHIC;
1292 }
1293 else
1294 {
1295 camera.position = (Vector3){1.0f, 12.0f, 6.5f};
1296 camera.target = (Vector3){0.0f, 0.5f, 1.0f};
1297 camera.up = (Vector3){0.0f, 1.0f, 0.0f};
1298 camera.fovy = 45.0f;
1299 camera.projection = CAMERA_PERSPECTIVE;
1300 }
1301
1302 BeginDrawing();
1303 ClearBackground(DARKBLUE);
1304
1305 BeginMode3D(camera);
1306 DrawGrid(10, 1.0f);
1307 TowerDraw();
1308 EnemyDraw();
1309 ProjectileDraw();
1310 // PathFindingMapDraw();
1311 ParticleDraw();
1312 GameUpdate();
1313
1314 static int placementMode = TOWER_TYPE_NONE;
1315
1316 Ray ray = GetScreenToWorldRay(GetMousePosition(), camera);
1317 float planeDistance = ray.position.y / -ray.direction.y;
1318 float planeX = ray.direction.x * planeDistance + ray.position.x;
1319 float planeY = ray.direction.z * planeDistance + ray.position.z;
1320 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
1321 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
1322 if (placementMode && !guiState.isBlocked)
1323 {
1324 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
1325 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1326 {
1327 TowerTryAdd(placementMode, mapX, mapY);
1328 placementMode = TOWER_TYPE_NONE;
1329 }
1330 }
1331
1332 guiState.isBlocked = 0;
1333
1334 EndMode3D();
1335
1336 const char *title = "Tower defense tutorial";
1337 int titleWidth = MeasureText(title, 20);
1338 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f + 2, 5 + 2, 20, BLACK);
1339 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f, 5, 20, WHITE);
1340
1341 const char *buttonText = cameraMode == 0 ? "2D" : "3D";
1342 if (Button(buttonText, 10, 10, 80, 30, 0))
1343 {
1344 cameraMode = !cameraMode;
1345 }
1346
1347 static ButtonState buildWallButtonState = {0};
1348 static ButtonState buildGunButtonState = {0};
1349 buildWallButtonState.isSelected = placementMode == TOWER_TYPE_WALL;
1350 buildGunButtonState.isSelected = placementMode == TOWER_TYPE_GUN;
1351
1352 if (Button("Wall", 10, 50, 80, 30, &buildWallButtonState))
1353 {
1354 placementMode = buildWallButtonState.isSelected ? 0 : TOWER_TYPE_WALL;
1355 }
1356 if (Button("Gun", 10, 90, 80, 30, &buildGunButtonState))
1357 {
1358 placementMode = buildGunButtonState.isSelected ? 0 : TOWER_TYPE_GUN;
1359 }
1360
1361 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1362 {
1363 ResetGame();
1364 }
1365
1366 EndDrawing();
1367 }
1368
1369 CloseWindow();
1370
1371 return 0;
1372 }
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
All right, that is already quite interesting to play around with. Placing buildings and seeing the enemies react to them is quite satisfying. But at this point, we have no way to win or lose the game. There are no rules, no resource and no goal.
Coming up with the rules and to implement them is the topic of the next part, as this is the end of this part!
Wrap up
We have a basic UI, can get the mouse position in the world, can place buildings, and the enemies react can destroy buildings. The C file has now nearly 1400 lines of code - but I hope that it is still manageable.
It is easier to prepare and show the differences between each version when having only one file, but eventually, it might make sense at some point to split the code parts that are less frequently changed into separate files so it's easier to focus on the currently changed part.
That said, I hope you enjoyed this part and to see you in the next one next week!