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:

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.

🍪