Simple tower defense tutorial, part 5
It's already the 5th part of this tutorial series! Let's see what we have:
We can place buildings, enemies spawn and seek their way to the castle, the towers shoot at the enemies, the walls block the enemies, the enemies attack the buildings and explode ... what we need now are the rules. Placing towers should cost resources and we should have a way to earn resources.
I never enjoy copying existing things too closely, so I want to modify the classic rules for building and earning resources a bit. The part I enjoy in tower defense games is watching the world work. What I don't like so much is the hectic pace of upgrading things left and right and clicking buttons frenetically to get it activated as soon as the money suffices.
What I would rather want to see is a kind of puzzle where I prepare the defense but have only limited ways to interact with it once the enemies are rushing in.
Since this is a tutorial, let's not overthing this and just start working this out!
Here's what the logic flow should work like:
- Load a level: A level defines the scenario we're playing in:
- Define enemy spawn points
- Provide limited amount of resources
- Declare the number of waves the player needs to survive
- Start the game
- Phase 1: The player can place buildings
- Phase 2: The player starts the next wave
- Phase 3: The wave was either defeated or the player lost; show a report
- Repeat until all waves are done
- Show final report
The first step is to reorganize the code around the game loop. In the current version, the game loop is tighlty integrated in the main function's loop. In order to prepare "thinking" in levels, we need to perform a few changes.
There are always different ways how to handle this. While I am trying to keep in this tutorial as simple as I can imagine, I want to prevent that I have to integrate future changes inside of the main loop or the function it is directly calling. I want to introduce an indirection; in an object oriented language, this would be a class. In C, we can use a struct and function pointers to achieve a similar effect.
The idea is to regard the game level as its own object with its current own state and update and rendering logic. These functions are still utilizing the existing functions for handling the towers, enemies, and so on, but we're moving the responsibility to call them to the level related functions. If we later want levels to handle things differently, we will adjust the level functions and not the main loop.
This simplifies the main loop where we can later integrate the other game states like the main menu, level selection, credits and so on. Let's first have a closer look on the general structure:
1 typedef enum LevelState
2 {
3 LEVEL_STATE_NONE,
4 LEVEL_STATE_BUILDING,
5 LEVEL_STATE_BATTLE,
6 LEVEL_STATE_WON_WAVE,
7 LEVEL_STATE_LOST_WAVE,
8 LEVEL_STATE_WON_LEVEL,
9 } LevelState;
10
11 typedef struct Level
12 {
13 LevelState state;
14 Camera3D camera;
15 int placementMode;
16 } Level;
17
18 Level levels[] = {
19 [0] = {
20 .state = LEVEL_STATE_BUILDING,
21 },
22 };
The enum defines all the states the level can be, though at the moment there's not really much we're doing with it. Another part of the restructuring is described best by highlighting the following function declarations:
1 void InitLevel(Level *level);
2 void DrawLevel(Level *level);
3 void UpdateLevel(Level *level);
Each function gets the level struct it is using as a parameter. Any value that the level functions need to store between calls will be stored there. If you are familiar with object oriented programming, you will see the resemblance to an object and its methods and the object variables it is using. Some of our enemy and tower functions follow a similar pattern.
Just going by the names, it should be pretty clear what each function is doing. What we will trust on is that InitLevel function is always called before the DrawLevel and UpdateLevel functions with the same level struct and that we don't use any other level struct data. Since the other program parts (path finding, towers, enemies, etc.) are not part of the level struct, the init function needs to initialize them as well before they are used for the purposes of the level. Since we don't intend to run multiple levels at the same time, this is not really a problem, but I wanted to mention it to clarify how our program works and which implicit constraints we have to accept.
So the next code block shows the state after the refactoring:
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 typedef struct ButtonState {
45 char isSelected;
46 } ButtonState;
47
48 typedef struct GUIState {
49 int isBlocked;
50 } GUIState;
51
52 GUIState guiState = {0};
53 GameTime gameTime = {0};
54 Tower towers[TOWER_MAX_COUNT];
55 int towerCount = 0;
56
57 float TowerGetMaxHealth(Tower *tower);
58 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
59
60 //# Particle system
61
62 void ParticleInit()
63 {
64 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
65 {
66 particles[i] = (Particle){0};
67 }
68 particleCount = 0;
69 }
70
71 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
72 {
73 if (particleCount >= PARTICLE_MAX_COUNT)
74 {
75 return;
76 }
77
78 int index = -1;
79 for (int i = 0; i < particleCount; i++)
80 {
81 if (particles[i].particleType == PARTICLE_TYPE_NONE)
82 {
83 index = i;
84 break;
85 }
86 }
87
88 if (index == -1)
89 {
90 index = particleCount++;
91 }
92
93 Particle *particle = &particles[index];
94 particle->particleType = particleType;
95 particle->spawnTime = gameTime.time;
96 particle->lifetime = lifetime;
97 particle->position = position;
98 particle->velocity = velocity;
99 }
100
101 void ParticleUpdate()
102 {
103 for (int i = 0; i < particleCount; i++)
104 {
105 Particle *particle = &particles[i];
106 if (particle->particleType == PARTICLE_TYPE_NONE)
107 {
108 continue;
109 }
110
111 float age = gameTime.time - particle->spawnTime;
112
113 if (particle->lifetime > age)
114 {
115 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
116 }
117 else {
118 particle->particleType = PARTICLE_TYPE_NONE;
119 }
120 }
121 }
122
123 void DrawExplosionParticle(Particle *particle, float transition)
124 {
125 float size = 1.2f * (1.0f - transition);
126 Color startColor = WHITE;
127 Color endColor = RED;
128 Color color = ColorLerp(startColor, endColor, transition);
129 DrawCube(particle->position, size, size, size, color);
130 }
131
132 void ParticleDraw()
133 {
134 for (int i = 0; i < particleCount; i++)
135 {
136 Particle particle = particles[i];
137 if (particle.particleType == PARTICLE_TYPE_NONE)
138 {
139 continue;
140 }
141
142 float age = gameTime.time - particle.spawnTime;
143 float transition = age / particle.lifetime;
144 switch (particle.particleType)
145 {
146 case PARTICLE_TYPE_EXPLOSION:
147 DrawExplosionParticle(&particle, transition);
148 break;
149 default:
150 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
151 break;
152 }
153 }
154 }
155
156 //# Pathfinding map
157 typedef struct DeltaSrc
158 {
159 char x, y;
160 } DeltaSrc;
161
162 typedef struct PathfindingMap
163 {
164 int width, height;
165 float scale;
166 float *distances;
167 long *towerIndex;
168 DeltaSrc *deltaSrc;
169 float maxDistance;
170 Matrix toMapSpace;
171 Matrix toWorldSpace;
172 } PathfindingMap;
173
174 // when we execute the pathfinding algorithm, we need to store the active nodes
175 // in a queue. Each node has a position, a distance from the start, and the
176 // position of the node that we came from.
177 typedef struct PathfindingNode
178 {
179 int16_t x, y, fromX, fromY;
180 float distance;
181 } PathfindingNode;
182
183 // The queue is a simple array of nodes, we add nodes to the end and remove
184 // nodes from the front. We keep the array around to avoid unnecessary allocations
185 static PathfindingNode *pathfindingNodeQueue = 0;
186 static int pathfindingNodeQueueCount = 0;
187 static int pathfindingNodeQueueCapacity = 0;
188
189 // The pathfinding map stores the distances from the castle to each cell in the map.
190 PathfindingMap pathfindingMap = {0};
191
192 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
193 {
194 // transforming between map space and world space allows us to adapt
195 // position and scale of the map without changing the pathfinding data
196 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
197 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
198 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
199 pathfindingMap.width = width;
200 pathfindingMap.height = height;
201 pathfindingMap.scale = scale;
202 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
203 for (int i = 0; i < width * height; i++)
204 {
205 pathfindingMap.distances[i] = -1.0f;
206 }
207
208 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
209 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
210 }
211
212 float PathFindingGetDistance(int mapX, int mapY)
213 {
214 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
215 {
216 // when outside the map, we return the manhattan distance to the castle (0,0)
217 return fabsf((float)mapX) + fabsf((float)mapY);
218 }
219
220 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
221 }
222
223 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
224 {
225 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
226 {
227 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
228 // we use MemAlloc/MemRealloc to allocate memory for the queue
229 // I am not entirely sure if MemRealloc allows passing a null pointer
230 // so we check if the pointer is null and use MemAlloc in that case
231 if (pathfindingNodeQueue == 0)
232 {
233 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
234 }
235 else
236 {
237 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
238 }
239 }
240
241 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
242 node->x = x;
243 node->y = y;
244 node->fromX = fromX;
245 node->fromY = fromY;
246 node->distance = distance;
247 }
248
249 PathfindingNode *PathFindingNodePop()
250 {
251 if (pathfindingNodeQueueCount == 0)
252 {
253 return 0;
254 }
255 // we return the first node in the queue; we want to return a pointer to the node
256 // so we can return 0 if the queue is empty.
257 // We should _not_ return a pointer to the element in the list, because the list
258 // may be reallocated and the pointer would become invalid. Or the
259 // popped element is overwritten by the next push operation.
260 // Using static here means that the variable is permanently allocated.
261 static PathfindingNode node;
262 node = pathfindingNodeQueue[0];
263 // we shift all nodes one position to the front
264 for (int i = 1; i < pathfindingNodeQueueCount; i++)
265 {
266 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
267 }
268 --pathfindingNodeQueueCount;
269 return &node;
270 }
271
272 // transform a world position to a map position in the array;
273 // returns true if the position is inside the map
274 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
275 {
276 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
277 *mapX = (int16_t)mapPosition.x;
278 *mapY = (int16_t)mapPosition.z;
279 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
280 }
281
282 void PathFindingMapUpdate()
283 {
284 const int castleX = 0, castleY = 0;
285 int16_t castleMapX, castleMapY;
286 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
287 {
288 return;
289 }
290 int width = pathfindingMap.width, height = pathfindingMap.height;
291
292 // reset the distances to -1
293 for (int i = 0; i < width * height; i++)
294 {
295 pathfindingMap.distances[i] = -1.0f;
296 }
297 // reset the tower indices
298 for (int i = 0; i < width * height; i++)
299 {
300 pathfindingMap.towerIndex[i] = -1;
301 }
302 // reset the delta src
303 for (int i = 0; i < width * height; i++)
304 {
305 pathfindingMap.deltaSrc[i].x = 0;
306 pathfindingMap.deltaSrc[i].y = 0;
307 }
308
309 for (int i = 0; i < towerCount; i++)
310 {
311 Tower *tower = &towers[i];
312 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
313 {
314 continue;
315 }
316 int16_t mapX, mapY;
317 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
318 // this would not work correctly and needs to be refined to allow towers covering multiple cells
319 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
320 // one cell. For now.
321 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
322 {
323 continue;
324 }
325 int index = mapY * width + mapX;
326 pathfindingMap.towerIndex[index] = i;
327 }
328
329 // we start at the castle and add the castle to the queue
330 pathfindingMap.maxDistance = 0.0f;
331 pathfindingNodeQueueCount = 0;
332 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
333 PathfindingNode *node = 0;
334 while ((node = PathFindingNodePop()))
335 {
336 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
337 {
338 continue;
339 }
340 int index = node->y * width + node->x;
341 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
342 {
343 continue;
344 }
345
346 int deltaX = node->x - node->fromX;
347 int deltaY = node->y - node->fromY;
348 // even if the cell is blocked by a tower, we still may want to store the direction
349 // (though this might not be needed, IDK right now)
350 pathfindingMap.deltaSrc[index].x = (char) deltaX;
351 pathfindingMap.deltaSrc[index].y = (char) deltaY;
352
353 // we skip nodes that are blocked by towers
354 if (pathfindingMap.towerIndex[index] >= 0)
355 {
356 node->distance += 8.0f;
357 }
358 pathfindingMap.distances[index] = node->distance;
359 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
360 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
361 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
362 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
363 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
364 }
365 }
366
367 void PathFindingMapDraw()
368 {
369 float cellSize = pathfindingMap.scale * 0.9f;
370 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
371 for (int x = 0; x < pathfindingMap.width; x++)
372 {
373 for (int y = 0; y < pathfindingMap.height; y++)
374 {
375 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
376 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
377 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
378 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
379 // animate the distance "wave" to show how the pathfinding algorithm expands
380 // from the castle
381 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
382 {
383 color = BLACK;
384 }
385 DrawCube(position, cellSize, 0.1f, cellSize, color);
386 }
387 }
388 }
389
390 Vector2 PathFindingGetGradient(Vector3 world)
391 {
392 int16_t mapX, mapY;
393 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
394 {
395 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
396 return (Vector2){(float)-delta.x, (float)-delta.y};
397 }
398 // fallback to a simple gradient calculation
399 float n = PathFindingGetDistance(mapX, mapY - 1);
400 float s = PathFindingGetDistance(mapX, mapY + 1);
401 float w = PathFindingGetDistance(mapX - 1, mapY);
402 float e = PathFindingGetDistance(mapX + 1, mapY);
403 return (Vector2){w - e + 0.25f, n - s + 0.125f};
404 }
405
406 //# Enemies
407
408 #define ENEMY_MAX_PATH_COUNT 8
409 #define ENEMY_MAX_COUNT 400
410 #define ENEMY_TYPE_NONE 0
411 #define ENEMY_TYPE_MINION 1
412
413 typedef struct EnemyId
414 {
415 uint16_t index;
416 uint16_t generation;
417 } EnemyId;
418
419 typedef struct EnemyClassConfig
420 {
421 float speed;
422 float health;
423 float radius;
424 float maxAcceleration;
425 float requiredContactTime;
426 float explosionDamage;
427 float explosionRange;
428 float explosionPushbackPower;
429 } EnemyClassConfig;
430
431 typedef struct Enemy
432 {
433 int16_t currentX, currentY;
434 int16_t nextX, nextY;
435 Vector2 simPosition;
436 Vector2 simVelocity;
437 uint16_t generation;
438 float startMovingTime;
439 float damage, futureDamage;
440 float contactTime;
441 uint8_t enemyType;
442 uint8_t movePathCount;
443 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
444 } Enemy;
445
446 Enemy enemies[ENEMY_MAX_COUNT];
447 int enemyCount = 0;
448
449 EnemyClassConfig enemyClassConfigs[] = {
450 [ENEMY_TYPE_MINION] = {
451 .health = 3.0f,
452 .speed = 1.0f,
453 .radius = 0.25f,
454 .maxAcceleration = 1.0f,
455 .explosionDamage = 1.0f,
456 .requiredContactTime = 0.5f,
457 .explosionRange = 1.0f,
458 .explosionPushbackPower = 0.25f,
459 },
460 };
461
462 int EnemyAddDamage(Enemy *enemy, float damage);
463
464 void EnemyInit()
465 {
466 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
467 {
468 enemies[i] = (Enemy){0};
469 }
470 enemyCount = 0;
471 }
472
473 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
474 {
475 return enemyClassConfigs[enemy->enemyType].speed;
476 }
477
478 float EnemyGetMaxHealth(Enemy *enemy)
479 {
480 return enemyClassConfigs[enemy->enemyType].health;
481 }
482
483 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
484 {
485 int16_t castleX = 0;
486 int16_t castleY = 0;
487 int16_t dx = castleX - currentX;
488 int16_t dy = castleY - currentY;
489 if (dx == 0 && dy == 0)
490 {
491 *nextX = currentX;
492 *nextY = currentY;
493 return 1;
494 }
495 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
496
497 if (gradient.x == 0 && gradient.y == 0)
498 {
499 *nextX = currentX;
500 *nextY = currentY;
501 return 1;
502 }
503
504 if (fabsf(gradient.x) > fabsf(gradient.y))
505 {
506 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
507 *nextY = currentY;
508 return 0;
509 }
510 *nextX = currentX;
511 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
512 return 0;
513 }
514
515
516 // this function predicts the movement of the unit for the next deltaT seconds
517 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
518 {
519 const float pointReachedDistance = 0.25f;
520 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
521 const float maxSimStepTime = 0.015625f;
522
523 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
524 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
525 int16_t nextX = enemy->nextX;
526 int16_t nextY = enemy->nextY;
527 Vector2 position = enemy->simPosition;
528 int passedCount = 0;
529 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
530 {
531 float stepTime = fminf(deltaT - t, maxSimStepTime);
532 Vector2 target = (Vector2){nextX, nextY};
533 float speed = Vector2Length(*velocity);
534 // draw the target position for debugging
535 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
536 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
537 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
538 {
539 // we reached the target position, let's move to the next waypoint
540 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
541 target = (Vector2){nextX, nextY};
542 // track how many waypoints we passed
543 passedCount++;
544 }
545
546 // acceleration towards the target
547 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
548 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
549 *velocity = Vector2Add(*velocity, acceleration);
550
551 // limit the speed to the maximum speed
552 if (speed > maxSpeed)
553 {
554 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
555 }
556
557 // move the enemy
558 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
559 }
560
561 if (waypointPassedCount)
562 {
563 (*waypointPassedCount) = passedCount;
564 }
565
566 return position;
567 }
568
569 void EnemyDraw()
570 {
571 for (int i = 0; i < enemyCount; i++)
572 {
573 Enemy enemy = enemies[i];
574 if (enemy.enemyType == ENEMY_TYPE_NONE)
575 {
576 continue;
577 }
578
579 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
580
581 if (enemy.movePathCount > 0)
582 {
583 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
584 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
585 }
586 for (int j = 1; j < enemy.movePathCount; j++)
587 {
588 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
589 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
590 DrawLine3D(p, q, GREEN);
591 }
592
593 switch (enemy.enemyType)
594 {
595 case ENEMY_TYPE_MINION:
596 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
597 break;
598 }
599 }
600 }
601
602 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
603 {
604 // damage the tower
605 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
606 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
607 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
608 float explosionRange2 = explosionRange * explosionRange;
609 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
610 // explode the enemy
611 if (tower->damage >= TowerGetMaxHealth(tower))
612 {
613 tower->towerType = TOWER_TYPE_NONE;
614 }
615
616 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
617 explosionSource,
618 (Vector3){0, 0.1f, 0}, 1.0f);
619
620 enemy->enemyType = ENEMY_TYPE_NONE;
621
622 // push back enemies & dealing damage
623 for (int i = 0; i < enemyCount; i++)
624 {
625 Enemy *other = &enemies[i];
626 if (other->enemyType == ENEMY_TYPE_NONE)
627 {
628 continue;
629 }
630 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
631 if (distanceSqr > 0 && distanceSqr < explosionRange2)
632 {
633 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
634 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
635 EnemyAddDamage(other, explosionDamge);
636 }
637 }
638 }
639
640 void EnemyUpdate()
641 {
642 const float castleX = 0;
643 const float castleY = 0;
644 const float maxPathDistance2 = 0.25f * 0.25f;
645
646 for (int i = 0; i < enemyCount; i++)
647 {
648 Enemy *enemy = &enemies[i];
649 if (enemy->enemyType == ENEMY_TYPE_NONE)
650 {
651 continue;
652 }
653
654 int waypointPassedCount = 0;
655 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
656 enemy->startMovingTime = gameTime.time;
657 // track path of unit
658 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
659 {
660 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
661 {
662 enemy->movePath[j] = enemy->movePath[j - 1];
663 }
664 enemy->movePath[0] = enemy->simPosition;
665 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
666 {
667 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
668 }
669 }
670
671 if (waypointPassedCount > 0)
672 {
673 enemy->currentX = enemy->nextX;
674 enemy->currentY = enemy->nextY;
675 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
676 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
677 {
678 // enemy reached the castle; remove it
679 enemy->enemyType = ENEMY_TYPE_NONE;
680 continue;
681 }
682 }
683 }
684
685 // handle collisions between enemies
686 for (int i = 0; i < enemyCount - 1; i++)
687 {
688 Enemy *enemyA = &enemies[i];
689 if (enemyA->enemyType == ENEMY_TYPE_NONE)
690 {
691 continue;
692 }
693 for (int j = i + 1; j < enemyCount; j++)
694 {
695 Enemy *enemyB = &enemies[j];
696 if (enemyB->enemyType == ENEMY_TYPE_NONE)
697 {
698 continue;
699 }
700 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
701 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
702 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
703 float radiusSum = radiusA + radiusB;
704 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
705 {
706 // collision
707 float distance = sqrtf(distanceSqr);
708 float overlap = radiusSum - distance;
709 // move the enemies apart, but softly; if we have a clog of enemies,
710 // moving them perfectly apart can cause them to jitter
711 float positionCorrection = overlap / 5.0f;
712 Vector2 direction = (Vector2){
713 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
714 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
715 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
716 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
717 }
718 }
719 }
720
721 // handle collisions between enemies and towers
722 for (int i = 0; i < enemyCount; i++)
723 {
724 Enemy *enemy = &enemies[i];
725 if (enemy->enemyType == ENEMY_TYPE_NONE)
726 {
727 continue;
728 }
729 enemy->contactTime -= gameTime.deltaTime;
730 if (enemy->contactTime < 0.0f)
731 {
732 enemy->contactTime = 0.0f;
733 }
734
735 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
736 // linear search over towers; could be optimized by using path finding tower map,
737 // but for now, we keep it simple
738 for (int j = 0; j < towerCount; j++)
739 {
740 Tower *tower = &towers[j];
741 if (tower->towerType == TOWER_TYPE_NONE)
742 {
743 continue;
744 }
745 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
746 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
747 if (distanceSqr > combinedRadius * combinedRadius)
748 {
749 continue;
750 }
751 // potential collision; square / circle intersection
752 float dx = tower->x - enemy->simPosition.x;
753 float dy = tower->y - enemy->simPosition.y;
754 float absDx = fabsf(dx);
755 float absDy = fabsf(dy);
756 Vector3 contactPoint = {0};
757 if (absDx <= 0.5f && absDx <= absDy) {
758 // vertical collision; push the enemy out horizontally
759 float overlap = enemyRadius + 0.5f - absDy;
760 if (overlap < 0.0f)
761 {
762 continue;
763 }
764 float direction = dy > 0.0f ? -1.0f : 1.0f;
765 enemy->simPosition.y += direction * overlap;
766 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
767 }
768 else if (absDy <= 0.5f && absDy <= absDx)
769 {
770 // horizontal collision; push the enemy out vertically
771 float overlap = enemyRadius + 0.5f - absDx;
772 if (overlap < 0.0f)
773 {
774 continue;
775 }
776 float direction = dx > 0.0f ? -1.0f : 1.0f;
777 enemy->simPosition.x += direction * overlap;
778 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
779 }
780 else
781 {
782 // possible collision with a corner
783 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
784 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
785 float cornerX = tower->x + cornerDX;
786 float cornerY = tower->y + cornerDY;
787 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
788 if (cornerDistanceSqr > enemyRadius * enemyRadius)
789 {
790 continue;
791 }
792 // push the enemy out along the diagonal
793 float cornerDistance = sqrtf(cornerDistanceSqr);
794 float overlap = enemyRadius - cornerDistance;
795 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
796 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
797 enemy->simPosition.x -= directionX * overlap;
798 enemy->simPosition.y -= directionY * overlap;
799 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
800 }
801
802 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
803 {
804 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
805 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
806 {
807 EnemyTriggerExplode(enemy, tower, contactPoint);
808 }
809 }
810 }
811 }
812 }
813
814 EnemyId EnemyGetId(Enemy *enemy)
815 {
816 return (EnemyId){enemy - enemies, enemy->generation};
817 }
818
819 Enemy *EnemyTryResolve(EnemyId enemyId)
820 {
821 if (enemyId.index >= ENEMY_MAX_COUNT)
822 {
823 return 0;
824 }
825 Enemy *enemy = &enemies[enemyId.index];
826 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
827 {
828 return 0;
829 }
830 return enemy;
831 }
832
833 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
834 {
835 Enemy *spawn = 0;
836 for (int i = 0; i < enemyCount; i++)
837 {
838 Enemy *enemy = &enemies[i];
839 if (enemy->enemyType == ENEMY_TYPE_NONE)
840 {
841 spawn = enemy;
842 break;
843 }
844 }
845
846 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
847 {
848 spawn = &enemies[enemyCount++];
849 }
850
851 if (spawn)
852 {
853 spawn->currentX = currentX;
854 spawn->currentY = currentY;
855 spawn->nextX = currentX;
856 spawn->nextY = currentY;
857 spawn->simPosition = (Vector2){currentX, currentY};
858 spawn->simVelocity = (Vector2){0, 0};
859 spawn->enemyType = enemyType;
860 spawn->startMovingTime = gameTime.time;
861 spawn->damage = 0.0f;
862 spawn->futureDamage = 0.0f;
863 spawn->generation++;
864 spawn->movePathCount = 0;
865 }
866
867 return spawn;
868 }
869
870 int EnemyAddDamage(Enemy *enemy, float damage)
871 {
872 enemy->damage += damage;
873 if (enemy->damage >= EnemyGetMaxHealth(enemy))
874 {
875 enemy->enemyType = ENEMY_TYPE_NONE;
876 return 1;
877 }
878
879 return 0;
880 }
881
882 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
883 {
884 int16_t castleX = 0;
885 int16_t castleY = 0;
886 Enemy* closest = 0;
887 int16_t closestDistance = 0;
888 float range2 = range * range;
889 for (int i = 0; i < enemyCount; i++)
890 {
891 Enemy* enemy = &enemies[i];
892 if (enemy->enemyType == ENEMY_TYPE_NONE)
893 {
894 continue;
895 }
896 float maxHealth = EnemyGetMaxHealth(enemy);
897 if (enemy->futureDamage >= maxHealth)
898 {
899 // ignore enemies that will die soon
900 continue;
901 }
902 int16_t dx = castleX - enemy->currentX;
903 int16_t dy = castleY - enemy->currentY;
904 int16_t distance = abs(dx) + abs(dy);
905 if (!closest || distance < closestDistance)
906 {
907 float tdx = towerX - enemy->currentX;
908 float tdy = towerY - enemy->currentY;
909 float tdistance2 = tdx * tdx + tdy * tdy;
910 if (tdistance2 <= range2)
911 {
912 closest = enemy;
913 closestDistance = distance;
914 }
915 }
916 }
917 return closest;
918 }
919
920 int EnemyCount()
921 {
922 int count = 0;
923 for (int i = 0; i < enemyCount; i++)
924 {
925 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
926 {
927 count++;
928 }
929 }
930 return count;
931 }
932
933 //# Projectiles
934 #define PROJECTILE_MAX_COUNT 1200
935 #define PROJECTILE_TYPE_NONE 0
936 #define PROJECTILE_TYPE_BULLET 1
937
938 typedef struct Projectile
939 {
940 uint8_t projectileType;
941 float shootTime;
942 float arrivalTime;
943 float damage;
944 Vector2 position;
945 Vector2 target;
946 Vector2 directionNormal;
947 EnemyId targetEnemy;
948 } Projectile;
949
950 Projectile projectiles[PROJECTILE_MAX_COUNT];
951 int projectileCount = 0;
952
953 void ProjectileInit()
954 {
955 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
956 {
957 projectiles[i] = (Projectile){0};
958 }
959 }
960
961 void ProjectileDraw()
962 {
963 for (int i = 0; i < projectileCount; i++)
964 {
965 Projectile projectile = projectiles[i];
966 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
967 {
968 continue;
969 }
970 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
971 if (transition >= 1.0f)
972 {
973 continue;
974 }
975 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
976 float x = position.x;
977 float y = position.y;
978 float dx = projectile.directionNormal.x;
979 float dy = projectile.directionNormal.y;
980 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
981 {
982 x -= dx * 0.1f;
983 y -= dy * 0.1f;
984 float size = 0.1f * d;
985 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
986 }
987 }
988 }
989
990 void ProjectileUpdate()
991 {
992 for (int i = 0; i < projectileCount; i++)
993 {
994 Projectile *projectile = &projectiles[i];
995 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
996 {
997 continue;
998 }
999 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
1000 if (transition >= 1.0f)
1001 {
1002 projectile->projectileType = PROJECTILE_TYPE_NONE;
1003 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
1004 if (enemy)
1005 {
1006 EnemyAddDamage(enemy, projectile->damage);
1007 }
1008 continue;
1009 }
1010 }
1011 }
1012
1013 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
1014 {
1015 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
1016 {
1017 Projectile *projectile = &projectiles[i];
1018 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
1019 {
1020 projectile->projectileType = projectileType;
1021 projectile->shootTime = gameTime.time;
1022 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
1023 projectile->damage = damage;
1024 projectile->position = position;
1025 projectile->target = target;
1026 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
1027 projectile->targetEnemy = EnemyGetId(enemy);
1028 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
1029 return projectile;
1030 }
1031 }
1032 return 0;
1033 }
1034
1035 //# Towers
1036
1037 void TowerInit()
1038 {
1039 for (int i = 0; i < TOWER_MAX_COUNT; i++)
1040 {
1041 towers[i] = (Tower){0};
1042 }
1043 towerCount = 0;
1044 }
1045
1046 Tower *TowerGetAt(int16_t x, int16_t y)
1047 {
1048 for (int i = 0; i < towerCount; i++)
1049 {
1050 if (towers[i].x == x && towers[i].y == y)
1051 {
1052 return &towers[i];
1053 }
1054 }
1055 return 0;
1056 }
1057
1058 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
1059 {
1060 if (towerCount >= TOWER_MAX_COUNT)
1061 {
1062 return 0;
1063 }
1064
1065 Tower *tower = TowerGetAt(x, y);
1066 if (tower)
1067 {
1068 return 0;
1069 }
1070
1071 tower = &towers[towerCount++];
1072 tower->x = x;
1073 tower->y = y;
1074 tower->towerType = towerType;
1075 tower->cooldown = 0.0f;
1076 tower->damage = 0.0f;
1077 return tower;
1078 }
1079
1080 float TowerGetMaxHealth(Tower *tower)
1081 {
1082 switch (tower->towerType)
1083 {
1084 case TOWER_TYPE_BASE:
1085 return 10.0f;
1086 case TOWER_TYPE_GUN:
1087 return 3.0f;
1088 case TOWER_TYPE_WALL:
1089 return 5.0f;
1090 }
1091 return 0.0f;
1092 }
1093
1094 void TowerDraw()
1095 {
1096 for (int i = 0; i < towerCount; i++)
1097 {
1098 Tower tower = towers[i];
1099 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
1100 switch (tower.towerType)
1101 {
1102 case TOWER_TYPE_BASE:
1103 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
1104 break;
1105 case TOWER_TYPE_GUN:
1106 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
1107 break;
1108 case TOWER_TYPE_WALL:
1109 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
1110 break;
1111 }
1112 }
1113 }
1114
1115 void TowerGunUpdate(Tower *tower)
1116 {
1117 if (tower->cooldown <= 0)
1118 {
1119 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
1120 if (enemy)
1121 {
1122 tower->cooldown = 0.125f;
1123 // shoot the enemy; determine future position of the enemy
1124 float bulletSpeed = 1.0f;
1125 float bulletDamage = 3.0f;
1126 Vector2 velocity = enemy->simVelocity;
1127 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
1128 Vector2 towerPosition = {tower->x, tower->y};
1129 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
1130 for (int i = 0; i < 8; i++) {
1131 velocity = enemy->simVelocity;
1132 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
1133 float distance = Vector2Distance(towerPosition, futurePosition);
1134 float eta2 = distance / bulletSpeed;
1135 if (fabs(eta - eta2) < 0.01f) {
1136 break;
1137 }
1138 eta = (eta2 + eta) * 0.5f;
1139 }
1140 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
1141 bulletSpeed, bulletDamage);
1142 enemy->futureDamage += bulletDamage;
1143 }
1144 }
1145 else
1146 {
1147 tower->cooldown -= gameTime.deltaTime;
1148 }
1149 }
1150
1151 void TowerUpdate()
1152 {
1153 for (int i = 0; i < towerCount; i++)
1154 {
1155 Tower *tower = &towers[i];
1156 switch (tower->towerType)
1157 {
1158 case TOWER_TYPE_GUN:
1159 TowerGunUpdate(tower);
1160 break;
1161 }
1162 }
1163 }
1164
1165 //# Game
1166
1167 typedef enum LevelState
1168 {
1169 LEVEL_STATE_NONE,
1170 LEVEL_STATE_BUILDING,
1171 LEVEL_STATE_BATTLE,
1172 LEVEL_STATE_WON_WAVE,
1173 LEVEL_STATE_LOST_WAVE,
1174 LEVEL_STATE_WON_LEVEL,
1175 } LevelState;
1176
1177 typedef struct Level
1178 {
1179 LevelState state;
1180 Camera3D camera;
1181 int placementMode;
1182 } Level;
1183
1184 void InitLevel(Level *level)
1185 {
1186 TowerInit();
1187 EnemyInit();
1188 ProjectileInit();
1189 ParticleInit();
1190 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1191
1192 level->placementMode = 0;
1193
1194 Camera *camera = &level->camera;
1195 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
1196 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
1197 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
1198 camera->fovy = 45.0f;
1199 camera->projection = CAMERA_PERSPECTIVE;
1200 }
1201
1202 void DrawLevel(Level *level)
1203 {
1204 BeginMode3D(level->camera);
1205 DrawGrid(10, 1.0f);
1206 TowerDraw();
1207 EnemyDraw();
1208 ProjectileDraw();
1209 // PathFindingMapDraw();
1210 ParticleDraw();
1211
1212 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
1213 float planeDistance = ray.position.y / -ray.direction.y;
1214 float planeX = ray.direction.x * planeDistance + ray.position.x;
1215 float planeY = ray.direction.z * planeDistance + ray.position.z;
1216 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
1217 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
1218 if (level->placementMode && !guiState.isBlocked)
1219 {
1220 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
1221 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1222 {
1223 TowerTryAdd(level->placementMode, mapX, mapY);
1224 level->placementMode = TOWER_TYPE_NONE;
1225 }
1226 }
1227
1228 guiState.isBlocked = 0;
1229
1230 EndMode3D();
1231
1232 const char *title = "Tower defense tutorial";
1233 int titleWidth = MeasureText(title, 20);
1234 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f + 2, 5 + 2, 20, BLACK);
1235 DrawText(title, (GetScreenWidth() - titleWidth) * 0.5f, 5, 20, WHITE);
1236
1237 static ButtonState buildWallButtonState = {0};
1238 static ButtonState buildGunButtonState = {0};
1239 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
1240 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
1241
1242 if (Button("Wall", 10, 50, 80, 30, &buildWallButtonState))
1243 {
1244 level->placementMode = buildWallButtonState.isSelected ? 0 : TOWER_TYPE_WALL;
1245 }
1246 if (Button("Gun", 10, 90, 80, 30, &buildGunButtonState))
1247 {
1248 level->placementMode = buildGunButtonState.isSelected ? 0 : TOWER_TYPE_GUN;
1249 }
1250
1251 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1252 {
1253 InitLevel(level);
1254 }
1255 }
1256
1257 void UpdateLevel(Level *level)
1258 {
1259 PathFindingMapUpdate();
1260 EnemyUpdate();
1261 TowerUpdate();
1262 ProjectileUpdate();
1263 ParticleUpdate();
1264 }
1265
1266 Level levels[] = {
1267 [0] = {
1268 .state = LEVEL_STATE_BUILDING,
1269 },
1270 };
1271
1272 Level *currentLevel = levels;
1273
1274 float nextSpawnTime = 0.0f;
1275
1276 void ResetGame()
1277 {
1278 InitLevel(currentLevel);
1279 }
1280
1281 void InitGame()
1282 {
1283 TowerInit();
1284 EnemyInit();
1285 ProjectileInit();
1286 ParticleInit();
1287 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1288
1289 currentLevel = levels;
1290 InitLevel(currentLevel);
1291 }
1292
1293 //# Immediate GUI functions
1294
1295 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1296 {
1297 Rectangle bounds = {x, y, width, height};
1298 int isPressed = 0;
1299 int isSelected = state && state->isSelected;
1300 if (CheckCollisionPointRec(GetMousePosition(), bounds))
1301 {
1302 Color color = isSelected ? DARKGRAY : GRAY;
1303 DrawRectangle(x, y, width, height, color);
1304 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !guiState.isBlocked)
1305 {
1306 isPressed = 1;
1307 }
1308 guiState.isBlocked = 1;
1309 }
1310 else
1311 {
1312 Color color = isSelected ? WHITE : LIGHTGRAY;
1313 DrawRectangle(x, y, width, height, color);
1314 }
1315 Font font = GetFontDefault();
1316 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1317 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, BLACK);
1318 return isPressed;
1319 }
1320
1321 //# Main game loop
1322
1323 void GameUpdate()
1324 {
1325 float dt = GetFrameTime();
1326 // cap maximum delta time to 0.1 seconds to prevent large time steps
1327 if (dt > 0.1f) dt = 0.1f;
1328 gameTime.time += dt;
1329 gameTime.deltaTime = dt;
1330
1331 UpdateLevel(currentLevel);
1332 }
1333
1334 int main(void)
1335 {
1336 int screenWidth, screenHeight;
1337 GetPreferredSize(&screenWidth, &screenHeight);
1338 InitWindow(screenWidth, screenHeight, "Tower defense");
1339 SetTargetFPS(30);
1340
1341 InitGame();
1342
1343 while (!WindowShouldClose())
1344 {
1345 if (IsPaused()) {
1346 // canvas is not visible in browser - do nothing
1347 continue;
1348 }
1349
1350 BeginDrawing();
1351 ClearBackground(DARKBLUE);
1352
1353 GameUpdate();
1354 DrawLevel(currentLevel);
1355
1356 EndDrawing();
1357 }
1358
1359 CloseWindow();
1360
1361 return 0;
1362 }
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
Functionalwise not much has changed; the camera toggle is gone, but the building placement still works, same as resetting the game. Now it's time to implement the phases we described above. The next changes don't cover the entire plan yet - but let's have a look how to implement the battle phase:
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 typedef struct ButtonState {
45 char isSelected;
46 } ButtonState;
47
48 typedef struct GUIState {
49 int isBlocked;
50 } GUIState;
51
52 GUIState guiState = {0};
53 GameTime gameTime = {0};
54 Tower towers[TOWER_MAX_COUNT];
55 int towerCount = 0;
56
57 float TowerGetMaxHealth(Tower *tower);
58 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
59
60 //# Particle system
61
62 void ParticleInit()
63 {
64 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
65 {
66 particles[i] = (Particle){0};
67 }
68 particleCount = 0;
69 }
70
71 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
72 {
73 if (particleCount >= PARTICLE_MAX_COUNT)
74 {
75 return;
76 }
77
78 int index = -1;
79 for (int i = 0; i < particleCount; i++)
80 {
81 if (particles[i].particleType == PARTICLE_TYPE_NONE)
82 {
83 index = i;
84 break;
85 }
86 }
87
88 if (index == -1)
89 {
90 index = particleCount++;
91 }
92
93 Particle *particle = &particles[index];
94 particle->particleType = particleType;
95 particle->spawnTime = gameTime.time;
96 particle->lifetime = lifetime;
97 particle->position = position;
98 particle->velocity = velocity;
99 }
100
101 void ParticleUpdate()
102 {
103 for (int i = 0; i < particleCount; i++)
104 {
105 Particle *particle = &particles[i];
106 if (particle->particleType == PARTICLE_TYPE_NONE)
107 {
108 continue;
109 }
110
111 float age = gameTime.time - particle->spawnTime;
112
113 if (particle->lifetime > age)
114 {
115 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
116 }
117 else {
118 particle->particleType = PARTICLE_TYPE_NONE;
119 }
120 }
121 }
122
123 void DrawExplosionParticle(Particle *particle, float transition)
124 {
125 float size = 1.2f * (1.0f - transition);
126 Color startColor = WHITE;
127 Color endColor = RED;
128 Color color = ColorLerp(startColor, endColor, transition);
129 DrawCube(particle->position, size, size, size, color);
130 }
131
132 void ParticleDraw()
133 {
134 for (int i = 0; i < particleCount; i++)
135 {
136 Particle particle = particles[i];
137 if (particle.particleType == PARTICLE_TYPE_NONE)
138 {
139 continue;
140 }
141
142 float age = gameTime.time - particle.spawnTime;
143 float transition = age / particle.lifetime;
144 switch (particle.particleType)
145 {
146 case PARTICLE_TYPE_EXPLOSION:
147 DrawExplosionParticle(&particle, transition);
148 break;
149 default:
150 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
151 break;
152 }
153 }
154 }
155
156 //# Pathfinding map
157 typedef struct DeltaSrc
158 {
159 char x, y;
160 } DeltaSrc;
161
162 typedef struct PathfindingMap
163 {
164 int width, height;
165 float scale;
166 float *distances;
167 long *towerIndex;
168 DeltaSrc *deltaSrc;
169 float maxDistance;
170 Matrix toMapSpace;
171 Matrix toWorldSpace;
172 } PathfindingMap;
173
174 // when we execute the pathfinding algorithm, we need to store the active nodes
175 // in a queue. Each node has a position, a distance from the start, and the
176 // position of the node that we came from.
177 typedef struct PathfindingNode
178 {
179 int16_t x, y, fromX, fromY;
180 float distance;
181 } PathfindingNode;
182
183 // The queue is a simple array of nodes, we add nodes to the end and remove
184 // nodes from the front. We keep the array around to avoid unnecessary allocations
185 static PathfindingNode *pathfindingNodeQueue = 0;
186 static int pathfindingNodeQueueCount = 0;
187 static int pathfindingNodeQueueCapacity = 0;
188
189 // The pathfinding map stores the distances from the castle to each cell in the map.
190 PathfindingMap pathfindingMap = {0};
191
192 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
193 {
194 // transforming between map space and world space allows us to adapt
195 // position and scale of the map without changing the pathfinding data
196 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
197 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
198 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
199 pathfindingMap.width = width;
200 pathfindingMap.height = height;
201 pathfindingMap.scale = scale;
202 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
203 for (int i = 0; i < width * height; i++)
204 {
205 pathfindingMap.distances[i] = -1.0f;
206 }
207
208 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
209 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
210 }
211
212 float PathFindingGetDistance(int mapX, int mapY)
213 {
214 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
215 {
216 // when outside the map, we return the manhattan distance to the castle (0,0)
217 return fabsf((float)mapX) + fabsf((float)mapY);
218 }
219
220 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
221 }
222
223 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
224 {
225 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
226 {
227 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
228 // we use MemAlloc/MemRealloc to allocate memory for the queue
229 // I am not entirely sure if MemRealloc allows passing a null pointer
230 // so we check if the pointer is null and use MemAlloc in that case
231 if (pathfindingNodeQueue == 0)
232 {
233 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
234 }
235 else
236 {
237 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
238 }
239 }
240
241 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
242 node->x = x;
243 node->y = y;
244 node->fromX = fromX;
245 node->fromY = fromY;
246 node->distance = distance;
247 }
248
249 PathfindingNode *PathFindingNodePop()
250 {
251 if (pathfindingNodeQueueCount == 0)
252 {
253 return 0;
254 }
255 // we return the first node in the queue; we want to return a pointer to the node
256 // so we can return 0 if the queue is empty.
257 // We should _not_ return a pointer to the element in the list, because the list
258 // may be reallocated and the pointer would become invalid. Or the
259 // popped element is overwritten by the next push operation.
260 // Using static here means that the variable is permanently allocated.
261 static PathfindingNode node;
262 node = pathfindingNodeQueue[0];
263 // we shift all nodes one position to the front
264 for (int i = 1; i < pathfindingNodeQueueCount; i++)
265 {
266 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
267 }
268 --pathfindingNodeQueueCount;
269 return &node;
270 }
271
272 // transform a world position to a map position in the array;
273 // returns true if the position is inside the map
274 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
275 {
276 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
277 *mapX = (int16_t)mapPosition.x;
278 *mapY = (int16_t)mapPosition.z;
279 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
280 }
281
282 void PathFindingMapUpdate()
283 {
284 const int castleX = 0, castleY = 0;
285 int16_t castleMapX, castleMapY;
286 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
287 {
288 return;
289 }
290 int width = pathfindingMap.width, height = pathfindingMap.height;
291
292 // reset the distances to -1
293 for (int i = 0; i < width * height; i++)
294 {
295 pathfindingMap.distances[i] = -1.0f;
296 }
297 // reset the tower indices
298 for (int i = 0; i < width * height; i++)
299 {
300 pathfindingMap.towerIndex[i] = -1;
301 }
302 // reset the delta src
303 for (int i = 0; i < width * height; i++)
304 {
305 pathfindingMap.deltaSrc[i].x = 0;
306 pathfindingMap.deltaSrc[i].y = 0;
307 }
308
309 for (int i = 0; i < towerCount; i++)
310 {
311 Tower *tower = &towers[i];
312 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
313 {
314 continue;
315 }
316 int16_t mapX, mapY;
317 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
318 // this would not work correctly and needs to be refined to allow towers covering multiple cells
319 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
320 // one cell. For now.
321 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
322 {
323 continue;
324 }
325 int index = mapY * width + mapX;
326 pathfindingMap.towerIndex[index] = i;
327 }
328
329 // we start at the castle and add the castle to the queue
330 pathfindingMap.maxDistance = 0.0f;
331 pathfindingNodeQueueCount = 0;
332 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
333 PathfindingNode *node = 0;
334 while ((node = PathFindingNodePop()))
335 {
336 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
337 {
338 continue;
339 }
340 int index = node->y * width + node->x;
341 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
342 {
343 continue;
344 }
345
346 int deltaX = node->x - node->fromX;
347 int deltaY = node->y - node->fromY;
348 // even if the cell is blocked by a tower, we still may want to store the direction
349 // (though this might not be needed, IDK right now)
350 pathfindingMap.deltaSrc[index].x = (char) deltaX;
351 pathfindingMap.deltaSrc[index].y = (char) deltaY;
352
353 // we skip nodes that are blocked by towers
354 if (pathfindingMap.towerIndex[index] >= 0)
355 {
356 node->distance += 8.0f;
357 }
358 pathfindingMap.distances[index] = node->distance;
359 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
360 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
361 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
362 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
363 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
364 }
365 }
366
367 void PathFindingMapDraw()
368 {
369 float cellSize = pathfindingMap.scale * 0.9f;
370 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
371 for (int x = 0; x < pathfindingMap.width; x++)
372 {
373 for (int y = 0; y < pathfindingMap.height; y++)
374 {
375 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
376 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
377 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
378 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
379 // animate the distance "wave" to show how the pathfinding algorithm expands
380 // from the castle
381 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
382 {
383 color = BLACK;
384 }
385 DrawCube(position, cellSize, 0.1f, cellSize, color);
386 }
387 }
388 }
389
390 Vector2 PathFindingGetGradient(Vector3 world)
391 {
392 int16_t mapX, mapY;
393 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
394 {
395 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
396 return (Vector2){(float)-delta.x, (float)-delta.y};
397 }
398 // fallback to a simple gradient calculation
399 float n = PathFindingGetDistance(mapX, mapY - 1);
400 float s = PathFindingGetDistance(mapX, mapY + 1);
401 float w = PathFindingGetDistance(mapX - 1, mapY);
402 float e = PathFindingGetDistance(mapX + 1, mapY);
403 return (Vector2){w - e + 0.25f, n - s + 0.125f};
404 }
405
406 //# Enemies
407
408 #define ENEMY_MAX_PATH_COUNT 8
409 #define ENEMY_MAX_COUNT 400
410 #define ENEMY_TYPE_NONE 0
411 #define ENEMY_TYPE_MINION 1
412
413 typedef struct EnemyId
414 {
415 uint16_t index;
416 uint16_t generation;
417 } EnemyId;
418
419 typedef struct EnemyClassConfig
420 {
421 float speed;
422 float health;
423 float radius;
424 float maxAcceleration;
425 float requiredContactTime;
426 float explosionDamage;
427 float explosionRange;
428 float explosionPushbackPower;
429 } EnemyClassConfig;
430
431 typedef struct Enemy
432 {
433 int16_t currentX, currentY;
434 int16_t nextX, nextY;
435 Vector2 simPosition;
436 Vector2 simVelocity;
437 uint16_t generation;
438 float startMovingTime;
439 float damage, futureDamage;
440 float contactTime;
441 uint8_t enemyType;
442 uint8_t movePathCount;
443 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
444 } Enemy;
445
446 Enemy enemies[ENEMY_MAX_COUNT];
447 int enemyCount = 0;
448
449 EnemyClassConfig enemyClassConfigs[] = {
450 [ENEMY_TYPE_MINION] = {
451 .health = 3.0f,
452 .speed = 1.0f,
453 .radius = 0.25f,
454 .maxAcceleration = 1.0f,
455 .explosionDamage = 1.0f,
456 .requiredContactTime = 0.5f,
457 .explosionRange = 1.0f,
458 .explosionPushbackPower = 0.25f,
459 },
460 };
461
462 int EnemyAddDamage(Enemy *enemy, float damage);
463
464 void EnemyInit()
465 {
466 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
467 {
468 enemies[i] = (Enemy){0};
469 }
470 enemyCount = 0;
471 }
472
473 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
474 {
475 return enemyClassConfigs[enemy->enemyType].speed;
476 }
477
478 float EnemyGetMaxHealth(Enemy *enemy)
479 {
480 return enemyClassConfigs[enemy->enemyType].health;
481 }
482
483 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
484 {
485 int16_t castleX = 0;
486 int16_t castleY = 0;
487 int16_t dx = castleX - currentX;
488 int16_t dy = castleY - currentY;
489 if (dx == 0 && dy == 0)
490 {
491 *nextX = currentX;
492 *nextY = currentY;
493 return 1;
494 }
495 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
496
497 if (gradient.x == 0 && gradient.y == 0)
498 {
499 *nextX = currentX;
500 *nextY = currentY;
501 return 1;
502 }
503
504 if (fabsf(gradient.x) > fabsf(gradient.y))
505 {
506 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
507 *nextY = currentY;
508 return 0;
509 }
510 *nextX = currentX;
511 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
512 return 0;
513 }
514
515
516 // this function predicts the movement of the unit for the next deltaT seconds
517 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
518 {
519 const float pointReachedDistance = 0.25f;
520 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
521 const float maxSimStepTime = 0.015625f;
522
523 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
524 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
525 int16_t nextX = enemy->nextX;
526 int16_t nextY = enemy->nextY;
527 Vector2 position = enemy->simPosition;
528 int passedCount = 0;
529 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
530 {
531 float stepTime = fminf(deltaT - t, maxSimStepTime);
532 Vector2 target = (Vector2){nextX, nextY};
533 float speed = Vector2Length(*velocity);
534 // draw the target position for debugging
535 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
536 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
537 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
538 {
539 // we reached the target position, let's move to the next waypoint
540 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
541 target = (Vector2){nextX, nextY};
542 // track how many waypoints we passed
543 passedCount++;
544 }
545
546 // acceleration towards the target
547 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
548 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
549 *velocity = Vector2Add(*velocity, acceleration);
550
551 // limit the speed to the maximum speed
552 if (speed > maxSpeed)
553 {
554 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
555 }
556
557 // move the enemy
558 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
559 }
560
561 if (waypointPassedCount)
562 {
563 (*waypointPassedCount) = passedCount;
564 }
565
566 return position;
567 }
568
569 void EnemyDraw()
570 {
571 for (int i = 0; i < enemyCount; i++)
572 {
573 Enemy enemy = enemies[i];
574 if (enemy.enemyType == ENEMY_TYPE_NONE)
575 {
576 continue;
577 }
578
579 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
580
581 if (enemy.movePathCount > 0)
582 {
583 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
584 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
585 }
586 for (int j = 1; j < enemy.movePathCount; j++)
587 {
588 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
589 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
590 DrawLine3D(p, q, GREEN);
591 }
592
593 switch (enemy.enemyType)
594 {
595 case ENEMY_TYPE_MINION:
596 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
597 break;
598 }
599 }
600 }
601
602 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
603 {
604 // damage the tower
605 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
606 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
607 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
608 float explosionRange2 = explosionRange * explosionRange;
609 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
610 // explode the enemy
611 if (tower->damage >= TowerGetMaxHealth(tower))
612 {
613 tower->towerType = TOWER_TYPE_NONE;
614 }
615
616 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
617 explosionSource,
618 (Vector3){0, 0.1f, 0}, 1.0f);
619
620 enemy->enemyType = ENEMY_TYPE_NONE;
621
622 // push back enemies & dealing damage
623 for (int i = 0; i < enemyCount; i++)
624 {
625 Enemy *other = &enemies[i];
626 if (other->enemyType == ENEMY_TYPE_NONE)
627 {
628 continue;
629 }
630 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
631 if (distanceSqr > 0 && distanceSqr < explosionRange2)
632 {
633 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
634 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
635 EnemyAddDamage(other, explosionDamge);
636 }
637 }
638 }
639
640 void EnemyUpdate()
641 {
642 const float castleX = 0;
643 const float castleY = 0;
644 const float maxPathDistance2 = 0.25f * 0.25f;
645
646 for (int i = 0; i < enemyCount; i++)
647 {
648 Enemy *enemy = &enemies[i];
649 if (enemy->enemyType == ENEMY_TYPE_NONE)
650 {
651 continue;
652 }
653
654 int waypointPassedCount = 0;
655 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
656 enemy->startMovingTime = gameTime.time;
657 // track path of unit
658 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
659 {
660 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
661 {
662 enemy->movePath[j] = enemy->movePath[j - 1];
663 }
664 enemy->movePath[0] = enemy->simPosition;
665 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
666 {
667 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
668 }
669 }
670
671 if (waypointPassedCount > 0)
672 {
673 enemy->currentX = enemy->nextX;
674 enemy->currentY = enemy->nextY;
675 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
676 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
677 {
678 // enemy reached the castle; remove it
679 enemy->enemyType = ENEMY_TYPE_NONE;
680 continue;
681 }
682 }
683 }
684
685 // handle collisions between enemies
686 for (int i = 0; i < enemyCount - 1; i++)
687 {
688 Enemy *enemyA = &enemies[i];
689 if (enemyA->enemyType == ENEMY_TYPE_NONE)
690 {
691 continue;
692 }
693 for (int j = i + 1; j < enemyCount; j++)
694 {
695 Enemy *enemyB = &enemies[j];
696 if (enemyB->enemyType == ENEMY_TYPE_NONE)
697 {
698 continue;
699 }
700 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
701 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
702 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
703 float radiusSum = radiusA + radiusB;
704 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
705 {
706 // collision
707 float distance = sqrtf(distanceSqr);
708 float overlap = radiusSum - distance;
709 // move the enemies apart, but softly; if we have a clog of enemies,
710 // moving them perfectly apart can cause them to jitter
711 float positionCorrection = overlap / 5.0f;
712 Vector2 direction = (Vector2){
713 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
714 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
715 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
716 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
717 }
718 }
719 }
720
721 // handle collisions between enemies and towers
722 for (int i = 0; i < enemyCount; i++)
723 {
724 Enemy *enemy = &enemies[i];
725 if (enemy->enemyType == ENEMY_TYPE_NONE)
726 {
727 continue;
728 }
729 enemy->contactTime -= gameTime.deltaTime;
730 if (enemy->contactTime < 0.0f)
731 {
732 enemy->contactTime = 0.0f;
733 }
734
735 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
736 // linear search over towers; could be optimized by using path finding tower map,
737 // but for now, we keep it simple
738 for (int j = 0; j < towerCount; j++)
739 {
740 Tower *tower = &towers[j];
741 if (tower->towerType == TOWER_TYPE_NONE)
742 {
743 continue;
744 }
745 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
746 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
747 if (distanceSqr > combinedRadius * combinedRadius)
748 {
749 continue;
750 }
751 // potential collision; square / circle intersection
752 float dx = tower->x - enemy->simPosition.x;
753 float dy = tower->y - enemy->simPosition.y;
754 float absDx = fabsf(dx);
755 float absDy = fabsf(dy);
756 Vector3 contactPoint = {0};
757 if (absDx <= 0.5f && absDx <= absDy) {
758 // vertical collision; push the enemy out horizontally
759 float overlap = enemyRadius + 0.5f - absDy;
760 if (overlap < 0.0f)
761 {
762 continue;
763 }
764 float direction = dy > 0.0f ? -1.0f : 1.0f;
765 enemy->simPosition.y += direction * overlap;
766 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
767 }
768 else if (absDy <= 0.5f && absDy <= absDx)
769 {
770 // horizontal collision; push the enemy out vertically
771 float overlap = enemyRadius + 0.5f - absDx;
772 if (overlap < 0.0f)
773 {
774 continue;
775 }
776 float direction = dx > 0.0f ? -1.0f : 1.0f;
777 enemy->simPosition.x += direction * overlap;
778 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
779 }
780 else
781 {
782 // possible collision with a corner
783 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
784 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
785 float cornerX = tower->x + cornerDX;
786 float cornerY = tower->y + cornerDY;
787 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
788 if (cornerDistanceSqr > enemyRadius * enemyRadius)
789 {
790 continue;
791 }
792 // push the enemy out along the diagonal
793 float cornerDistance = sqrtf(cornerDistanceSqr);
794 float overlap = enemyRadius - cornerDistance;
795 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
796 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
797 enemy->simPosition.x -= directionX * overlap;
798 enemy->simPosition.y -= directionY * overlap;
799 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
800 }
801
802 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
803 {
804 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
805 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
806 {
807 EnemyTriggerExplode(enemy, tower, contactPoint);
808 }
809 }
810 }
811 }
812 }
813
814 EnemyId EnemyGetId(Enemy *enemy)
815 {
816 return (EnemyId){enemy - enemies, enemy->generation};
817 }
818
819 Enemy *EnemyTryResolve(EnemyId enemyId)
820 {
821 if (enemyId.index >= ENEMY_MAX_COUNT)
822 {
823 return 0;
824 }
825 Enemy *enemy = &enemies[enemyId.index];
826 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
827 {
828 return 0;
829 }
830 return enemy;
831 }
832
833 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
834 {
835 Enemy *spawn = 0;
836 for (int i = 0; i < enemyCount; i++)
837 {
838 Enemy *enemy = &enemies[i];
839 if (enemy->enemyType == ENEMY_TYPE_NONE)
840 {
841 spawn = enemy;
842 break;
843 }
844 }
845
846 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
847 {
848 spawn = &enemies[enemyCount++];
849 }
850
851 if (spawn)
852 {
853 spawn->currentX = currentX;
854 spawn->currentY = currentY;
855 spawn->nextX = currentX;
856 spawn->nextY = currentY;
857 spawn->simPosition = (Vector2){currentX, currentY};
858 spawn->simVelocity = (Vector2){0, 0};
859 spawn->enemyType = enemyType;
860 spawn->startMovingTime = gameTime.time;
861 spawn->damage = 0.0f;
862 spawn->futureDamage = 0.0f;
863 spawn->generation++;
864 spawn->movePathCount = 0;
865 }
866
867 return spawn;
868 }
869
870 int EnemyAddDamage(Enemy *enemy, float damage)
871 {
872 enemy->damage += damage;
873 if (enemy->damage >= EnemyGetMaxHealth(enemy))
874 {
875 enemy->enemyType = ENEMY_TYPE_NONE;
876 return 1;
877 }
878
879 return 0;
880 }
881
882 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
883 {
884 int16_t castleX = 0;
885 int16_t castleY = 0;
886 Enemy* closest = 0;
887 int16_t closestDistance = 0;
888 float range2 = range * range;
889 for (int i = 0; i < enemyCount; i++)
890 {
891 Enemy* enemy = &enemies[i];
892 if (enemy->enemyType == ENEMY_TYPE_NONE)
893 {
894 continue;
895 }
896 float maxHealth = EnemyGetMaxHealth(enemy);
897 if (enemy->futureDamage >= maxHealth)
898 {
899 // ignore enemies that will die soon
900 continue;
901 }
902 int16_t dx = castleX - enemy->currentX;
903 int16_t dy = castleY - enemy->currentY;
904 int16_t distance = abs(dx) + abs(dy);
905 if (!closest || distance < closestDistance)
906 {
907 float tdx = towerX - enemy->currentX;
908 float tdy = towerY - enemy->currentY;
909 float tdistance2 = tdx * tdx + tdy * tdy;
910 if (tdistance2 <= range2)
911 {
912 closest = enemy;
913 closestDistance = distance;
914 }
915 }
916 }
917 return closest;
918 }
919
920 int EnemyCount()
921 {
922 int count = 0;
923 for (int i = 0; i < enemyCount; i++)
924 {
925 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
926 {
927 count++;
928 }
929 }
930 return count;
931 }
932
933 //# Projectiles
934 #define PROJECTILE_MAX_COUNT 1200
935 #define PROJECTILE_TYPE_NONE 0
936 #define PROJECTILE_TYPE_BULLET 1
937
938 typedef struct Projectile
939 {
940 uint8_t projectileType;
941 float shootTime;
942 float arrivalTime;
943 float damage;
944 Vector2 position;
945 Vector2 target;
946 Vector2 directionNormal;
947 EnemyId targetEnemy;
948 } Projectile;
949
950 Projectile projectiles[PROJECTILE_MAX_COUNT];
951 int projectileCount = 0;
952
953 void ProjectileInit()
954 {
955 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
956 {
957 projectiles[i] = (Projectile){0};
958 }
959 }
960
961 void ProjectileDraw()
962 {
963 for (int i = 0; i < projectileCount; i++)
964 {
965 Projectile projectile = projectiles[i];
966 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
967 {
968 continue;
969 }
970 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
971 if (transition >= 1.0f)
972 {
973 continue;
974 }
975 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
976 float x = position.x;
977 float y = position.y;
978 float dx = projectile.directionNormal.x;
979 float dy = projectile.directionNormal.y;
980 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
981 {
982 x -= dx * 0.1f;
983 y -= dy * 0.1f;
984 float size = 0.1f * d;
985 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
986 }
987 }
988 }
989
990 void ProjectileUpdate()
991 {
992 for (int i = 0; i < projectileCount; i++)
993 {
994 Projectile *projectile = &projectiles[i];
995 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
996 {
997 continue;
998 }
999 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
1000 if (transition >= 1.0f)
1001 {
1002 projectile->projectileType = PROJECTILE_TYPE_NONE;
1003 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
1004 if (enemy)
1005 {
1006 EnemyAddDamage(enemy, projectile->damage);
1007 }
1008 continue;
1009 }
1010 }
1011 }
1012
1013 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
1014 {
1015 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
1016 {
1017 Projectile *projectile = &projectiles[i];
1018 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
1019 {
1020 projectile->projectileType = projectileType;
1021 projectile->shootTime = gameTime.time;
1022 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
1023 projectile->damage = damage;
1024 projectile->position = position;
1025 projectile->target = target;
1026 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
1027 projectile->targetEnemy = EnemyGetId(enemy);
1028 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
1029 return projectile;
1030 }
1031 }
1032 return 0;
1033 }
1034
1035 //# Towers
1036
1037 void TowerInit()
1038 {
1039 for (int i = 0; i < TOWER_MAX_COUNT; i++)
1040 {
1041 towers[i] = (Tower){0};
1042 }
1043 towerCount = 0;
1044 }
1045
1046 Tower *TowerGetAt(int16_t x, int16_t y)
1047 {
1048 for (int i = 0; i < towerCount; i++)
1049 {
1050 if (towers[i].x == x && towers[i].y == y)
1051 {
1052 return &towers[i];
1053 }
1054 }
1055 return 0;
1056 }
1057
1058 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
1059 {
1060 if (towerCount >= TOWER_MAX_COUNT)
1061 {
1062 return 0;
1063 }
1064
1065 Tower *tower = TowerGetAt(x, y);
1066 if (tower)
1067 {
1068 return 0;
1069 }
1070
1071 tower = &towers[towerCount++];
1072 tower->x = x;
1073 tower->y = y;
1074 tower->towerType = towerType;
1075 tower->cooldown = 0.0f;
1076 tower->damage = 0.0f;
1077 return tower;
1078 }
1079
1080 float TowerGetMaxHealth(Tower *tower)
1081 {
1082 switch (tower->towerType)
1083 {
1084 case TOWER_TYPE_BASE:
1085 return 10.0f;
1086 case TOWER_TYPE_GUN:
1087 return 3.0f;
1088 case TOWER_TYPE_WALL:
1089 return 5.0f;
1090 }
1091 return 0.0f;
1092 }
1093
1094 void TowerDraw()
1095 {
1096 for (int i = 0; i < towerCount; i++)
1097 {
1098 Tower tower = towers[i];
1099 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
1100 switch (tower.towerType)
1101 {
1102 case TOWER_TYPE_BASE:
1103 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
1104 break;
1105 case TOWER_TYPE_GUN:
1106 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
1107 break;
1108 case TOWER_TYPE_WALL:
1109 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
1110 break;
1111 }
1112 }
1113 }
1114
1115 void TowerGunUpdate(Tower *tower)
1116 {
1117 if (tower->cooldown <= 0)
1118 {
1119 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
1120 if (enemy)
1121 {
1122 tower->cooldown = 0.125f;
1123 // shoot the enemy; determine future position of the enemy
1124 float bulletSpeed = 1.0f;
1125 float bulletDamage = 3.0f;
1126 Vector2 velocity = enemy->simVelocity;
1127 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
1128 Vector2 towerPosition = {tower->x, tower->y};
1129 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
1130 for (int i = 0; i < 8; i++) {
1131 velocity = enemy->simVelocity;
1132 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
1133 float distance = Vector2Distance(towerPosition, futurePosition);
1134 float eta2 = distance / bulletSpeed;
1135 if (fabs(eta - eta2) < 0.01f) {
1136 break;
1137 }
1138 eta = (eta2 + eta) * 0.5f;
1139 }
1140 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
1141 bulletSpeed, bulletDamage);
1142 enemy->futureDamage += bulletDamage;
1143 }
1144 }
1145 else
1146 {
1147 tower->cooldown -= gameTime.deltaTime;
1148 }
1149 }
1150
1151 void TowerUpdate()
1152 {
1153 for (int i = 0; i < towerCount; i++)
1154 {
1155 Tower *tower = &towers[i];
1156 switch (tower->towerType)
1157 {
1158 case TOWER_TYPE_GUN:
1159 TowerGunUpdate(tower);
1160 break;
1161 }
1162 }
1163 }
1164
1165 //# Game
1166
1167 typedef enum LevelState
1168 {
1169 LEVEL_STATE_NONE,
1170 LEVEL_STATE_BUILDING,
1171 LEVEL_STATE_BATTLE,
1172 LEVEL_STATE_WON_WAVE,
1173 LEVEL_STATE_LOST_WAVE,
1174 LEVEL_STATE_WON_LEVEL,
1175 LEVEL_STATE_RESET,
1176 } LevelState;
1177
1178 typedef struct EnemyWave {
1179 uint8_t enemyType;
1180 uint8_t wave;
1181 uint16_t count;
1182 float interval;
1183 float delay;
1184 Vector2 spawnPosition;
1185
1186 uint16_t spawned;
1187 float timeToSpawnNext;
1188 } EnemyWave;
1189
1190 typedef struct Level
1191 {
1192 LevelState state;
1193 LevelState nextState;
1194 Camera3D camera;
1195 int placementMode;
1196
1197 EnemyWave waves[10];
1198 int currentWave;
1199 int activeWaveCount;
1200 } Level;
1201
1202 void InitLevel(Level *level)
1203 {
1204 TowerInit();
1205 EnemyInit();
1206 ProjectileInit();
1207 ParticleInit();
1208 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1209
1210 level->placementMode = 0;
1211 level->state = LEVEL_STATE_BUILDING;
1212 level->nextState = LEVEL_STATE_NONE;
1213
1214 Camera *camera = &level->camera;
1215 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
1216 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
1217 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
1218 camera->fovy = 45.0f;
1219 camera->projection = CAMERA_PERSPECTIVE;
1220 }
1221
1222 void DrawLevelBuildingState(Level *level)
1223 {
1224 BeginMode3D(level->camera);
1225 DrawGrid(10, 1.0f);
1226 TowerDraw();
1227 EnemyDraw();
1228 ProjectileDraw();
1229 ParticleDraw();
1230
1231 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
1232 float planeDistance = ray.position.y / -ray.direction.y;
1233 float planeX = ray.direction.x * planeDistance + ray.position.x;
1234 float planeY = ray.direction.z * planeDistance + ray.position.z;
1235 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
1236 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
1237 if (level->placementMode && !guiState.isBlocked)
1238 {
1239 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
1240 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1241 {
1242 TowerTryAdd(level->placementMode, mapX, mapY);
1243 level->placementMode = TOWER_TYPE_NONE;
1244 }
1245 }
1246
1247 guiState.isBlocked = 0;
1248
1249 EndMode3D();
1250
1251 static ButtonState buildWallButtonState = {0};
1252 static ButtonState buildGunButtonState = {0};
1253 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
1254 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
1255
1256 if (Button("Wall", 10, 50, 80, 30, &buildWallButtonState))
1257 {
1258 level->placementMode = buildWallButtonState.isSelected ? 0 : TOWER_TYPE_WALL;
1259 }
1260 if (Button("Gun", 10, 90, 80, 30, &buildGunButtonState))
1261 {
1262 level->placementMode = buildGunButtonState.isSelected ? 0 : TOWER_TYPE_GUN;
1263 }
1264
1265 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1266 {
1267 level->nextState = LEVEL_STATE_RESET;
1268 }
1269
1270 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
1271 {
1272 level->nextState = LEVEL_STATE_BATTLE;
1273 }
1274
1275 const char *text = "Building phase";
1276 int textWidth = MeasureText(text, 20);
1277 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1278 }
1279
1280 void DrawLevelBattleState(Level *level)
1281 {
1282 BeginMode3D(level->camera);
1283 DrawGrid(10, 1.0f);
1284 TowerDraw();
1285 EnemyDraw();
1286 ProjectileDraw();
1287 ParticleDraw();
1288 guiState.isBlocked = 0;
1289 EndMode3D();
1290
1291 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1292 {
1293 level->nextState = LEVEL_STATE_RESET;
1294 }
1295
1296 int maxCount = 0;
1297 int remainingCount = 0;
1298 for (int i = 0; i < 10; i++)
1299 {
1300 EnemyWave *wave = &level->waves[i];
1301 if (wave->wave != level->currentWave)
1302 {
1303 continue;
1304 }
1305 maxCount += wave->count;
1306 remainingCount += wave->count - wave->spawned;
1307 }
1308 int aliveCount = EnemyCount();
1309 remainingCount += aliveCount;
1310
1311 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1312 int textWidth = MeasureText(text, 20);
1313 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1314 }
1315
1316 void DrawLevel(Level *level)
1317 {
1318 switch (level->state)
1319 {
1320 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1321 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1322 default: break;
1323 }
1324 }
1325
1326 void UpdateLevel(Level *level)
1327 {
1328 if (level->state == LEVEL_STATE_BATTLE)
1329 {
1330 int activeWaves = 0;
1331 for (int i = 0; i < 10; i++)
1332 {
1333 EnemyWave *wave = &level->waves[i];
1334 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1335 {
1336 continue;
1337 }
1338 activeWaves++;
1339 wave->timeToSpawnNext -= gameTime.deltaTime;
1340 if (wave->timeToSpawnNext <= 0.0f)
1341 {
1342 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1343 if (enemy)
1344 {
1345 wave->timeToSpawnNext = wave->interval;
1346 wave->spawned++;
1347 }
1348 }
1349 }
1350
1351 level->activeWaveCount = activeWaves;
1352 }
1353
1354 PathFindingMapUpdate();
1355 EnemyUpdate();
1356 TowerUpdate();
1357 ProjectileUpdate();
1358 ParticleUpdate();
1359
1360 if (level->nextState == LEVEL_STATE_RESET)
1361 {
1362 InitLevel(level);
1363 }
1364 if (level->nextState == LEVEL_STATE_BATTLE)
1365 {
1366 level->state = LEVEL_STATE_BATTLE;
1367 level->nextState = LEVEL_STATE_NONE;
1368 for (int i = 0; i < 10; i++)
1369 {
1370 EnemyWave *wave = &level->waves[i];
1371 wave->spawned = 0;
1372 wave->timeToSpawnNext = wave->delay;
1373 }
1374 }
1375 }
1376
1377 Level levels[] = {
1378 [0] = {
1379 .state = LEVEL_STATE_BUILDING,
1380 .waves[0] = {
1381 .enemyType = ENEMY_TYPE_MINION,
1382 .wave = 0,
1383 .count = 10,
1384 .interval = 1.0f,
1385 .delay = 1.0f,
1386 .spawnPosition = {0, 6},
1387 },
1388 },
1389 };
1390
1391 Level *currentLevel = levels;
1392
1393 float nextSpawnTime = 0.0f;
1394
1395 void ResetGame()
1396 {
1397 InitLevel(currentLevel);
1398 }
1399
1400 void InitGame()
1401 {
1402 TowerInit();
1403 EnemyInit();
1404 ProjectileInit();
1405 ParticleInit();
1406 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1407
1408 currentLevel = levels;
1409 InitLevel(currentLevel);
1410 }
1411
1412 //# Immediate GUI functions
1413
1414 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1415 {
1416 Rectangle bounds = {x, y, width, height};
1417 int isPressed = 0;
1418 int isSelected = state && state->isSelected;
1419 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked)
1420 {
1421 Color color = isSelected ? DARKGRAY : GRAY;
1422 DrawRectangle(x, y, width, height, color);
1423 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1424 {
1425 isPressed = 1;
1426 }
1427 guiState.isBlocked = 1;
1428 }
1429 else
1430 {
1431 Color color = isSelected ? WHITE : LIGHTGRAY;
1432 DrawRectangle(x, y, width, height, color);
1433 }
1434 Font font = GetFontDefault();
1435 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1436 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, BLACK);
1437 return isPressed;
1438 }
1439
1440 //# Main game loop
1441
1442 void GameUpdate()
1443 {
1444 float dt = GetFrameTime();
1445 // cap maximum delta time to 0.1 seconds to prevent large time steps
1446 if (dt > 0.1f) dt = 0.1f;
1447 gameTime.time += dt;
1448 gameTime.deltaTime = dt;
1449
1450 UpdateLevel(currentLevel);
1451 }
1452
1453 int main(void)
1454 {
1455 int screenWidth, screenHeight;
1456 GetPreferredSize(&screenWidth, &screenHeight);
1457 InitWindow(screenWidth, screenHeight, "Tower defense");
1458 SetTargetFPS(30);
1459
1460 InitGame();
1461
1462 while (!WindowShouldClose())
1463 {
1464 if (IsPaused()) {
1465 // canvas is not visible in browser - do nothing
1466 continue;
1467 }
1468
1469 BeginDrawing();
1470 ClearBackground(DARKBLUE);
1471
1472 GameUpdate();
1473 DrawLevel(currentLevel);
1474
1475 EndDrawing();
1476 }
1477
1478 CloseWindow();
1479
1480 return 0;
1481 }
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
When you start the wave now, most of the UI disappears and enemies begin to spawn. The waves are currently defined via this struct and declaration:
1 typedef struct EnemyWave {
2 uint8_t enemyType;
3 uint8_t wave;
4 uint16_t count;
5 float interval;
6 float delay;
7 Vector2 spawnPosition;
8
9 uint16_t spawned;
10 float timeToSpawnNext;
11 } EnemyWave;
12
13 typedef struct Level
14 {
15 (...)
16 EnemyWave waves[10];
17 int currentWave;
18 (...)
19 } Level;
20
21 (...)
22
23 Level levels[] = {
24 [0] = {
25 .state = LEVEL_STATE_BUILDING,
26 .waves[0] = {
27 .enemyType = ENEMY_TYPE_MINION,
28 .wave = 0,
29 .count = 10,
30 .interval = 1.0f,
31 .delay = 1.0f,
32 .spawnPosition = {0, 6},
33 },
34 },
35 };
The EnemyWave describes a single sequence of enemies that spawn during a wave and at which location they spawn. This allows us to spawn different enemies at different locations and times and intervals during a wave.
There is now also a progress counter for the running wave, but there's nothing happening yet when the wave is finished either by defeat or by winning. Let's add this now, including a second wave:
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 typedef struct ButtonState {
45 char isSelected;
46 } ButtonState;
47
48 typedef struct GUIState {
49 int isBlocked;
50 } GUIState;
51
52 GUIState guiState = {0};
53 GameTime gameTime = {0};
54 Tower towers[TOWER_MAX_COUNT];
55 int towerCount = 0;
56
57 float TowerGetMaxHealth(Tower *tower);
58 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
59
60 //# Particle system
61
62 void ParticleInit()
63 {
64 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
65 {
66 particles[i] = (Particle){0};
67 }
68 particleCount = 0;
69 }
70
71 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
72 {
73 if (particleCount >= PARTICLE_MAX_COUNT)
74 {
75 return;
76 }
77
78 int index = -1;
79 for (int i = 0; i < particleCount; i++)
80 {
81 if (particles[i].particleType == PARTICLE_TYPE_NONE)
82 {
83 index = i;
84 break;
85 }
86 }
87
88 if (index == -1)
89 {
90 index = particleCount++;
91 }
92
93 Particle *particle = &particles[index];
94 particle->particleType = particleType;
95 particle->spawnTime = gameTime.time;
96 particle->lifetime = lifetime;
97 particle->position = position;
98 particle->velocity = velocity;
99 }
100
101 void ParticleUpdate()
102 {
103 for (int i = 0; i < particleCount; i++)
104 {
105 Particle *particle = &particles[i];
106 if (particle->particleType == PARTICLE_TYPE_NONE)
107 {
108 continue;
109 }
110
111 float age = gameTime.time - particle->spawnTime;
112
113 if (particle->lifetime > age)
114 {
115 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
116 }
117 else {
118 particle->particleType = PARTICLE_TYPE_NONE;
119 }
120 }
121 }
122
123 void DrawExplosionParticle(Particle *particle, float transition)
124 {
125 float size = 1.2f * (1.0f - transition);
126 Color startColor = WHITE;
127 Color endColor = RED;
128 Color color = ColorLerp(startColor, endColor, transition);
129 DrawCube(particle->position, size, size, size, color);
130 }
131
132 void ParticleDraw()
133 {
134 for (int i = 0; i < particleCount; i++)
135 {
136 Particle particle = particles[i];
137 if (particle.particleType == PARTICLE_TYPE_NONE)
138 {
139 continue;
140 }
141
142 float age = gameTime.time - particle.spawnTime;
143 float transition = age / particle.lifetime;
144 switch (particle.particleType)
145 {
146 case PARTICLE_TYPE_EXPLOSION:
147 DrawExplosionParticle(&particle, transition);
148 break;
149 default:
150 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
151 break;
152 }
153 }
154 }
155
156 //# Pathfinding map
157 typedef struct DeltaSrc
158 {
159 char x, y;
160 } DeltaSrc;
161
162 typedef struct PathfindingMap
163 {
164 int width, height;
165 float scale;
166 float *distances;
167 long *towerIndex;
168 DeltaSrc *deltaSrc;
169 float maxDistance;
170 Matrix toMapSpace;
171 Matrix toWorldSpace;
172 } PathfindingMap;
173
174 // when we execute the pathfinding algorithm, we need to store the active nodes
175 // in a queue. Each node has a position, a distance from the start, and the
176 // position of the node that we came from.
177 typedef struct PathfindingNode
178 {
179 int16_t x, y, fromX, fromY;
180 float distance;
181 } PathfindingNode;
182
183 // The queue is a simple array of nodes, we add nodes to the end and remove
184 // nodes from the front. We keep the array around to avoid unnecessary allocations
185 static PathfindingNode *pathfindingNodeQueue = 0;
186 static int pathfindingNodeQueueCount = 0;
187 static int pathfindingNodeQueueCapacity = 0;
188
189 // The pathfinding map stores the distances from the castle to each cell in the map.
190 PathfindingMap pathfindingMap = {0};
191
192 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
193 {
194 // transforming between map space and world space allows us to adapt
195 // position and scale of the map without changing the pathfinding data
196 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
197 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
198 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
199 pathfindingMap.width = width;
200 pathfindingMap.height = height;
201 pathfindingMap.scale = scale;
202 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
203 for (int i = 0; i < width * height; i++)
204 {
205 pathfindingMap.distances[i] = -1.0f;
206 }
207
208 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
209 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
210 }
211
212 float PathFindingGetDistance(int mapX, int mapY)
213 {
214 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
215 {
216 // when outside the map, we return the manhattan distance to the castle (0,0)
217 return fabsf((float)mapX) + fabsf((float)mapY);
218 }
219
220 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
221 }
222
223 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
224 {
225 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
226 {
227 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
228 // we use MemAlloc/MemRealloc to allocate memory for the queue
229 // I am not entirely sure if MemRealloc allows passing a null pointer
230 // so we check if the pointer is null and use MemAlloc in that case
231 if (pathfindingNodeQueue == 0)
232 {
233 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
234 }
235 else
236 {
237 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
238 }
239 }
240
241 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
242 node->x = x;
243 node->y = y;
244 node->fromX = fromX;
245 node->fromY = fromY;
246 node->distance = distance;
247 }
248
249 PathfindingNode *PathFindingNodePop()
250 {
251 if (pathfindingNodeQueueCount == 0)
252 {
253 return 0;
254 }
255 // we return the first node in the queue; we want to return a pointer to the node
256 // so we can return 0 if the queue is empty.
257 // We should _not_ return a pointer to the element in the list, because the list
258 // may be reallocated and the pointer would become invalid. Or the
259 // popped element is overwritten by the next push operation.
260 // Using static here means that the variable is permanently allocated.
261 static PathfindingNode node;
262 node = pathfindingNodeQueue[0];
263 // we shift all nodes one position to the front
264 for (int i = 1; i < pathfindingNodeQueueCount; i++)
265 {
266 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
267 }
268 --pathfindingNodeQueueCount;
269 return &node;
270 }
271
272 // transform a world position to a map position in the array;
273 // returns true if the position is inside the map
274 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
275 {
276 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
277 *mapX = (int16_t)mapPosition.x;
278 *mapY = (int16_t)mapPosition.z;
279 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
280 }
281
282 void PathFindingMapUpdate()
283 {
284 const int castleX = 0, castleY = 0;
285 int16_t castleMapX, castleMapY;
286 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
287 {
288 return;
289 }
290 int width = pathfindingMap.width, height = pathfindingMap.height;
291
292 // reset the distances to -1
293 for (int i = 0; i < width * height; i++)
294 {
295 pathfindingMap.distances[i] = -1.0f;
296 }
297 // reset the tower indices
298 for (int i = 0; i < width * height; i++)
299 {
300 pathfindingMap.towerIndex[i] = -1;
301 }
302 // reset the delta src
303 for (int i = 0; i < width * height; i++)
304 {
305 pathfindingMap.deltaSrc[i].x = 0;
306 pathfindingMap.deltaSrc[i].y = 0;
307 }
308
309 for (int i = 0; i < towerCount; i++)
310 {
311 Tower *tower = &towers[i];
312 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
313 {
314 continue;
315 }
316 int16_t mapX, mapY;
317 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
318 // this would not work correctly and needs to be refined to allow towers covering multiple cells
319 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
320 // one cell. For now.
321 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
322 {
323 continue;
324 }
325 int index = mapY * width + mapX;
326 pathfindingMap.towerIndex[index] = i;
327 }
328
329 // we start at the castle and add the castle to the queue
330 pathfindingMap.maxDistance = 0.0f;
331 pathfindingNodeQueueCount = 0;
332 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
333 PathfindingNode *node = 0;
334 while ((node = PathFindingNodePop()))
335 {
336 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
337 {
338 continue;
339 }
340 int index = node->y * width + node->x;
341 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
342 {
343 continue;
344 }
345
346 int deltaX = node->x - node->fromX;
347 int deltaY = node->y - node->fromY;
348 // even if the cell is blocked by a tower, we still may want to store the direction
349 // (though this might not be needed, IDK right now)
350 pathfindingMap.deltaSrc[index].x = (char) deltaX;
351 pathfindingMap.deltaSrc[index].y = (char) deltaY;
352
353 // we skip nodes that are blocked by towers
354 if (pathfindingMap.towerIndex[index] >= 0)
355 {
356 node->distance += 8.0f;
357 }
358 pathfindingMap.distances[index] = node->distance;
359 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
360 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
361 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
362 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
363 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
364 }
365 }
366
367 void PathFindingMapDraw()
368 {
369 float cellSize = pathfindingMap.scale * 0.9f;
370 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
371 for (int x = 0; x < pathfindingMap.width; x++)
372 {
373 for (int y = 0; y < pathfindingMap.height; y++)
374 {
375 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
376 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
377 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
378 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
379 // animate the distance "wave" to show how the pathfinding algorithm expands
380 // from the castle
381 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
382 {
383 color = BLACK;
384 }
385 DrawCube(position, cellSize, 0.1f, cellSize, color);
386 }
387 }
388 }
389
390 Vector2 PathFindingGetGradient(Vector3 world)
391 {
392 int16_t mapX, mapY;
393 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
394 {
395 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
396 return (Vector2){(float)-delta.x, (float)-delta.y};
397 }
398 // fallback to a simple gradient calculation
399 float n = PathFindingGetDistance(mapX, mapY - 1);
400 float s = PathFindingGetDistance(mapX, mapY + 1);
401 float w = PathFindingGetDistance(mapX - 1, mapY);
402 float e = PathFindingGetDistance(mapX + 1, mapY);
403 return (Vector2){w - e + 0.25f, n - s + 0.125f};
404 }
405
406 //# Enemies
407
408 #define ENEMY_MAX_PATH_COUNT 8
409 #define ENEMY_MAX_COUNT 400
410 #define ENEMY_TYPE_NONE 0
411 #define ENEMY_TYPE_MINION 1
412
413 typedef struct EnemyId
414 {
415 uint16_t index;
416 uint16_t generation;
417 } EnemyId;
418
419 typedef struct EnemyClassConfig
420 {
421 float speed;
422 float health;
423 float radius;
424 float maxAcceleration;
425 float requiredContactTime;
426 float explosionDamage;
427 float explosionRange;
428 float explosionPushbackPower;
429 } EnemyClassConfig;
430
431 typedef struct Enemy
432 {
433 int16_t currentX, currentY;
434 int16_t nextX, nextY;
435 Vector2 simPosition;
436 Vector2 simVelocity;
437 uint16_t generation;
438 float startMovingTime;
439 float damage, futureDamage;
440 float contactTime;
441 uint8_t enemyType;
442 uint8_t movePathCount;
443 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
444 } Enemy;
445
446 Enemy enemies[ENEMY_MAX_COUNT];
447 int enemyCount = 0;
448
449 EnemyClassConfig enemyClassConfigs[] = {
450 [ENEMY_TYPE_MINION] = {
451 .health = 3.0f,
452 .speed = 1.0f,
453 .radius = 0.25f,
454 .maxAcceleration = 1.0f,
455 .explosionDamage = 1.0f,
456 .requiredContactTime = 0.5f,
457 .explosionRange = 1.0f,
458 .explosionPushbackPower = 0.25f,
459 },
460 };
461
462 int EnemyAddDamage(Enemy *enemy, float damage);
463
464 void EnemyInit()
465 {
466 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
467 {
468 enemies[i] = (Enemy){0};
469 }
470 enemyCount = 0;
471 }
472
473 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
474 {
475 return enemyClassConfigs[enemy->enemyType].speed;
476 }
477
478 float EnemyGetMaxHealth(Enemy *enemy)
479 {
480 return enemyClassConfigs[enemy->enemyType].health;
481 }
482
483 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
484 {
485 int16_t castleX = 0;
486 int16_t castleY = 0;
487 int16_t dx = castleX - currentX;
488 int16_t dy = castleY - currentY;
489 if (dx == 0 && dy == 0)
490 {
491 *nextX = currentX;
492 *nextY = currentY;
493 return 1;
494 }
495 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
496
497 if (gradient.x == 0 && gradient.y == 0)
498 {
499 *nextX = currentX;
500 *nextY = currentY;
501 return 1;
502 }
503
504 if (fabsf(gradient.x) > fabsf(gradient.y))
505 {
506 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
507 *nextY = currentY;
508 return 0;
509 }
510 *nextX = currentX;
511 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
512 return 0;
513 }
514
515
516 // this function predicts the movement of the unit for the next deltaT seconds
517 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
518 {
519 const float pointReachedDistance = 0.25f;
520 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
521 const float maxSimStepTime = 0.015625f;
522
523 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
524 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
525 int16_t nextX = enemy->nextX;
526 int16_t nextY = enemy->nextY;
527 Vector2 position = enemy->simPosition;
528 int passedCount = 0;
529 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
530 {
531 float stepTime = fminf(deltaT - t, maxSimStepTime);
532 Vector2 target = (Vector2){nextX, nextY};
533 float speed = Vector2Length(*velocity);
534 // draw the target position for debugging
535 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
536 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
537 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
538 {
539 // we reached the target position, let's move to the next waypoint
540 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
541 target = (Vector2){nextX, nextY};
542 // track how many waypoints we passed
543 passedCount++;
544 }
545
546 // acceleration towards the target
547 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
548 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
549 *velocity = Vector2Add(*velocity, acceleration);
550
551 // limit the speed to the maximum speed
552 if (speed > maxSpeed)
553 {
554 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
555 }
556
557 // move the enemy
558 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
559 }
560
561 if (waypointPassedCount)
562 {
563 (*waypointPassedCount) = passedCount;
564 }
565
566 return position;
567 }
568
569 void EnemyDraw()
570 {
571 for (int i = 0; i < enemyCount; i++)
572 {
573 Enemy enemy = enemies[i];
574 if (enemy.enemyType == ENEMY_TYPE_NONE)
575 {
576 continue;
577 }
578
579 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
580
581 if (enemy.movePathCount > 0)
582 {
583 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
584 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
585 }
586 for (int j = 1; j < enemy.movePathCount; j++)
587 {
588 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
589 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
590 DrawLine3D(p, q, GREEN);
591 }
592
593 switch (enemy.enemyType)
594 {
595 case ENEMY_TYPE_MINION:
596 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
597 break;
598 }
599 }
600 }
601
602 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
603 {
604 // damage the tower
605 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
606 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
607 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
608 float explosionRange2 = explosionRange * explosionRange;
609 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
610 // explode the enemy
611 if (tower->damage >= TowerGetMaxHealth(tower))
612 {
613 tower->towerType = TOWER_TYPE_NONE;
614 }
615
616 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
617 explosionSource,
618 (Vector3){0, 0.1f, 0}, 1.0f);
619
620 enemy->enemyType = ENEMY_TYPE_NONE;
621
622 // push back enemies & dealing damage
623 for (int i = 0; i < enemyCount; i++)
624 {
625 Enemy *other = &enemies[i];
626 if (other->enemyType == ENEMY_TYPE_NONE)
627 {
628 continue;
629 }
630 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
631 if (distanceSqr > 0 && distanceSqr < explosionRange2)
632 {
633 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
634 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
635 EnemyAddDamage(other, explosionDamge);
636 }
637 }
638 }
639
640 void EnemyUpdate()
641 {
642 const float castleX = 0;
643 const float castleY = 0;
644 const float maxPathDistance2 = 0.25f * 0.25f;
645
646 for (int i = 0; i < enemyCount; i++)
647 {
648 Enemy *enemy = &enemies[i];
649 if (enemy->enemyType == ENEMY_TYPE_NONE)
650 {
651 continue;
652 }
653
654 int waypointPassedCount = 0;
655 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
656 enemy->startMovingTime = gameTime.time;
657 // track path of unit
658 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
659 {
660 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
661 {
662 enemy->movePath[j] = enemy->movePath[j - 1];
663 }
664 enemy->movePath[0] = enemy->simPosition;
665 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
666 {
667 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
668 }
669 }
670
671 if (waypointPassedCount > 0)
672 {
673 enemy->currentX = enemy->nextX;
674 enemy->currentY = enemy->nextY;
675 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
676 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
677 {
678 // enemy reached the castle; remove it
679 enemy->enemyType = ENEMY_TYPE_NONE;
680 continue;
681 }
682 }
683 }
684
685 // handle collisions between enemies
686 for (int i = 0; i < enemyCount - 1; i++)
687 {
688 Enemy *enemyA = &enemies[i];
689 if (enemyA->enemyType == ENEMY_TYPE_NONE)
690 {
691 continue;
692 }
693 for (int j = i + 1; j < enemyCount; j++)
694 {
695 Enemy *enemyB = &enemies[j];
696 if (enemyB->enemyType == ENEMY_TYPE_NONE)
697 {
698 continue;
699 }
700 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
701 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
702 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
703 float radiusSum = radiusA + radiusB;
704 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
705 {
706 // collision
707 float distance = sqrtf(distanceSqr);
708 float overlap = radiusSum - distance;
709 // move the enemies apart, but softly; if we have a clog of enemies,
710 // moving them perfectly apart can cause them to jitter
711 float positionCorrection = overlap / 5.0f;
712 Vector2 direction = (Vector2){
713 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
714 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
715 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
716 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
717 }
718 }
719 }
720
721 // handle collisions between enemies and towers
722 for (int i = 0; i < enemyCount; i++)
723 {
724 Enemy *enemy = &enemies[i];
725 if (enemy->enemyType == ENEMY_TYPE_NONE)
726 {
727 continue;
728 }
729 enemy->contactTime -= gameTime.deltaTime;
730 if (enemy->contactTime < 0.0f)
731 {
732 enemy->contactTime = 0.0f;
733 }
734
735 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
736 // linear search over towers; could be optimized by using path finding tower map,
737 // but for now, we keep it simple
738 for (int j = 0; j < towerCount; j++)
739 {
740 Tower *tower = &towers[j];
741 if (tower->towerType == TOWER_TYPE_NONE)
742 {
743 continue;
744 }
745 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
746 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
747 if (distanceSqr > combinedRadius * combinedRadius)
748 {
749 continue;
750 }
751 // potential collision; square / circle intersection
752 float dx = tower->x - enemy->simPosition.x;
753 float dy = tower->y - enemy->simPosition.y;
754 float absDx = fabsf(dx);
755 float absDy = fabsf(dy);
756 Vector3 contactPoint = {0};
757 if (absDx <= 0.5f && absDx <= absDy) {
758 // vertical collision; push the enemy out horizontally
759 float overlap = enemyRadius + 0.5f - absDy;
760 if (overlap < 0.0f)
761 {
762 continue;
763 }
764 float direction = dy > 0.0f ? -1.0f : 1.0f;
765 enemy->simPosition.y += direction * overlap;
766 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
767 }
768 else if (absDy <= 0.5f && absDy <= absDx)
769 {
770 // horizontal collision; push the enemy out vertically
771 float overlap = enemyRadius + 0.5f - absDx;
772 if (overlap < 0.0f)
773 {
774 continue;
775 }
776 float direction = dx > 0.0f ? -1.0f : 1.0f;
777 enemy->simPosition.x += direction * overlap;
778 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
779 }
780 else
781 {
782 // possible collision with a corner
783 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
784 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
785 float cornerX = tower->x + cornerDX;
786 float cornerY = tower->y + cornerDY;
787 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
788 if (cornerDistanceSqr > enemyRadius * enemyRadius)
789 {
790 continue;
791 }
792 // push the enemy out along the diagonal
793 float cornerDistance = sqrtf(cornerDistanceSqr);
794 float overlap = enemyRadius - cornerDistance;
795 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
796 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
797 enemy->simPosition.x -= directionX * overlap;
798 enemy->simPosition.y -= directionY * overlap;
799 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
800 }
801
802 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
803 {
804 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
805 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
806 {
807 EnemyTriggerExplode(enemy, tower, contactPoint);
808 }
809 }
810 }
811 }
812 }
813
814 EnemyId EnemyGetId(Enemy *enemy)
815 {
816 return (EnemyId){enemy - enemies, enemy->generation};
817 }
818
819 Enemy *EnemyTryResolve(EnemyId enemyId)
820 {
821 if (enemyId.index >= ENEMY_MAX_COUNT)
822 {
823 return 0;
824 }
825 Enemy *enemy = &enemies[enemyId.index];
826 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
827 {
828 return 0;
829 }
830 return enemy;
831 }
832
833 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
834 {
835 Enemy *spawn = 0;
836 for (int i = 0; i < enemyCount; i++)
837 {
838 Enemy *enemy = &enemies[i];
839 if (enemy->enemyType == ENEMY_TYPE_NONE)
840 {
841 spawn = enemy;
842 break;
843 }
844 }
845
846 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
847 {
848 spawn = &enemies[enemyCount++];
849 }
850
851 if (spawn)
852 {
853 spawn->currentX = currentX;
854 spawn->currentY = currentY;
855 spawn->nextX = currentX;
856 spawn->nextY = currentY;
857 spawn->simPosition = (Vector2){currentX, currentY};
858 spawn->simVelocity = (Vector2){0, 0};
859 spawn->enemyType = enemyType;
860 spawn->startMovingTime = gameTime.time;
861 spawn->damage = 0.0f;
862 spawn->futureDamage = 0.0f;
863 spawn->generation++;
864 spawn->movePathCount = 0;
865 }
866
867 return spawn;
868 }
869
870 int EnemyAddDamage(Enemy *enemy, float damage)
871 {
872 enemy->damage += damage;
873 if (enemy->damage >= EnemyGetMaxHealth(enemy))
874 {
875 enemy->enemyType = ENEMY_TYPE_NONE;
876 return 1;
877 }
878
879 return 0;
880 }
881
882 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
883 {
884 int16_t castleX = 0;
885 int16_t castleY = 0;
886 Enemy* closest = 0;
887 int16_t closestDistance = 0;
888 float range2 = range * range;
889 for (int i = 0; i < enemyCount; i++)
890 {
891 Enemy* enemy = &enemies[i];
892 if (enemy->enemyType == ENEMY_TYPE_NONE)
893 {
894 continue;
895 }
896 float maxHealth = EnemyGetMaxHealth(enemy);
897 if (enemy->futureDamage >= maxHealth)
898 {
899 // ignore enemies that will die soon
900 continue;
901 }
902 int16_t dx = castleX - enemy->currentX;
903 int16_t dy = castleY - enemy->currentY;
904 int16_t distance = abs(dx) + abs(dy);
905 if (!closest || distance < closestDistance)
906 {
907 float tdx = towerX - enemy->currentX;
908 float tdy = towerY - enemy->currentY;
909 float tdistance2 = tdx * tdx + tdy * tdy;
910 if (tdistance2 <= range2)
911 {
912 closest = enemy;
913 closestDistance = distance;
914 }
915 }
916 }
917 return closest;
918 }
919
920 int EnemyCount()
921 {
922 int count = 0;
923 for (int i = 0; i < enemyCount; i++)
924 {
925 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
926 {
927 count++;
928 }
929 }
930 return count;
931 }
932
933 //# Projectiles
934 #define PROJECTILE_MAX_COUNT 1200
935 #define PROJECTILE_TYPE_NONE 0
936 #define PROJECTILE_TYPE_BULLET 1
937
938 typedef struct Projectile
939 {
940 uint8_t projectileType;
941 float shootTime;
942 float arrivalTime;
943 float damage;
944 Vector2 position;
945 Vector2 target;
946 Vector2 directionNormal;
947 EnemyId targetEnemy;
948 } Projectile;
949
950 Projectile projectiles[PROJECTILE_MAX_COUNT];
951 int projectileCount = 0;
952
953 void ProjectileInit()
954 {
955 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
956 {
957 projectiles[i] = (Projectile){0};
958 }
959 }
960
961 void ProjectileDraw()
962 {
963 for (int i = 0; i < projectileCount; i++)
964 {
965 Projectile projectile = projectiles[i];
966 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
967 {
968 continue;
969 }
970 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
971 if (transition >= 1.0f)
972 {
973 continue;
974 }
975 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
976 float x = position.x;
977 float y = position.y;
978 float dx = projectile.directionNormal.x;
979 float dy = projectile.directionNormal.y;
980 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
981 {
982 x -= dx * 0.1f;
983 y -= dy * 0.1f;
984 float size = 0.1f * d;
985 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
986 }
987 }
988 }
989
990 void ProjectileUpdate()
991 {
992 for (int i = 0; i < projectileCount; i++)
993 {
994 Projectile *projectile = &projectiles[i];
995 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
996 {
997 continue;
998 }
999 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
1000 if (transition >= 1.0f)
1001 {
1002 projectile->projectileType = PROJECTILE_TYPE_NONE;
1003 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
1004 if (enemy)
1005 {
1006 EnemyAddDamage(enemy, projectile->damage);
1007 }
1008 continue;
1009 }
1010 }
1011 }
1012
1013 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
1014 {
1015 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
1016 {
1017 Projectile *projectile = &projectiles[i];
1018 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
1019 {
1020 projectile->projectileType = projectileType;
1021 projectile->shootTime = gameTime.time;
1022 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
1023 projectile->damage = damage;
1024 projectile->position = position;
1025 projectile->target = target;
1026 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
1027 projectile->targetEnemy = EnemyGetId(enemy);
1028 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
1029 return projectile;
1030 }
1031 }
1032 return 0;
1033 }
1034
1035 //# Towers
1036
1037 void TowerInit()
1038 {
1039 for (int i = 0; i < TOWER_MAX_COUNT; i++)
1040 {
1041 towers[i] = (Tower){0};
1042 }
1043 towerCount = 0;
1044 }
1045
1046 Tower *TowerGetAt(int16_t x, int16_t y)
1047 {
1048 for (int i = 0; i < towerCount; i++)
1049 {
1050 if (towers[i].x == x && towers[i].y == y)
1051 {
1052 return &towers[i];
1053 }
1054 }
1055 return 0;
1056 }
1057
1058 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
1059 {
1060 if (towerCount >= TOWER_MAX_COUNT)
1061 {
1062 return 0;
1063 }
1064
1065 Tower *tower = TowerGetAt(x, y);
1066 if (tower)
1067 {
1068 return 0;
1069 }
1070
1071 tower = &towers[towerCount++];
1072 tower->x = x;
1073 tower->y = y;
1074 tower->towerType = towerType;
1075 tower->cooldown = 0.0f;
1076 tower->damage = 0.0f;
1077 return tower;
1078 }
1079
1080 Tower *GetTowerByType(uint8_t towerType)
1081 {
1082 for (int i = 0; i < towerCount; i++)
1083 {
1084 if (towers[i].towerType == towerType)
1085 {
1086 return &towers[i];
1087 }
1088 }
1089 return 0;
1090 }
1091
1092 float TowerGetMaxHealth(Tower *tower)
1093 {
1094 switch (tower->towerType)
1095 {
1096 case TOWER_TYPE_BASE:
1097 return 10.0f;
1098 case TOWER_TYPE_GUN:
1099 return 3.0f;
1100 case TOWER_TYPE_WALL:
1101 return 5.0f;
1102 }
1103 return 0.0f;
1104 }
1105
1106 void TowerDraw()
1107 {
1108 for (int i = 0; i < towerCount; i++)
1109 {
1110 Tower tower = towers[i];
1111 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
1112 switch (tower.towerType)
1113 {
1114 case TOWER_TYPE_BASE:
1115 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
1116 break;
1117 case TOWER_TYPE_GUN:
1118 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
1119 break;
1120 case TOWER_TYPE_WALL:
1121 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
1122 break;
1123 }
1124 }
1125 }
1126
1127 void TowerGunUpdate(Tower *tower)
1128 {
1129 if (tower->cooldown <= 0)
1130 {
1131 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
1132 if (enemy)
1133 {
1134 tower->cooldown = 0.125f;
1135 // shoot the enemy; determine future position of the enemy
1136 float bulletSpeed = 1.0f;
1137 float bulletDamage = 3.0f;
1138 Vector2 velocity = enemy->simVelocity;
1139 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
1140 Vector2 towerPosition = {tower->x, tower->y};
1141 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
1142 for (int i = 0; i < 8; i++) {
1143 velocity = enemy->simVelocity;
1144 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
1145 float distance = Vector2Distance(towerPosition, futurePosition);
1146 float eta2 = distance / bulletSpeed;
1147 if (fabs(eta - eta2) < 0.01f) {
1148 break;
1149 }
1150 eta = (eta2 + eta) * 0.5f;
1151 }
1152 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
1153 bulletSpeed, bulletDamage);
1154 enemy->futureDamage += bulletDamage;
1155 }
1156 }
1157 else
1158 {
1159 tower->cooldown -= gameTime.deltaTime;
1160 }
1161 }
1162
1163 void TowerUpdate()
1164 {
1165 for (int i = 0; i < towerCount; i++)
1166 {
1167 Tower *tower = &towers[i];
1168 switch (tower->towerType)
1169 {
1170 case TOWER_TYPE_GUN:
1171 TowerGunUpdate(tower);
1172 break;
1173 }
1174 }
1175 }
1176
1177 //# Game
1178
1179 typedef enum LevelState
1180 {
1181 LEVEL_STATE_NONE,
1182 LEVEL_STATE_BUILDING,
1183 LEVEL_STATE_BATTLE,
1184 LEVEL_STATE_WON_WAVE,
1185 LEVEL_STATE_LOST_WAVE,
1186 LEVEL_STATE_WON_LEVEL,
1187 LEVEL_STATE_RESET,
1188 } LevelState;
1189
1190 typedef struct EnemyWave {
1191 uint8_t enemyType;
1192 uint8_t wave;
1193 uint16_t count;
1194 float interval;
1195 float delay;
1196 Vector2 spawnPosition;
1197
1198 uint16_t spawned;
1199 float timeToSpawnNext;
1200 } EnemyWave;
1201
1202 typedef struct Level
1203 {
1204 LevelState state;
1205 LevelState nextState;
1206 Camera3D camera;
1207 int placementMode;
1208
1209 EnemyWave waves[10];
1210 int currentWave;
1211 float waveEndTimer;
1212 } Level;
1213
1214 void InitLevel(Level *level)
1215 {
1216 TowerInit();
1217 EnemyInit();
1218 ProjectileInit();
1219 ParticleInit();
1220 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1221
1222 level->placementMode = 0;
1223 level->state = LEVEL_STATE_BUILDING;
1224 level->nextState = LEVEL_STATE_NONE;
1225
1226 Camera *camera = &level->camera;
1227 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
1228 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
1229 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
1230 camera->fovy = 45.0f;
1231 camera->projection = CAMERA_PERSPECTIVE;
1232 }
1233
1234 void DrawLevelReportLostWave(Level *level)
1235 {
1236 BeginMode3D(level->camera);
1237 DrawGrid(10, 1.0f);
1238 TowerDraw();
1239 EnemyDraw();
1240 ProjectileDraw();
1241 ParticleDraw();
1242 guiState.isBlocked = 0;
1243 EndMode3D();
1244
1245 const char *text = "Wave lost";
1246 int textWidth = MeasureText(text, 20);
1247 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1248
1249 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1250 {
1251 level->nextState = LEVEL_STATE_RESET;
1252 }
1253 }
1254
1255 int HasLevelNextWave(Level *level)
1256 {
1257 for (int i = 0; i < 10; i++)
1258 {
1259 EnemyWave *wave = &level->waves[i];
1260 if (wave->wave == level->currentWave)
1261 {
1262 return 1;
1263 }
1264 }
1265 return 0;
1266 }
1267
1268 void DrawLevelReportWonWave(Level *level)
1269 {
1270 BeginMode3D(level->camera);
1271 DrawGrid(10, 1.0f);
1272 TowerDraw();
1273 EnemyDraw();
1274 ProjectileDraw();
1275 ParticleDraw();
1276 guiState.isBlocked = 0;
1277 EndMode3D();
1278
1279 const char *text = "Wave won";
1280 int textWidth = MeasureText(text, 20);
1281 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1282
1283
1284 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1285 {
1286 level->nextState = LEVEL_STATE_RESET;
1287 }
1288
1289 if (HasLevelNextWave(level))
1290 {
1291 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
1292 {
1293 level->nextState = LEVEL_STATE_BUILDING;
1294 }
1295 }
1296 else {
1297 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
1298 {
1299 level->nextState = LEVEL_STATE_WON_LEVEL;
1300 }
1301 }
1302 }
1303
1304 void DrawLevelBuildingState(Level *level)
1305 {
1306 BeginMode3D(level->camera);
1307 DrawGrid(10, 1.0f);
1308 TowerDraw();
1309 EnemyDraw();
1310 ProjectileDraw();
1311 ParticleDraw();
1312
1313 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
1314 float planeDistance = ray.position.y / -ray.direction.y;
1315 float planeX = ray.direction.x * planeDistance + ray.position.x;
1316 float planeY = ray.direction.z * planeDistance + ray.position.z;
1317 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
1318 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
1319 if (level->placementMode && !guiState.isBlocked)
1320 {
1321 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
1322 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1323 {
1324 TowerTryAdd(level->placementMode, mapX, mapY);
1325 level->placementMode = TOWER_TYPE_NONE;
1326 }
1327 }
1328
1329 guiState.isBlocked = 0;
1330
1331 EndMode3D();
1332
1333 static ButtonState buildWallButtonState = {0};
1334 static ButtonState buildGunButtonState = {0};
1335 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
1336 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
1337
1338 if (Button("Wall", 10, 50, 80, 30, &buildWallButtonState))
1339 {
1340 level->placementMode = buildWallButtonState.isSelected ? 0 : TOWER_TYPE_WALL;
1341 }
1342 if (Button("Gun", 10, 90, 80, 30, &buildGunButtonState))
1343 {
1344 level->placementMode = buildGunButtonState.isSelected ? 0 : TOWER_TYPE_GUN;
1345 }
1346
1347 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1348 {
1349 level->nextState = LEVEL_STATE_RESET;
1350 }
1351
1352 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
1353 {
1354 level->nextState = LEVEL_STATE_BATTLE;
1355 }
1356
1357 const char *text = "Building phase";
1358 int textWidth = MeasureText(text, 20);
1359 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1360 }
1361
1362 void InitBattleStateConditions(Level *level)
1363 {
1364 level->state = LEVEL_STATE_BATTLE;
1365 level->nextState = LEVEL_STATE_NONE;
1366 level->waveEndTimer = 0.0f;
1367 for (int i = 0; i < 10; i++)
1368 {
1369 EnemyWave *wave = &level->waves[i];
1370 wave->spawned = 0;
1371 wave->timeToSpawnNext = wave->delay;
1372 }
1373 }
1374
1375 void DrawLevelBattleState(Level *level)
1376 {
1377 BeginMode3D(level->camera);
1378 DrawGrid(10, 1.0f);
1379 TowerDraw();
1380 EnemyDraw();
1381 ProjectileDraw();
1382 ParticleDraw();
1383 guiState.isBlocked = 0;
1384 EndMode3D();
1385
1386 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1387 {
1388 level->nextState = LEVEL_STATE_RESET;
1389 }
1390
1391 int maxCount = 0;
1392 int remainingCount = 0;
1393 for (int i = 0; i < 10; i++)
1394 {
1395 EnemyWave *wave = &level->waves[i];
1396 if (wave->wave != level->currentWave)
1397 {
1398 continue;
1399 }
1400 maxCount += wave->count;
1401 remainingCount += wave->count - wave->spawned;
1402 }
1403 int aliveCount = EnemyCount();
1404 remainingCount += aliveCount;
1405
1406 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1407 int textWidth = MeasureText(text, 20);
1408 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1409 }
1410
1411 void DrawLevel(Level *level)
1412 {
1413 switch (level->state)
1414 {
1415 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1416 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1417 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
1418 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
1419 default: break;
1420 }
1421 }
1422
1423 void UpdateLevel(Level *level)
1424 {
1425 if (level->state == LEVEL_STATE_BATTLE)
1426 {
1427 int activeWaves = 0;
1428 for (int i = 0; i < 10; i++)
1429 {
1430 EnemyWave *wave = &level->waves[i];
1431 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1432 {
1433 continue;
1434 }
1435 activeWaves++;
1436 wave->timeToSpawnNext -= gameTime.deltaTime;
1437 if (wave->timeToSpawnNext <= 0.0f)
1438 {
1439 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1440 if (enemy)
1441 {
1442 wave->timeToSpawnNext = wave->interval;
1443 wave->spawned++;
1444 }
1445 }
1446 }
1447 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
1448 level->waveEndTimer += gameTime.deltaTime;
1449 if (level->waveEndTimer >= 2.0f)
1450 {
1451 level->nextState = LEVEL_STATE_LOST_WAVE;
1452 }
1453 }
1454 else if (activeWaves == 0 && EnemyCount() == 0)
1455 {
1456 level->waveEndTimer += gameTime.deltaTime;
1457 if (level->waveEndTimer >= 2.0f)
1458 {
1459 level->nextState = LEVEL_STATE_WON_WAVE;
1460 }
1461 }
1462 }
1463
1464 PathFindingMapUpdate();
1465 EnemyUpdate();
1466 TowerUpdate();
1467 ProjectileUpdate();
1468 ParticleUpdate();
1469
1470 if (level->nextState == LEVEL_STATE_RESET)
1471 {
1472 InitLevel(level);
1473 }
1474
1475 if (level->nextState == LEVEL_STATE_BATTLE)
1476 {
1477 InitBattleStateConditions(level);
1478 }
1479
1480 if (level->nextState == LEVEL_STATE_WON_WAVE)
1481 {
1482 level->currentWave++;
1483 level->state = LEVEL_STATE_WON_WAVE;
1484 }
1485
1486 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1487 {
1488 level->state = LEVEL_STATE_LOST_WAVE;
1489 }
1490
1491 if (level->nextState == LEVEL_STATE_BUILDING)
1492 {
1493 level->state = LEVEL_STATE_BUILDING;
1494 }
1495
1496 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1497 {
1498 // make something of this later
1499 InitLevel(level);
1500 }
1501
1502 level->nextState = LEVEL_STATE_NONE;
1503 }
1504
1505 Level levels[] = {
1506 [0] = {
1507 .state = LEVEL_STATE_BUILDING,
1508 .waves[0] = {
1509 .enemyType = ENEMY_TYPE_MINION,
1510 .wave = 0,
1511 .count = 10,
1512 .interval = 1.0f,
1513 .delay = 1.0f,
1514 .spawnPosition = {0, 6},
1515 },
1516 },
1517 };
1518
1519 Level *currentLevel = levels;
1520
1521 float nextSpawnTime = 0.0f;
1522
1523 void ResetGame()
1524 {
1525 InitLevel(currentLevel);
1526 }
1527
1528 void InitGame()
1529 {
1530 TowerInit();
1531 EnemyInit();
1532 ProjectileInit();
1533 ParticleInit();
1534 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1535
1536 currentLevel = levels;
1537 InitLevel(currentLevel);
1538 }
1539
1540 //# Immediate GUI functions
1541
1542 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1543 {
1544 Rectangle bounds = {x, y, width, height};
1545 int isPressed = 0;
1546 int isSelected = state && state->isSelected;
1547 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked)
1548 {
1549 Color color = isSelected ? DARKGRAY : GRAY;
1550 DrawRectangle(x, y, width, height, color);
1551 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1552 {
1553 isPressed = 1;
1554 }
1555 guiState.isBlocked = 1;
1556 }
1557 else
1558 {
1559 Color color = isSelected ? WHITE : LIGHTGRAY;
1560 DrawRectangle(x, y, width, height, color);
1561 }
1562 Font font = GetFontDefault();
1563 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1564 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, BLACK);
1565 return isPressed;
1566 }
1567
1568 //# Main game loop
1569
1570 void GameUpdate()
1571 {
1572 float dt = GetFrameTime();
1573 // cap maximum delta time to 0.1 seconds to prevent large time steps
1574 if (dt > 0.1f) dt = 0.1f;
1575 gameTime.time += dt;
1576 gameTime.deltaTime = dt;
1577
1578 UpdateLevel(currentLevel);
1579 }
1580
1581 int main(void)
1582 {
1583 int screenWidth, screenHeight;
1584 GetPreferredSize(&screenWidth, &screenHeight);
1585 InitWindow(screenWidth, screenHeight, "Tower defense");
1586 SetTargetFPS(30);
1587
1588 InitGame();
1589
1590 while (!WindowShouldClose())
1591 {
1592 if (IsPaused()) {
1593 // canvas is not visible in browser - do nothing
1594 continue;
1595 }
1596
1597 BeginDrawing();
1598 ClearBackground(DARKBLUE);
1599
1600 GameUpdate();
1601 DrawLevel(currentLevel);
1602
1603 EndDrawing();
1604 }
1605
1606 CloseWindow();
1607
1608 return 0;
1609 }
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 single wave that can be defeated or lost against. The game will show a screen that could be the report of the battle result, but right now it effectively only offers buttons to restart the level since there's right now only one wave that's been configured.
The next logical step should be to add multiple waves and a resource system that the game is tracking. We'll store the resources in the level struct. Additionally, we want to see the current amount of resources and the costs of the towers when we're in the building phase:
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 ENEMY_MAX_PATH_COUNT 8
9 #define ENEMY_MAX_COUNT 400
10 #define ENEMY_TYPE_NONE 0
11 #define ENEMY_TYPE_MINION 1
12
13 #define PARTICLE_MAX_COUNT 400
14 #define PARTICLE_TYPE_NONE 0
15 #define PARTICLE_TYPE_EXPLOSION 1
16
17 typedef struct Particle
18 {
19 uint8_t particleType;
20 float spawnTime;
21 float lifetime;
22 Vector3 position;
23 Vector3 velocity;
24 } Particle;
25
26 Particle particles[PARTICLE_MAX_COUNT];
27 int particleCount = 0;
28
29 #define TOWER_MAX_COUNT 400
30 #define TOWER_TYPE_NONE 0
31 #define TOWER_TYPE_BASE 1
32 #define TOWER_TYPE_GUN 2
33 #define TOWER_TYPE_WALL 3
34
35 typedef struct Tower
36 {
37 int16_t x, y;
38 uint8_t towerType;
39 float cooldown;
40 float damage;
41 } Tower;
42
43 typedef struct GameTime
44 {
45 float time;
46 float deltaTime;
47 } GameTime;
48
49 typedef struct ButtonState {
50 char isSelected;
51 char isDisabled;
52 } ButtonState;
53
54 typedef struct GUIState {
55 int isBlocked;
56 } GUIState;
57
58 GUIState guiState = {0};
59 GameTime gameTime = {0};
60 Tower towers[TOWER_MAX_COUNT];
61 int towerCount = 0;
62
63 typedef enum LevelState
64 {
65 LEVEL_STATE_NONE,
66 LEVEL_STATE_BUILDING,
67 LEVEL_STATE_BATTLE,
68 LEVEL_STATE_WON_WAVE,
69 LEVEL_STATE_LOST_WAVE,
70 LEVEL_STATE_WON_LEVEL,
71 LEVEL_STATE_RESET,
72 } LevelState;
73
74 typedef struct EnemyWave {
75 uint8_t enemyType;
76 uint8_t wave;
77 uint16_t count;
78 float interval;
79 float delay;
80 Vector2 spawnPosition;
81
82 uint16_t spawned;
83 float timeToSpawnNext;
84 } EnemyWave;
85
86 typedef struct Level
87 {
88 LevelState state;
89 LevelState nextState;
90 Camera3D camera;
91 int placementMode;
92
93 int initialGold;
94 int playerGold;
95
96 EnemyWave waves[10];
97 int currentWave;
98 float waveEndTimer;
99 } Level;
100
101 Level levels[] = {
102 [0] = {
103 .state = LEVEL_STATE_BUILDING,
104 .initialGold = 10,
105 .waves[0] = {
106 .enemyType = ENEMY_TYPE_MINION,
107 .wave = 0,
108 .count = 10,
109 .interval = 1.0f,
110 .delay = 1.0f,
111 .spawnPosition = {0, 6},
112 },
113 .waves[1] = {
114 .enemyType = ENEMY_TYPE_MINION,
115 .wave = 1,
116 .count = 20,
117 .interval = 0.5f,
118 .delay = 1.0f,
119 .spawnPosition = {0, 6},
120 },
121 .waves[2] = {
122 .enemyType = ENEMY_TYPE_MINION,
123 .wave = 2,
124 .count = 30,
125 .interval = 0.25f,
126 .delay = 1.0f,
127 .spawnPosition = {0, 6},
128 }
129 },
130 };
131
132 Level *currentLevel = levels;
133
134 float TowerGetMaxHealth(Tower *tower);
135 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
136
137 //# Particle system
138
139 void ParticleInit()
140 {
141 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
142 {
143 particles[i] = (Particle){0};
144 }
145 particleCount = 0;
146 }
147
148 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
149 {
150 if (particleCount >= PARTICLE_MAX_COUNT)
151 {
152 return;
153 }
154
155 int index = -1;
156 for (int i = 0; i < particleCount; i++)
157 {
158 if (particles[i].particleType == PARTICLE_TYPE_NONE)
159 {
160 index = i;
161 break;
162 }
163 }
164
165 if (index == -1)
166 {
167 index = particleCount++;
168 }
169
170 Particle *particle = &particles[index];
171 particle->particleType = particleType;
172 particle->spawnTime = gameTime.time;
173 particle->lifetime = lifetime;
174 particle->position = position;
175 particle->velocity = velocity;
176 }
177
178 void ParticleUpdate()
179 {
180 for (int i = 0; i < particleCount; i++)
181 {
182 Particle *particle = &particles[i];
183 if (particle->particleType == PARTICLE_TYPE_NONE)
184 {
185 continue;
186 }
187
188 float age = gameTime.time - particle->spawnTime;
189
190 if (particle->lifetime > age)
191 {
192 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
193 }
194 else {
195 particle->particleType = PARTICLE_TYPE_NONE;
196 }
197 }
198 }
199
200 void DrawExplosionParticle(Particle *particle, float transition)
201 {
202 float size = 1.2f * (1.0f - transition);
203 Color startColor = WHITE;
204 Color endColor = RED;
205 Color color = ColorLerp(startColor, endColor, transition);
206 DrawCube(particle->position, size, size, size, color);
207 }
208
209 void ParticleDraw()
210 {
211 for (int i = 0; i < particleCount; i++)
212 {
213 Particle particle = particles[i];
214 if (particle.particleType == PARTICLE_TYPE_NONE)
215 {
216 continue;
217 }
218
219 float age = gameTime.time - particle.spawnTime;
220 float transition = age / particle.lifetime;
221 switch (particle.particleType)
222 {
223 case PARTICLE_TYPE_EXPLOSION:
224 DrawExplosionParticle(&particle, transition);
225 break;
226 default:
227 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
228 break;
229 }
230 }
231 }
232
233 //# Pathfinding map
234 typedef struct DeltaSrc
235 {
236 char x, y;
237 } DeltaSrc;
238
239 typedef struct PathfindingMap
240 {
241 int width, height;
242 float scale;
243 float *distances;
244 long *towerIndex;
245 DeltaSrc *deltaSrc;
246 float maxDistance;
247 Matrix toMapSpace;
248 Matrix toWorldSpace;
249 } PathfindingMap;
250
251 // when we execute the pathfinding algorithm, we need to store the active nodes
252 // in a queue. Each node has a position, a distance from the start, and the
253 // position of the node that we came from.
254 typedef struct PathfindingNode
255 {
256 int16_t x, y, fromX, fromY;
257 float distance;
258 } PathfindingNode;
259
260 // The queue is a simple array of nodes, we add nodes to the end and remove
261 // nodes from the front. We keep the array around to avoid unnecessary allocations
262 static PathfindingNode *pathfindingNodeQueue = 0;
263 static int pathfindingNodeQueueCount = 0;
264 static int pathfindingNodeQueueCapacity = 0;
265
266 // The pathfinding map stores the distances from the castle to each cell in the map.
267 PathfindingMap pathfindingMap = {0};
268
269 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
270 {
271 // transforming between map space and world space allows us to adapt
272 // position and scale of the map without changing the pathfinding data
273 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
274 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
275 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
276 pathfindingMap.width = width;
277 pathfindingMap.height = height;
278 pathfindingMap.scale = scale;
279 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
280 for (int i = 0; i < width * height; i++)
281 {
282 pathfindingMap.distances[i] = -1.0f;
283 }
284
285 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
286 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
287 }
288
289 float PathFindingGetDistance(int mapX, int mapY)
290 {
291 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
292 {
293 // when outside the map, we return the manhattan distance to the castle (0,0)
294 return fabsf((float)mapX) + fabsf((float)mapY);
295 }
296
297 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
298 }
299
300 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
301 {
302 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
303 {
304 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
305 // we use MemAlloc/MemRealloc to allocate memory for the queue
306 // I am not entirely sure if MemRealloc allows passing a null pointer
307 // so we check if the pointer is null and use MemAlloc in that case
308 if (pathfindingNodeQueue == 0)
309 {
310 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
311 }
312 else
313 {
314 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
315 }
316 }
317
318 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
319 node->x = x;
320 node->y = y;
321 node->fromX = fromX;
322 node->fromY = fromY;
323 node->distance = distance;
324 }
325
326 PathfindingNode *PathFindingNodePop()
327 {
328 if (pathfindingNodeQueueCount == 0)
329 {
330 return 0;
331 }
332 // we return the first node in the queue; we want to return a pointer to the node
333 // so we can return 0 if the queue is empty.
334 // We should _not_ return a pointer to the element in the list, because the list
335 // may be reallocated and the pointer would become invalid. Or the
336 // popped element is overwritten by the next push operation.
337 // Using static here means that the variable is permanently allocated.
338 static PathfindingNode node;
339 node = pathfindingNodeQueue[0];
340 // we shift all nodes one position to the front
341 for (int i = 1; i < pathfindingNodeQueueCount; i++)
342 {
343 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
344 }
345 --pathfindingNodeQueueCount;
346 return &node;
347 }
348
349 // transform a world position to a map position in the array;
350 // returns true if the position is inside the map
351 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
352 {
353 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
354 *mapX = (int16_t)mapPosition.x;
355 *mapY = (int16_t)mapPosition.z;
356 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
357 }
358
359 void PathFindingMapUpdate()
360 {
361 const int castleX = 0, castleY = 0;
362 int16_t castleMapX, castleMapY;
363 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
364 {
365 return;
366 }
367 int width = pathfindingMap.width, height = pathfindingMap.height;
368
369 // reset the distances to -1
370 for (int i = 0; i < width * height; i++)
371 {
372 pathfindingMap.distances[i] = -1.0f;
373 }
374 // reset the tower indices
375 for (int i = 0; i < width * height; i++)
376 {
377 pathfindingMap.towerIndex[i] = -1;
378 }
379 // reset the delta src
380 for (int i = 0; i < width * height; i++)
381 {
382 pathfindingMap.deltaSrc[i].x = 0;
383 pathfindingMap.deltaSrc[i].y = 0;
384 }
385
386 for (int i = 0; i < towerCount; i++)
387 {
388 Tower *tower = &towers[i];
389 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
390 {
391 continue;
392 }
393 int16_t mapX, mapY;
394 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
395 // this would not work correctly and needs to be refined to allow towers covering multiple cells
396 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
397 // one cell. For now.
398 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
399 {
400 continue;
401 }
402 int index = mapY * width + mapX;
403 pathfindingMap.towerIndex[index] = i;
404 }
405
406 // we start at the castle and add the castle to the queue
407 pathfindingMap.maxDistance = 0.0f;
408 pathfindingNodeQueueCount = 0;
409 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
410 PathfindingNode *node = 0;
411 while ((node = PathFindingNodePop()))
412 {
413 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
414 {
415 continue;
416 }
417 int index = node->y * width + node->x;
418 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
419 {
420 continue;
421 }
422
423 int deltaX = node->x - node->fromX;
424 int deltaY = node->y - node->fromY;
425 // even if the cell is blocked by a tower, we still may want to store the direction
426 // (though this might not be needed, IDK right now)
427 pathfindingMap.deltaSrc[index].x = (char) deltaX;
428 pathfindingMap.deltaSrc[index].y = (char) deltaY;
429
430 // we skip nodes that are blocked by towers
431 if (pathfindingMap.towerIndex[index] >= 0)
432 {
433 node->distance += 8.0f;
434 }
435 pathfindingMap.distances[index] = node->distance;
436 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
437 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
438 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
439 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
440 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
441 }
442 }
443
444 void PathFindingMapDraw()
445 {
446 float cellSize = pathfindingMap.scale * 0.9f;
447 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
448 for (int x = 0; x < pathfindingMap.width; x++)
449 {
450 for (int y = 0; y < pathfindingMap.height; y++)
451 {
452 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
453 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
454 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
455 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
456 // animate the distance "wave" to show how the pathfinding algorithm expands
457 // from the castle
458 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
459 {
460 color = BLACK;
461 }
462 DrawCube(position, cellSize, 0.1f, cellSize, color);
463 }
464 }
465 }
466
467 Vector2 PathFindingGetGradient(Vector3 world)
468 {
469 int16_t mapX, mapY;
470 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
471 {
472 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
473 return (Vector2){(float)-delta.x, (float)-delta.y};
474 }
475 // fallback to a simple gradient calculation
476 float n = PathFindingGetDistance(mapX, mapY - 1);
477 float s = PathFindingGetDistance(mapX, mapY + 1);
478 float w = PathFindingGetDistance(mapX - 1, mapY);
479 float e = PathFindingGetDistance(mapX + 1, mapY);
480 return (Vector2){w - e + 0.25f, n - s + 0.125f};
481 }
482
483 //# Enemies
484
485 typedef struct EnemyId
486 {
487 uint16_t index;
488 uint16_t generation;
489 } EnemyId;
490
491 typedef struct EnemyClassConfig
492 {
493 float speed;
494 float health;
495 float radius;
496 float maxAcceleration;
497 float requiredContactTime;
498 float explosionDamage;
499 float explosionRange;
500 float explosionPushbackPower;
501 int goldValue;
502 } EnemyClassConfig;
503
504 typedef struct Enemy
505 {
506 int16_t currentX, currentY;
507 int16_t nextX, nextY;
508 Vector2 simPosition;
509 Vector2 simVelocity;
510 uint16_t generation;
511 float startMovingTime;
512 float damage, futureDamage;
513 float contactTime;
514 uint8_t enemyType;
515 uint8_t movePathCount;
516 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
517 } Enemy;
518
519 Enemy enemies[ENEMY_MAX_COUNT];
520 int enemyCount = 0;
521
522 EnemyClassConfig enemyClassConfigs[] = {
523 [ENEMY_TYPE_MINION] = {
524 .health = 3.0f,
525 .speed = 1.0f,
526 .radius = 0.25f,
527 .maxAcceleration = 1.0f,
528 .explosionDamage = 1.0f,
529 .requiredContactTime = 0.5f,
530 .explosionRange = 1.0f,
531 .explosionPushbackPower = 0.25f,
532 .goldValue = 1,
533 },
534 };
535
536 int EnemyAddDamage(Enemy *enemy, float damage);
537
538 void EnemyInit()
539 {
540 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
541 {
542 enemies[i] = (Enemy){0};
543 }
544 enemyCount = 0;
545 }
546
547 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
548 {
549 return enemyClassConfigs[enemy->enemyType].speed;
550 }
551
552 float EnemyGetMaxHealth(Enemy *enemy)
553 {
554 return enemyClassConfigs[enemy->enemyType].health;
555 }
556
557 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
558 {
559 int16_t castleX = 0;
560 int16_t castleY = 0;
561 int16_t dx = castleX - currentX;
562 int16_t dy = castleY - currentY;
563 if (dx == 0 && dy == 0)
564 {
565 *nextX = currentX;
566 *nextY = currentY;
567 return 1;
568 }
569 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
570
571 if (gradient.x == 0 && gradient.y == 0)
572 {
573 *nextX = currentX;
574 *nextY = currentY;
575 return 1;
576 }
577
578 if (fabsf(gradient.x) > fabsf(gradient.y))
579 {
580 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
581 *nextY = currentY;
582 return 0;
583 }
584 *nextX = currentX;
585 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
586 return 0;
587 }
588
589
590 // this function predicts the movement of the unit for the next deltaT seconds
591 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
592 {
593 const float pointReachedDistance = 0.25f;
594 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
595 const float maxSimStepTime = 0.015625f;
596
597 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
598 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
599 int16_t nextX = enemy->nextX;
600 int16_t nextY = enemy->nextY;
601 Vector2 position = enemy->simPosition;
602 int passedCount = 0;
603 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
604 {
605 float stepTime = fminf(deltaT - t, maxSimStepTime);
606 Vector2 target = (Vector2){nextX, nextY};
607 float speed = Vector2Length(*velocity);
608 // draw the target position for debugging
609 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
610 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
611 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
612 {
613 // we reached the target position, let's move to the next waypoint
614 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
615 target = (Vector2){nextX, nextY};
616 // track how many waypoints we passed
617 passedCount++;
618 }
619
620 // acceleration towards the target
621 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
622 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
623 *velocity = Vector2Add(*velocity, acceleration);
624
625 // limit the speed to the maximum speed
626 if (speed > maxSpeed)
627 {
628 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
629 }
630
631 // move the enemy
632 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
633 }
634
635 if (waypointPassedCount)
636 {
637 (*waypointPassedCount) = passedCount;
638 }
639
640 return position;
641 }
642
643 void EnemyDraw()
644 {
645 for (int i = 0; i < enemyCount; i++)
646 {
647 Enemy enemy = enemies[i];
648 if (enemy.enemyType == ENEMY_TYPE_NONE)
649 {
650 continue;
651 }
652
653 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
654
655 if (enemy.movePathCount > 0)
656 {
657 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
658 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
659 }
660 for (int j = 1; j < enemy.movePathCount; j++)
661 {
662 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
663 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
664 DrawLine3D(p, q, GREEN);
665 }
666
667 switch (enemy.enemyType)
668 {
669 case ENEMY_TYPE_MINION:
670 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
671 break;
672 }
673 }
674 }
675
676 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
677 {
678 // damage the tower
679 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
680 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
681 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
682 float explosionRange2 = explosionRange * explosionRange;
683 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
684 // explode the enemy
685 if (tower->damage >= TowerGetMaxHealth(tower))
686 {
687 tower->towerType = TOWER_TYPE_NONE;
688 }
689
690 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
691 explosionSource,
692 (Vector3){0, 0.1f, 0}, 1.0f);
693
694 enemy->enemyType = ENEMY_TYPE_NONE;
695
696 // push back enemies & dealing damage
697 for (int i = 0; i < enemyCount; i++)
698 {
699 Enemy *other = &enemies[i];
700 if (other->enemyType == ENEMY_TYPE_NONE)
701 {
702 continue;
703 }
704 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
705 if (distanceSqr > 0 && distanceSqr < explosionRange2)
706 {
707 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
708 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
709 EnemyAddDamage(other, explosionDamge);
710 }
711 }
712 }
713
714 void EnemyUpdate()
715 {
716 const float castleX = 0;
717 const float castleY = 0;
718 const float maxPathDistance2 = 0.25f * 0.25f;
719
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
728 int waypointPassedCount = 0;
729 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
730 enemy->startMovingTime = gameTime.time;
731 // track path of unit
732 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
733 {
734 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
735 {
736 enemy->movePath[j] = enemy->movePath[j - 1];
737 }
738 enemy->movePath[0] = enemy->simPosition;
739 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
740 {
741 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
742 }
743 }
744
745 if (waypointPassedCount > 0)
746 {
747 enemy->currentX = enemy->nextX;
748 enemy->currentY = enemy->nextY;
749 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
750 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
751 {
752 // enemy reached the castle; remove it
753 enemy->enemyType = ENEMY_TYPE_NONE;
754 continue;
755 }
756 }
757 }
758
759 // handle collisions between enemies
760 for (int i = 0; i < enemyCount - 1; i++)
761 {
762 Enemy *enemyA = &enemies[i];
763 if (enemyA->enemyType == ENEMY_TYPE_NONE)
764 {
765 continue;
766 }
767 for (int j = i + 1; j < enemyCount; j++)
768 {
769 Enemy *enemyB = &enemies[j];
770 if (enemyB->enemyType == ENEMY_TYPE_NONE)
771 {
772 continue;
773 }
774 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
775 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
776 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
777 float radiusSum = radiusA + radiusB;
778 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
779 {
780 // collision
781 float distance = sqrtf(distanceSqr);
782 float overlap = radiusSum - distance;
783 // move the enemies apart, but softly; if we have a clog of enemies,
784 // moving them perfectly apart can cause them to jitter
785 float positionCorrection = overlap / 5.0f;
786 Vector2 direction = (Vector2){
787 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
788 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
789 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
790 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
791 }
792 }
793 }
794
795 // handle collisions between enemies and towers
796 for (int i = 0; i < enemyCount; i++)
797 {
798 Enemy *enemy = &enemies[i];
799 if (enemy->enemyType == ENEMY_TYPE_NONE)
800 {
801 continue;
802 }
803 enemy->contactTime -= gameTime.deltaTime;
804 if (enemy->contactTime < 0.0f)
805 {
806 enemy->contactTime = 0.0f;
807 }
808
809 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
810 // linear search over towers; could be optimized by using path finding tower map,
811 // but for now, we keep it simple
812 for (int j = 0; j < towerCount; j++)
813 {
814 Tower *tower = &towers[j];
815 if (tower->towerType == TOWER_TYPE_NONE)
816 {
817 continue;
818 }
819 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
820 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
821 if (distanceSqr > combinedRadius * combinedRadius)
822 {
823 continue;
824 }
825 // potential collision; square / circle intersection
826 float dx = tower->x - enemy->simPosition.x;
827 float dy = tower->y - enemy->simPosition.y;
828 float absDx = fabsf(dx);
829 float absDy = fabsf(dy);
830 Vector3 contactPoint = {0};
831 if (absDx <= 0.5f && absDx <= absDy) {
832 // vertical collision; push the enemy out horizontally
833 float overlap = enemyRadius + 0.5f - absDy;
834 if (overlap < 0.0f)
835 {
836 continue;
837 }
838 float direction = dy > 0.0f ? -1.0f : 1.0f;
839 enemy->simPosition.y += direction * overlap;
840 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
841 }
842 else if (absDy <= 0.5f && absDy <= absDx)
843 {
844 // horizontal collision; push the enemy out vertically
845 float overlap = enemyRadius + 0.5f - absDx;
846 if (overlap < 0.0f)
847 {
848 continue;
849 }
850 float direction = dx > 0.0f ? -1.0f : 1.0f;
851 enemy->simPosition.x += direction * overlap;
852 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
853 }
854 else
855 {
856 // possible collision with a corner
857 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
858 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
859 float cornerX = tower->x + cornerDX;
860 float cornerY = tower->y + cornerDY;
861 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
862 if (cornerDistanceSqr > enemyRadius * enemyRadius)
863 {
864 continue;
865 }
866 // push the enemy out along the diagonal
867 float cornerDistance = sqrtf(cornerDistanceSqr);
868 float overlap = enemyRadius - cornerDistance;
869 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
870 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
871 enemy->simPosition.x -= directionX * overlap;
872 enemy->simPosition.y -= directionY * overlap;
873 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
874 }
875
876 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
877 {
878 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
879 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
880 {
881 EnemyTriggerExplode(enemy, tower, contactPoint);
882 }
883 }
884 }
885 }
886 }
887
888 EnemyId EnemyGetId(Enemy *enemy)
889 {
890 return (EnemyId){enemy - enemies, enemy->generation};
891 }
892
893 Enemy *EnemyTryResolve(EnemyId enemyId)
894 {
895 if (enemyId.index >= ENEMY_MAX_COUNT)
896 {
897 return 0;
898 }
899 Enemy *enemy = &enemies[enemyId.index];
900 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
901 {
902 return 0;
903 }
904 return enemy;
905 }
906
907 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
908 {
909 Enemy *spawn = 0;
910 for (int i = 0; i < enemyCount; i++)
911 {
912 Enemy *enemy = &enemies[i];
913 if (enemy->enemyType == ENEMY_TYPE_NONE)
914 {
915 spawn = enemy;
916 break;
917 }
918 }
919
920 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
921 {
922 spawn = &enemies[enemyCount++];
923 }
924
925 if (spawn)
926 {
927 spawn->currentX = currentX;
928 spawn->currentY = currentY;
929 spawn->nextX = currentX;
930 spawn->nextY = currentY;
931 spawn->simPosition = (Vector2){currentX, currentY};
932 spawn->simVelocity = (Vector2){0, 0};
933 spawn->enemyType = enemyType;
934 spawn->startMovingTime = gameTime.time;
935 spawn->damage = 0.0f;
936 spawn->futureDamage = 0.0f;
937 spawn->generation++;
938 spawn->movePathCount = 0;
939 }
940
941 return spawn;
942 }
943
944 int EnemyAddDamage(Enemy *enemy, float damage)
945 {
946 enemy->damage += damage;
947 if (enemy->damage >= EnemyGetMaxHealth(enemy))
948 {
949 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
950 enemy->enemyType = ENEMY_TYPE_NONE;
951 return 1;
952 }
953
954 return 0;
955 }
956
957 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
958 {
959 int16_t castleX = 0;
960 int16_t castleY = 0;
961 Enemy* closest = 0;
962 int16_t closestDistance = 0;
963 float range2 = range * range;
964 for (int i = 0; i < enemyCount; i++)
965 {
966 Enemy* enemy = &enemies[i];
967 if (enemy->enemyType == ENEMY_TYPE_NONE)
968 {
969 continue;
970 }
971 float maxHealth = EnemyGetMaxHealth(enemy);
972 if (enemy->futureDamage >= maxHealth)
973 {
974 // ignore enemies that will die soon
975 continue;
976 }
977 int16_t dx = castleX - enemy->currentX;
978 int16_t dy = castleY - enemy->currentY;
979 int16_t distance = abs(dx) + abs(dy);
980 if (!closest || distance < closestDistance)
981 {
982 float tdx = towerX - enemy->currentX;
983 float tdy = towerY - enemy->currentY;
984 float tdistance2 = tdx * tdx + tdy * tdy;
985 if (tdistance2 <= range2)
986 {
987 closest = enemy;
988 closestDistance = distance;
989 }
990 }
991 }
992 return closest;
993 }
994
995 int EnemyCount()
996 {
997 int count = 0;
998 for (int i = 0; i < enemyCount; i++)
999 {
1000 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
1001 {
1002 count++;
1003 }
1004 }
1005 return count;
1006 }
1007
1008 //# Projectiles
1009 #define PROJECTILE_MAX_COUNT 1200
1010 #define PROJECTILE_TYPE_NONE 0
1011 #define PROJECTILE_TYPE_BULLET 1
1012
1013 typedef struct Projectile
1014 {
1015 uint8_t projectileType;
1016 float shootTime;
1017 float arrivalTime;
1018 float damage;
1019 Vector2 position;
1020 Vector2 target;
1021 Vector2 directionNormal;
1022 EnemyId targetEnemy;
1023 } Projectile;
1024
1025 Projectile projectiles[PROJECTILE_MAX_COUNT];
1026 int projectileCount = 0;
1027
1028 void ProjectileInit()
1029 {
1030 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
1031 {
1032 projectiles[i] = (Projectile){0};
1033 }
1034 }
1035
1036 void ProjectileDraw()
1037 {
1038 for (int i = 0; i < projectileCount; i++)
1039 {
1040 Projectile projectile = projectiles[i];
1041 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
1042 {
1043 continue;
1044 }
1045 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
1046 if (transition >= 1.0f)
1047 {
1048 continue;
1049 }
1050 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
1051 float x = position.x;
1052 float y = position.y;
1053 float dx = projectile.directionNormal.x;
1054 float dy = projectile.directionNormal.y;
1055 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
1056 {
1057 x -= dx * 0.1f;
1058 y -= dy * 0.1f;
1059 float size = 0.1f * d;
1060 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
1061 }
1062 }
1063 }
1064
1065 void ProjectileUpdate()
1066 {
1067 for (int i = 0; i < projectileCount; i++)
1068 {
1069 Projectile *projectile = &projectiles[i];
1070 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
1071 {
1072 continue;
1073 }
1074 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
1075 if (transition >= 1.0f)
1076 {
1077 projectile->projectileType = PROJECTILE_TYPE_NONE;
1078 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
1079 if (enemy)
1080 {
1081 EnemyAddDamage(enemy, projectile->damage);
1082 }
1083 continue;
1084 }
1085 }
1086 }
1087
1088 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
1089 {
1090 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
1091 {
1092 Projectile *projectile = &projectiles[i];
1093 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
1094 {
1095 projectile->projectileType = projectileType;
1096 projectile->shootTime = gameTime.time;
1097 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
1098 projectile->damage = damage;
1099 projectile->position = position;
1100 projectile->target = target;
1101 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
1102 projectile->targetEnemy = EnemyGetId(enemy);
1103 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
1104 return projectile;
1105 }
1106 }
1107 return 0;
1108 }
1109
1110 //# Towers
1111
1112 void TowerInit()
1113 {
1114 for (int i = 0; i < TOWER_MAX_COUNT; i++)
1115 {
1116 towers[i] = (Tower){0};
1117 }
1118 towerCount = 0;
1119 }
1120
1121 Tower *TowerGetAt(int16_t x, int16_t y)
1122 {
1123 for (int i = 0; i < towerCount; i++)
1124 {
1125 if (towers[i].x == x && towers[i].y == y)
1126 {
1127 return &towers[i];
1128 }
1129 }
1130 return 0;
1131 }
1132
1133 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
1134 {
1135 if (towerCount >= TOWER_MAX_COUNT)
1136 {
1137 return 0;
1138 }
1139
1140 Tower *tower = TowerGetAt(x, y);
1141 if (tower)
1142 {
1143 return 0;
1144 }
1145
1146 tower = &towers[towerCount++];
1147 tower->x = x;
1148 tower->y = y;
1149 tower->towerType = towerType;
1150 tower->cooldown = 0.0f;
1151 tower->damage = 0.0f;
1152 return tower;
1153 }
1154
1155 Tower *GetTowerByType(uint8_t towerType)
1156 {
1157 for (int i = 0; i < towerCount; i++)
1158 {
1159 if (towers[i].towerType == towerType)
1160 {
1161 return &towers[i];
1162 }
1163 }
1164 return 0;
1165 }
1166
1167 int GetTowerCosts(uint8_t towerType)
1168 {
1169 switch (towerType)
1170 {
1171 case TOWER_TYPE_BASE:
1172 return 0;
1173 case TOWER_TYPE_GUN:
1174 return 6;
1175 case TOWER_TYPE_WALL:
1176 return 2;
1177 }
1178 return 0;
1179 }
1180
1181 float TowerGetMaxHealth(Tower *tower)
1182 {
1183 switch (tower->towerType)
1184 {
1185 case TOWER_TYPE_BASE:
1186 return 10.0f;
1187 case TOWER_TYPE_GUN:
1188 return 3.0f;
1189 case TOWER_TYPE_WALL:
1190 return 5.0f;
1191 }
1192 return 0.0f;
1193 }
1194
1195 void TowerDraw()
1196 {
1197 for (int i = 0; i < towerCount; i++)
1198 {
1199 Tower tower = towers[i];
1200 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
1201 switch (tower.towerType)
1202 {
1203 case TOWER_TYPE_BASE:
1204 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
1205 break;
1206 case TOWER_TYPE_GUN:
1207 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
1208 break;
1209 case TOWER_TYPE_WALL:
1210 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
1211 break;
1212 }
1213 }
1214 }
1215
1216 void TowerGunUpdate(Tower *tower)
1217 {
1218 if (tower->cooldown <= 0)
1219 {
1220 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
1221 if (enemy)
1222 {
1223 tower->cooldown = 0.125f;
1224 // shoot the enemy; determine future position of the enemy
1225 float bulletSpeed = 1.0f;
1226 float bulletDamage = 3.0f;
1227 Vector2 velocity = enemy->simVelocity;
1228 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
1229 Vector2 towerPosition = {tower->x, tower->y};
1230 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
1231 for (int i = 0; i < 8; i++) {
1232 velocity = enemy->simVelocity;
1233 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
1234 float distance = Vector2Distance(towerPosition, futurePosition);
1235 float eta2 = distance / bulletSpeed;
1236 if (fabs(eta - eta2) < 0.01f) {
1237 break;
1238 }
1239 eta = (eta2 + eta) * 0.5f;
1240 }
1241 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
1242 bulletSpeed, bulletDamage);
1243 enemy->futureDamage += bulletDamage;
1244 }
1245 }
1246 else
1247 {
1248 tower->cooldown -= gameTime.deltaTime;
1249 }
1250 }
1251
1252 void TowerUpdate()
1253 {
1254 for (int i = 0; i < towerCount; i++)
1255 {
1256 Tower *tower = &towers[i];
1257 switch (tower->towerType)
1258 {
1259 case TOWER_TYPE_GUN:
1260 TowerGunUpdate(tower);
1261 break;
1262 }
1263 }
1264 }
1265
1266 //# Game
1267
1268 void InitLevel(Level *level)
1269 {
1270 TowerInit();
1271 EnemyInit();
1272 ProjectileInit();
1273 ParticleInit();
1274 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1275
1276 level->placementMode = 0;
1277 level->state = LEVEL_STATE_BUILDING;
1278 level->nextState = LEVEL_STATE_NONE;
1279 level->playerGold = level->initialGold;
1280
1281 Camera *camera = &level->camera;
1282 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
1283 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
1284 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
1285 camera->fovy = 45.0f;
1286 camera->projection = CAMERA_PERSPECTIVE;
1287 }
1288
1289 void DrawLevelHud(Level *level)
1290 {
1291 const char *text = TextFormat("Gold: %d", level->playerGold);
1292 Font font = GetFontDefault();
1293 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
1294 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
1295 }
1296
1297 void DrawLevelReportLostWave(Level *level)
1298 {
1299 BeginMode3D(level->camera);
1300 DrawGrid(10, 1.0f);
1301 TowerDraw();
1302 EnemyDraw();
1303 ProjectileDraw();
1304 ParticleDraw();
1305 guiState.isBlocked = 0;
1306 EndMode3D();
1307
1308 const char *text = "Wave lost";
1309 int textWidth = MeasureText(text, 20);
1310 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1311
1312 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1313 {
1314 level->nextState = LEVEL_STATE_RESET;
1315 }
1316 }
1317
1318 int HasLevelNextWave(Level *level)
1319 {
1320 for (int i = 0; i < 10; i++)
1321 {
1322 EnemyWave *wave = &level->waves[i];
1323 if (wave->wave == level->currentWave)
1324 {
1325 return 1;
1326 }
1327 }
1328 return 0;
1329 }
1330
1331 void DrawLevelReportWonWave(Level *level)
1332 {
1333 BeginMode3D(level->camera);
1334 DrawGrid(10, 1.0f);
1335 TowerDraw();
1336 EnemyDraw();
1337 ProjectileDraw();
1338 ParticleDraw();
1339 guiState.isBlocked = 0;
1340 EndMode3D();
1341
1342 const char *text = "Wave won";
1343 int textWidth = MeasureText(text, 20);
1344 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1345
1346
1347 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1348 {
1349 level->nextState = LEVEL_STATE_RESET;
1350 }
1351
1352 if (HasLevelNextWave(level))
1353 {
1354 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
1355 {
1356 level->nextState = LEVEL_STATE_BUILDING;
1357 }
1358 }
1359 else {
1360 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
1361 {
1362 level->nextState = LEVEL_STATE_WON_LEVEL;
1363 }
1364 }
1365 }
1366
1367 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
1368 {
1369 static ButtonState buttonStates[8] = {0};
1370 int cost = GetTowerCosts(towerType);
1371 const char *text = TextFormat("%s: %d", name, cost);
1372 buttonStates[towerType].isSelected = level->placementMode == towerType;
1373 buttonStates[towerType].isDisabled = level->playerGold < cost;
1374 if (Button(text, x, y, width, height, &buttonStates[towerType]))
1375 {
1376 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
1377 }
1378 }
1379
1380 void DrawLevelBuildingState(Level *level)
1381 {
1382 BeginMode3D(level->camera);
1383 DrawGrid(10, 1.0f);
1384 TowerDraw();
1385 EnemyDraw();
1386 ProjectileDraw();
1387 ParticleDraw();
1388
1389 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
1390 float planeDistance = ray.position.y / -ray.direction.y;
1391 float planeX = ray.direction.x * planeDistance + ray.position.x;
1392 float planeY = ray.direction.z * planeDistance + ray.position.z;
1393 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
1394 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
1395 if (level->placementMode && !guiState.isBlocked)
1396 {
1397 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
1398 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1399 {
1400 if (TowerTryAdd(level->placementMode, mapX, mapY))
1401 {
1402 level->playerGold -= GetTowerCosts(level->placementMode);
1403 level->placementMode = TOWER_TYPE_NONE;
1404 }
1405 }
1406 }
1407
1408 guiState.isBlocked = 0;
1409
1410 EndMode3D();
1411
1412 static ButtonState buildWallButtonState = {0};
1413 static ButtonState buildGunButtonState = {0};
1414 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
1415 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
1416
1417 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
1418 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
1419
1420 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1421 {
1422 level->nextState = LEVEL_STATE_RESET;
1423 }
1424
1425 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
1426 {
1427 level->nextState = LEVEL_STATE_BATTLE;
1428 }
1429
1430 const char *text = "Building phase";
1431 int textWidth = MeasureText(text, 20);
1432 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1433 }
1434
1435 void InitBattleStateConditions(Level *level)
1436 {
1437 level->state = LEVEL_STATE_BATTLE;
1438 level->nextState = LEVEL_STATE_NONE;
1439 level->waveEndTimer = 0.0f;
1440 for (int i = 0; i < 10; i++)
1441 {
1442 EnemyWave *wave = &level->waves[i];
1443 wave->spawned = 0;
1444 wave->timeToSpawnNext = wave->delay;
1445 }
1446 }
1447
1448 void DrawLevelBattleState(Level *level)
1449 {
1450 BeginMode3D(level->camera);
1451 DrawGrid(10, 1.0f);
1452 TowerDraw();
1453 EnemyDraw();
1454 ProjectileDraw();
1455 ParticleDraw();
1456 guiState.isBlocked = 0;
1457 EndMode3D();
1458
1459 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1460 {
1461 level->nextState = LEVEL_STATE_RESET;
1462 }
1463
1464 int maxCount = 0;
1465 int remainingCount = 0;
1466 for (int i = 0; i < 10; i++)
1467 {
1468 EnemyWave *wave = &level->waves[i];
1469 if (wave->wave != level->currentWave)
1470 {
1471 continue;
1472 }
1473 maxCount += wave->count;
1474 remainingCount += wave->count - wave->spawned;
1475 }
1476 int aliveCount = EnemyCount();
1477 remainingCount += aliveCount;
1478
1479 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1480 int textWidth = MeasureText(text, 20);
1481 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1482 }
1483
1484 void DrawLevel(Level *level)
1485 {
1486 switch (level->state)
1487 {
1488 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1489 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1490 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
1491 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
1492 default: break;
1493 }
1494
1495 DrawLevelHud(level);
1496 }
1497
1498 void UpdateLevel(Level *level)
1499 {
1500 if (level->state == LEVEL_STATE_BATTLE)
1501 {
1502 int activeWaves = 0;
1503 for (int i = 0; i < 10; i++)
1504 {
1505 EnemyWave *wave = &level->waves[i];
1506 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1507 {
1508 continue;
1509 }
1510 activeWaves++;
1511 wave->timeToSpawnNext -= gameTime.deltaTime;
1512 if (wave->timeToSpawnNext <= 0.0f)
1513 {
1514 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1515 if (enemy)
1516 {
1517 wave->timeToSpawnNext = wave->interval;
1518 wave->spawned++;
1519 }
1520 }
1521 }
1522 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
1523 level->waveEndTimer += gameTime.deltaTime;
1524 if (level->waveEndTimer >= 2.0f)
1525 {
1526 level->nextState = LEVEL_STATE_LOST_WAVE;
1527 }
1528 }
1529 else if (activeWaves == 0 && EnemyCount() == 0)
1530 {
1531 level->waveEndTimer += gameTime.deltaTime;
1532 if (level->waveEndTimer >= 2.0f)
1533 {
1534 level->nextState = LEVEL_STATE_WON_WAVE;
1535 }
1536 }
1537 }
1538
1539 PathFindingMapUpdate();
1540 EnemyUpdate();
1541 TowerUpdate();
1542 ProjectileUpdate();
1543 ParticleUpdate();
1544
1545 if (level->nextState == LEVEL_STATE_RESET)
1546 {
1547 InitLevel(level);
1548 }
1549
1550 if (level->nextState == LEVEL_STATE_BATTLE)
1551 {
1552 InitBattleStateConditions(level);
1553 }
1554
1555 if (level->nextState == LEVEL_STATE_WON_WAVE)
1556 {
1557 level->currentWave++;
1558 level->state = LEVEL_STATE_WON_WAVE;
1559 }
1560
1561 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1562 {
1563 level->state = LEVEL_STATE_LOST_WAVE;
1564 }
1565
1566 if (level->nextState == LEVEL_STATE_BUILDING)
1567 {
1568 level->state = LEVEL_STATE_BUILDING;
1569 }
1570
1571 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1572 {
1573 // make something of this later
1574 InitLevel(level);
1575 }
1576
1577 level->nextState = LEVEL_STATE_NONE;
1578 }
1579
1580 float nextSpawnTime = 0.0f;
1581
1582 void ResetGame()
1583 {
1584 InitLevel(currentLevel);
1585 }
1586
1587 void InitGame()
1588 {
1589 TowerInit();
1590 EnemyInit();
1591 ProjectileInit();
1592 ParticleInit();
1593 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1594
1595 currentLevel = levels;
1596 InitLevel(currentLevel);
1597 }
1598
1599 //# Immediate GUI functions
1600
1601 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1602 {
1603 Rectangle bounds = {x, y, width, height};
1604 int isPressed = 0;
1605 int isSelected = state && state->isSelected;
1606 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
1607 {
1608 Color color = isSelected ? DARKGRAY : GRAY;
1609 DrawRectangle(x, y, width, height, color);
1610 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1611 {
1612 isPressed = 1;
1613 }
1614 guiState.isBlocked = 1;
1615 }
1616 else
1617 {
1618 Color color = isSelected ? WHITE : LIGHTGRAY;
1619 DrawRectangle(x, y, width, height, color);
1620 }
1621 Font font = GetFontDefault();
1622 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1623 Color textColor = state->isDisabled ? GRAY : BLACK;
1624 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
1625 return isPressed;
1626 }
1627
1628 //# Main game loop
1629
1630 void GameUpdate()
1631 {
1632 float dt = GetFrameTime();
1633 // cap maximum delta time to 0.1 seconds to prevent large time steps
1634 if (dt > 0.1f) dt = 0.1f;
1635 gameTime.time += dt;
1636 gameTime.deltaTime = dt;
1637
1638 UpdateLevel(currentLevel);
1639 }
1640
1641 int main(void)
1642 {
1643 int screenWidth, screenHeight;
1644 GetPreferredSize(&screenWidth, &screenHeight);
1645 InitWindow(screenWidth, screenHeight, "Tower defense");
1646 SetTargetFPS(30);
1647
1648 InitGame();
1649
1650 while (!WindowShouldClose())
1651 {
1652 if (IsPaused()) {
1653 // canvas is not visible in browser - do nothing
1654 continue;
1655 }
1656
1657 BeginDrawing();
1658 ClearBackground(DARKBLUE);
1659
1660 GameUpdate();
1661 DrawLevel(currentLevel);
1662
1663 EndDrawing();
1664 }
1665
1666 CloseWindow();
1667
1668 return 0;
1669 }
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
Wrap up
This starts to look like a game! We have a level system that allows us to define the scenario we're playing in, there's the build and battle phase and the game can be won or lost. We have a resource system and we no longer can build towers without having the resources for it.
The code however begins to become really messy. We have a lot of global variables and the declarations are all over the place. The next part will be about another important topic when working on game code: Refactoring. We will clean up the code and split it into files and understand how to structure code in C.