Simple tower defense tutorial, part 12: Building placement UX

Since touch screen interactions are working now (it wasn't working before I fixed some issues with the raylib web bindings that were specific to how I use the canvas integration), it would be nice if building placement on touch screens was working in a proper way.

Since I do get occasional feedback (thanks!) that the game does not properly work on touch screens, I want to address this issue now.

Draft how the building placement UX should work

Currently, you can choose a building to place and on the next tap on the map, the preview is shown at that location and the next tap places the building on whichever point you tapped on the map. This qualifies as "working", but it isn't something that is very nice to use.

UX (user experience) as a topic is complex and subjective. The complexity comes from the different input systems (mouse, keyboard, touch, gamepad) and what works in one environment might feel very awkward in another. Balancing this out is what makes this a subjective topic.

In this case, I will choose a pattern that I am familiar with from other games:

It would also be nice if the movement of the preview tower was smooth and playful. Also, the tower could be moved using the arrow keys on the keyboard this way.

Let's see how this will feel in practice!

Implementing the new UX

The first things to do is to establish the build mode with the cancel and OK buttons.

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 #include <stdlib.h>
  5 #include <math.h>
  6 
  7 //# Variables
  8 GUIState guiState = {0};
  9 GameTime gameTime = {0};
 10 
 11 Model floorTileAModel = {0};
 12 Model floorTileBModel = {0};
 13 Model treeModel[2] = {0};
 14 Model firTreeModel[2] = {0};
 15 Model rockModels[5] = {0};
 16 Model grassPatchModel[1] = {0};
 17 
 18 Model pathArrowModel = {0};
 19 
 20 Texture2D palette, spriteSheet;
 21 
 22 Level levels[] = {
 23   [0] = {
 24     .state = LEVEL_STATE_BUILDING,
 25     .initialGold = 20,
 26     .waves[0] = {
 27       .enemyType = ENEMY_TYPE_MINION,
 28       .wave = 0,
 29       .count = 5,
 30       .interval = 2.5f,
 31       .delay = 1.0f,
 32       .spawnPosition = {2, 6},
 33     },
 34     .waves[1] = {
 35       .enemyType = ENEMY_TYPE_MINION,
 36       .wave = 0,
 37       .count = 5,
 38       .interval = 2.5f,
 39       .delay = 1.0f,
 40       .spawnPosition = {-2, 6},
 41     },
 42     .waves[2] = {
 43       .enemyType = ENEMY_TYPE_MINION,
 44       .wave = 1,
 45       .count = 20,
 46       .interval = 1.5f,
 47       .delay = 1.0f,
 48       .spawnPosition = {0, 6},
 49     },
 50     .waves[3] = {
 51       .enemyType = ENEMY_TYPE_MINION,
 52       .wave = 2,
 53       .count = 30,
 54       .interval = 1.2f,
 55       .delay = 1.0f,
 56       .spawnPosition = {0, 6},
 57     }
 58   },
 59 };
 60 
 61 Level *currentLevel = levels;
 62 
 63 //# Game
 64 
 65 static Model LoadGLBModel(char *filename)
 66 {
 67   Model model = LoadModel(TextFormat("data/%s.glb",filename));
 68   for (int i = 0; i < model.materialCount; i++)
 69   {
 70     model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 71   }
 72   return model;
 73 }
 74 
 75 void LoadAssets()
 76 {
 77   // load a sprite sheet that contains all units
 78   spriteSheet = LoadTexture("data/spritesheet.png");
 79   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
 80 
 81   // we'll use a palette texture to colorize the all buildings and environment art
 82   palette = LoadTexture("data/palette.png");
 83   // The texture uses gradients on very small space, so we'll enable bilinear filtering
 84   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
 85 
 86   floorTileAModel = LoadGLBModel("floor-tile-a");
 87   floorTileBModel = LoadGLBModel("floor-tile-b");
 88   treeModel[0] = LoadGLBModel("leaftree-large-1-a");
 89   treeModel[1] = LoadGLBModel("leaftree-large-1-b");
 90   firTreeModel[0] = LoadGLBModel("firtree-1-a");
 91   firTreeModel[1] = LoadGLBModel("firtree-1-b");
 92   rockModels[0] = LoadGLBModel("rock-1");
 93   rockModels[1] = LoadGLBModel("rock-2");
 94   rockModels[2] = LoadGLBModel("rock-3");
 95   rockModels[3] = LoadGLBModel("rock-4");
 96   rockModels[4] = LoadGLBModel("rock-5");
 97   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
 98 
 99   pathArrowModel = LoadGLBModel("direction-arrow-x");
100 }
101 
102 void InitLevel(Level *level)
103 {
104   level->seed = (int)(GetTime() * 100.0f);
105 
106   TowerInit();
107   EnemyInit();
108   ProjectileInit();
109   ParticleInit();
110   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
111 
112   level->placementMode = 0;
113   level->state = LEVEL_STATE_BUILDING;
114   level->nextState = LEVEL_STATE_NONE;
115   level->playerGold = level->initialGold;
116   level->currentWave = 0;
117 
118   Camera *camera = &level->camera;
119   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
120   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
121   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
122   camera->fovy = 10.0f;
123   camera->projection = CAMERA_ORTHOGRAPHIC;
124 }
125 
126 void DrawLevelHud(Level *level)
127 {
128   const char *text = TextFormat("Gold: %d", level->playerGold);
129   Font font = GetFontDefault();
130   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
131   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
132 }
133 
134 void DrawLevelReportLostWave(Level *level)
135 {
136   BeginMode3D(level->camera);
137   DrawLevelGround(level);
138   TowerDraw();
139   EnemyDraw();
140   ProjectileDraw();
141   ParticleDraw();
142   guiState.isBlocked = 0;
143   EndMode3D();
144 
145   TowerDrawHealthBars(level->camera);
146 
147   const char *text = "Wave lost";
148   int textWidth = MeasureText(text, 20);
149   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
150 
151   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
152   {
153     level->nextState = LEVEL_STATE_RESET;
154   }
155 }
156 
157 int HasLevelNextWave(Level *level)
158 {
159   for (int i = 0; i < 10; i++)
160   {
161     EnemyWave *wave = &level->waves[i];
162     if (wave->wave == level->currentWave)
163     {
164       return 1;
165     }
166   }
167   return 0;
168 }
169 
170 void DrawLevelReportWonWave(Level *level)
171 {
172   BeginMode3D(level->camera);
173   DrawLevelGround(level);
174   TowerDraw();
175   EnemyDraw();
176   ProjectileDraw();
177   ParticleDraw();
178   guiState.isBlocked = 0;
179   EndMode3D();
180 
181   TowerDrawHealthBars(level->camera);
182 
183   const char *text = "Wave won";
184   int textWidth = MeasureText(text, 20);
185   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
186 
187 
188   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
189   {
190     level->nextState = LEVEL_STATE_RESET;
191   }
192 
193   if (HasLevelNextWave(level))
194   {
195     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
196     {
197       level->nextState = LEVEL_STATE_BUILDING;
198     }
199   }
200   else {
201     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
202     {
203       level->nextState = LEVEL_STATE_WON_LEVEL;
204     }
205   }
206 }
207 
208 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
209 {
210   static ButtonState buttonStates[8] = {0};
211   int cost = GetTowerCosts(towerType);
212   const char *text = TextFormat("%s: %d", name, cost);
213   buttonStates[towerType].isSelected = level->placementMode == towerType;
214   buttonStates[towerType].isDisabled = level->playerGold < cost;
215   if (Button(text, x, y, width, height, &buttonStates[towerType]))
216   {
217     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
218 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
219 } 220 } 221 222 float GetRandomFloat(float min, float max) 223 { 224 int random = GetRandomValue(0, 0xfffffff); 225 return ((float)random / (float)0xfffffff) * (max - min) + min; 226 } 227 228 void DrawLevelGround(Level *level) 229 { 230 // draw checkerboard ground pattern 231 for (int x = -5; x <= 5; x += 1) 232 { 233 for (int y = -5; y <= 5; y += 1) 234 { 235 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 236 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 237 } 238 } 239 240 int oldSeed = GetRandomValue(0, 0xfffffff); 241 SetRandomSeed(level->seed); 242 // increase probability for trees via duplicated entries 243 Model borderModels[64]; 244 int maxRockCount = GetRandomValue(2, 6); 245 int maxTreeCount = GetRandomValue(10, 20); 246 int maxFirTreeCount = GetRandomValue(5, 10); 247 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount; 248 int grassPatchCount = GetRandomValue(5, 30); 249 250 int modelCount = 0; 251 for (int i = 0; i < maxRockCount && modelCount < 63; i++) 252 { 253 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)]; 254 } 255 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++) 256 { 257 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)]; 258 } 259 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++) 260 { 261 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)]; 262 } 263 for (int i = 0; i < grassPatchCount && modelCount < 63; i++) 264 { 265 borderModels[modelCount++] = grassPatchModel[0]; 266 } 267 268 // draw some objects around the border of the map 269 Vector3 up = {0, 1, 0}; 270 // a pseudo random number generator to get the same result every time 271 const float wiggle = 0.75f; 272 const int layerCount = 3; 273 for (int layer = 0; layer < layerCount; layer++) 274 { 275 int layerPos = 6 + layer; 276 for (int x = -6 + layer; x <= 6 + layer; x += 1) 277 { 278 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 279 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 280 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 281 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 282 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 283 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 284 } 285 286 for (int z = -5 + layer; z <= 5 + layer; z += 1) 287 { 288 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 289 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 290 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 291 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 292 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 293 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 294 } 295 } 296 297 SetRandomSeed(oldSeed); 298 } 299 300 void DrawEnemyPath(Level *level, Color arrowColor) 301 { 302 const int castleX = 0, castleY = 0; 303 const int maxWaypointCount = 200; 304 const float timeStep = 1.0f; 305 Vector3 arrowScale = {0.75f, 0.75f, 0.75f}; 306 307 // we start with a time offset to simulate the path, 308 // this way the arrows are animated in a forward moving direction 309 // The time is wrapped around the time step to get a smooth animation 310 float timeOffset = fmodf(GetTime(), timeStep); 311 312 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 313 { 314 EnemyWave *wave = &level->waves[i]; 315 if (wave->wave != level->currentWave) 316 { 317 continue; 318 } 319 320 // use this dummy enemy to simulate the path 321 Enemy dummy = { 322 .enemyType = ENEMY_TYPE_MINION, 323 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y}, 324 .nextX = wave->spawnPosition.x, 325 .nextY = wave->spawnPosition.y, 326 .currentX = wave->spawnPosition.x, 327 .currentY = wave->spawnPosition.y, 328 }; 329 330 float deltaTime = timeOffset; 331 for (int j = 0; j < maxWaypointCount; j++) 332 { 333 int waypointPassedCount = 0; 334 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount); 335 // after the initial variable starting offset, we use a fixed time step 336 deltaTime = timeStep; 337 dummy.simPosition = pos; 338 339 // Update the dummy's position just like we do in the regular enemy update loop 340 for (int k = 0; k < waypointPassedCount; k++) 341 { 342 dummy.currentX = dummy.nextX; 343 dummy.currentY = dummy.nextY; 344 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) && 345 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 346 { 347 break; 348 } 349 } 350 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 351 { 352 break; 353 } 354 355 // get the angle we need to rotate the arrow model. The velocity is just fine for this. 356 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f; 357 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor); 358 } 359 } 360 } 361
362 void DrawEnemyPaths(Level *level) 363 { 364 // disable depth testing for the path arrows 365 // flush the 3D batch to draw the arrows on top of everything 366 rlDrawRenderBatchActive(); 367 rlDisableDepthTest(); 368 DrawEnemyPath(level, (Color){64, 64, 64, 160}); 369 370 rlDrawRenderBatchActive(); 371 rlEnableDepthTest(); 372 DrawEnemyPath(level, WHITE); 373 } 374 375 void DrawLevelBuildingPlacementState(Level *level)
376 { 377 BeginMode3D(level->camera); 378 DrawLevelGround(level); 379 380 int blockedCellCount = 0; 381 Vector2 blockedCells[1]; 382 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 383 float planeDistance = ray.position.y / -ray.direction.y; 384 float planeX = ray.direction.x * planeDistance + ray.position.x; 385 float planeY = ray.direction.z * planeDistance + ray.position.z; 386 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 387 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
388 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
389 {
390 level->placementX = mapX; 391 level->placementY = mapY; 392 } 393 else 394 { 395 mapX = level->placementX; 396 mapY = level->placementY; 397 } 398 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 399 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
400 PathFindingMapUpdate(blockedCellCount, blockedCells); 401 402 TowerDraw();
403 EnemyDraw(); 404 ProjectileDraw();
405 ParticleDraw();
406 DrawEnemyPaths(level); 407 408 guiState.isBlocked = 0; 409 410 EndMode3D();
411
412 TowerDrawHealthBars(level->camera); 413 414 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0)) 415 { 416 level->nextState = LEVEL_STATE_BUILDING; 417 level->placementMode = TOWER_TYPE_NONE; 418 TraceLog(LOG_INFO, "Cancel building"); 419 } 420 421 if (Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0)) 422 { 423 level->nextState = LEVEL_STATE_BUILDING; 424 if (TowerTryAdd(level->placementMode, mapX, mapY)) 425 { 426 level->playerGold -= GetTowerCosts(level->placementMode); 427 level->placementMode = TOWER_TYPE_NONE; 428 }
429 }
430 } 431 432 void DrawLevelBuildingState(Level *level) 433 {
434 BeginMode3D(level->camera);
435 DrawLevelGround(level); 436 437 PathFindingMapUpdate(0, 0); 438 TowerDraw(); 439 EnemyDraw(); 440 ProjectileDraw();
441 ParticleDraw();
442 DrawEnemyPaths(level);
443
444 guiState.isBlocked = 0; 445 446 EndMode3D();
447 448 TowerDrawHealthBars(level->camera); 449 450 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 451 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer"); 452 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 453 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult"); 454 455 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 456 { 457 level->nextState = LEVEL_STATE_RESET; 458 } 459 460 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 461 { 462 level->nextState = LEVEL_STATE_BATTLE; 463 } 464 465 const char *text = "Building phase"; 466 int textWidth = MeasureText(text, 20); 467 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 468 } 469 470 void InitBattleStateConditions(Level *level) 471 { 472 level->state = LEVEL_STATE_BATTLE; 473 level->nextState = LEVEL_STATE_NONE; 474 level->waveEndTimer = 0.0f; 475 for (int i = 0; i < 10; i++) 476 { 477 EnemyWave *wave = &level->waves[i]; 478 wave->spawned = 0; 479 wave->timeToSpawnNext = wave->delay; 480 } 481 } 482 483 void DrawLevelBattleState(Level *level) 484 { 485 BeginMode3D(level->camera); 486 DrawLevelGround(level); 487 TowerDraw(); 488 EnemyDraw(); 489 ProjectileDraw(); 490 ParticleDraw(); 491 guiState.isBlocked = 0; 492 EndMode3D(); 493 494 EnemyDrawHealthbars(level->camera); 495 TowerDrawHealthBars(level->camera); 496 497 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 498 { 499 level->nextState = LEVEL_STATE_RESET; 500 } 501 502 int maxCount = 0; 503 int remainingCount = 0; 504 for (int i = 0; i < 10; i++) 505 { 506 EnemyWave *wave = &level->waves[i]; 507 if (wave->wave != level->currentWave) 508 { 509 continue; 510 } 511 maxCount += wave->count; 512 remainingCount += wave->count - wave->spawned; 513 } 514 int aliveCount = EnemyCount(); 515 remainingCount += aliveCount; 516 517 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 518 int textWidth = MeasureText(text, 20); 519 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 520 } 521 522 void DrawLevel(Level *level)
523 { 524 switch (level->state)
525 { 526 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 527 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break; 528 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 529 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 530 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 531 default: break; 532 } 533 534 DrawLevelHud(level); 535 } 536 537 void UpdateLevel(Level *level) 538 { 539 if (level->state == LEVEL_STATE_BATTLE) 540 { 541 int activeWaves = 0; 542 for (int i = 0; i < 10; i++) 543 { 544 EnemyWave *wave = &level->waves[i]; 545 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 546 { 547 continue; 548 } 549 activeWaves++; 550 wave->timeToSpawnNext -= gameTime.deltaTime; 551 if (wave->timeToSpawnNext <= 0.0f) 552 { 553 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 554 if (enemy) 555 { 556 wave->timeToSpawnNext = wave->interval; 557 wave->spawned++; 558 } 559 } 560 } 561 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 562 level->waveEndTimer += gameTime.deltaTime; 563 if (level->waveEndTimer >= 2.0f) 564 { 565 level->nextState = LEVEL_STATE_LOST_WAVE; 566 } 567 } 568 else if (activeWaves == 0 && EnemyCount() == 0) 569 { 570 level->waveEndTimer += gameTime.deltaTime; 571 if (level->waveEndTimer >= 2.0f) 572 { 573 level->nextState = LEVEL_STATE_WON_WAVE; 574 } 575 } 576 } 577 578 PathFindingMapUpdate(0, 0); 579 EnemyUpdate(); 580 TowerUpdate(); 581 ProjectileUpdate(); 582 ParticleUpdate(); 583 584 if (level->nextState == LEVEL_STATE_RESET) 585 { 586 InitLevel(level); 587 } 588 589 if (level->nextState == LEVEL_STATE_BATTLE) 590 { 591 InitBattleStateConditions(level); 592 } 593 594 if (level->nextState == LEVEL_STATE_WON_WAVE) 595 { 596 level->currentWave++; 597 level->state = LEVEL_STATE_WON_WAVE; 598 } 599 600 if (level->nextState == LEVEL_STATE_LOST_WAVE) 601 { 602 level->state = LEVEL_STATE_LOST_WAVE; 603 }
604 605 if (level->nextState == LEVEL_STATE_BUILDING) 606 { 607 level->state = LEVEL_STATE_BUILDING; 608 } 609
610 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT) 611 { 612 level->state = LEVEL_STATE_BUILDING_PLACEMENT; 613 } 614 615 if (level->nextState == LEVEL_STATE_WON_LEVEL) 616 { 617 // make something of this later 618 InitLevel(level); 619 } 620 621 level->nextState = LEVEL_STATE_NONE; 622 } 623 624 float nextSpawnTime = 0.0f; 625 626 void ResetGame() 627 { 628 InitLevel(currentLevel); 629 } 630 631 void InitGame() 632 { 633 TowerInit(); 634 EnemyInit(); 635 ProjectileInit(); 636 ParticleInit(); 637 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 638 639 currentLevel = levels; 640 InitLevel(currentLevel); 641 } 642 643 //# Immediate GUI functions 644 645 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 646 { 647 const float healthBarHeight = 6.0f; 648 const float healthBarOffset = 15.0f; 649 const float inset = 2.0f; 650 const float innerWidth = healthBarWidth - inset * 2; 651 const float innerHeight = healthBarHeight - inset * 2; 652 653 Vector2 screenPos = GetWorldToScreen(position, camera); 654 float centerX = screenPos.x - healthBarWidth * 0.5f; 655 float topY = screenPos.y - healthBarOffset; 656 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 657 float healthWidth = innerWidth * healthRatio; 658 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 659 } 660 661 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 662 { 663 Rectangle bounds = {x, y, width, height}; 664 int isPressed = 0; 665 int isSelected = state && state->isSelected; 666 int isDisabled = state && state->isDisabled; 667 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 668 { 669 Color color = isSelected ? DARKGRAY : GRAY; 670 DrawRectangle(x, y, width, height, color); 671 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 672 { 673 isPressed = 1; 674 } 675 guiState.isBlocked = 1; 676 } 677 else 678 { 679 Color color = isSelected ? WHITE : LIGHTGRAY; 680 DrawRectangle(x, y, width, height, color); 681 } 682 Font font = GetFontDefault(); 683 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 684 Color textColor = isDisabled ? GRAY : BLACK; 685 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 686 return isPressed; 687 } 688 689 //# Main game loop 690 691 void GameUpdate() 692 { 693 float dt = GetFrameTime(); 694 // cap maximum delta time to 0.1 seconds to prevent large time steps 695 if (dt > 0.1f) dt = 0.1f; 696 gameTime.time += dt; 697 gameTime.deltaTime = dt; 698 699 UpdateLevel(currentLevel); 700 } 701 702 int main(void) 703 { 704 int screenWidth, screenHeight; 705 GetPreferredSize(&screenWidth, &screenHeight); 706 InitWindow(screenWidth, screenHeight, "Tower defense"); 707 SetTargetFPS(30); 708 709 LoadAssets(); 710 InitGame(); 711 712 while (!WindowShouldClose()) 713 { 714 if (IsPaused()) { 715 // canvas is not visible in browser - do nothing 716 continue; 717 } 718 719 BeginDrawing(); 720 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 721 722 GameUpdate(); 723 DrawLevel(currentLevel); 724 725 EndDrawing(); 726 } 727 728 CloseWindow(); 729 730 return 0; 731 }
  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 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27   Vector3 scale;
 28 } Particle;
 29 
 30 #define TOWER_MAX_COUNT 400
 31 enum TowerType
 32 {
 33   TOWER_TYPE_NONE,
 34   TOWER_TYPE_BASE,
 35   TOWER_TYPE_ARCHER,
 36   TOWER_TYPE_BALLISTA,
 37   TOWER_TYPE_CATAPULT,
 38   TOWER_TYPE_WALL,
 39   TOWER_TYPE_COUNT
 40 };
 41 
 42 typedef struct HitEffectConfig
 43 {
 44   float damage;
 45   float areaDamageRadius;
 46   float pushbackPowerDistance;
 47 } HitEffectConfig;
 48 
 49 typedef struct TowerTypeConfig
 50 {
 51   float cooldown;
 52   float range;
 53   float projectileSpeed;
 54   
 55   uint8_t cost;
 56   uint8_t projectileType;
 57   uint16_t maxHealth;
 58 
 59   HitEffectConfig hitEffect;
 60 } TowerTypeConfig;
 61 
 62 typedef struct Tower
 63 {
 64   int16_t x, y;
 65   uint8_t towerType;
 66   Vector2 lastTargetPosition;
 67   float cooldown;
 68   float damage;
 69 } Tower;
 70 
 71 typedef struct GameTime
 72 {
 73   float time;
 74   float deltaTime;
 75 } GameTime;
 76 
 77 typedef struct ButtonState {
 78   char isSelected;
 79   char isDisabled;
 80 } ButtonState;
 81 
 82 typedef struct GUIState {
 83   int isBlocked;
 84 } GUIState;
 85 
 86 typedef enum LevelState
 87 {
 88   LEVEL_STATE_NONE,
 89   LEVEL_STATE_BUILDING,
90 LEVEL_STATE_BUILDING_PLACEMENT,
91 LEVEL_STATE_BATTLE, 92 LEVEL_STATE_WON_WAVE, 93 LEVEL_STATE_LOST_WAVE, 94 LEVEL_STATE_WON_LEVEL, 95 LEVEL_STATE_RESET, 96 } LevelState; 97 98 typedef struct EnemyWave { 99 uint8_t enemyType; 100 uint8_t wave; 101 uint16_t count; 102 float interval; 103 float delay; 104 Vector2 spawnPosition; 105 106 uint16_t spawned; 107 float timeToSpawnNext; 108 } EnemyWave; 109 110 #define ENEMY_MAX_WAVE_COUNT 10 111 112 typedef struct Level 113 { 114 int seed; 115 LevelState state; 116 LevelState nextState; 117 Camera3D camera;
118 int placementMode; 119 int16_t placementX; 120 int16_t placementY;
121 122 int initialGold; 123 int playerGold; 124 125 EnemyWave waves[ENEMY_MAX_WAVE_COUNT]; 126 int currentWave; 127 float waveEndTimer; 128 } Level; 129 130 typedef struct DeltaSrc 131 { 132 char x, y; 133 } DeltaSrc; 134 135 typedef struct PathfindingMap 136 { 137 int width, height; 138 float scale; 139 float *distances; 140 long *towerIndex; 141 DeltaSrc *deltaSrc; 142 float maxDistance; 143 Matrix toMapSpace; 144 Matrix toWorldSpace; 145 } PathfindingMap; 146 147 // when we execute the pathfinding algorithm, we need to store the active nodes 148 // in a queue. Each node has a position, a distance from the start, and the 149 // position of the node that we came from. 150 typedef struct PathfindingNode 151 { 152 int16_t x, y, fromX, fromY; 153 float distance; 154 } PathfindingNode; 155 156 typedef struct EnemyId 157 { 158 uint16_t index; 159 uint16_t generation; 160 } EnemyId; 161 162 typedef struct EnemyClassConfig 163 { 164 float speed; 165 float health; 166 float radius; 167 float maxAcceleration; 168 float requiredContactTime; 169 float explosionDamage; 170 float explosionRange; 171 float explosionPushbackPower; 172 int goldValue; 173 } EnemyClassConfig; 174 175 typedef struct Enemy 176 { 177 int16_t currentX, currentY; 178 int16_t nextX, nextY; 179 Vector2 simPosition; 180 Vector2 simVelocity; 181 uint16_t generation; 182 float walkedDistance; 183 float startMovingTime; 184 float damage, futureDamage; 185 float contactTime; 186 uint8_t enemyType; 187 uint8_t movePathCount; 188 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 189 } Enemy; 190 191 // a unit that uses sprites to be drawn 192 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0 193 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1 194 typedef struct SpriteUnit 195 { 196 Rectangle srcRect; 197 Vector2 offset; 198 int frameCount; 199 float frameDuration; 200 Rectangle srcWeaponIdleRect; 201 Vector2 srcWeaponIdleOffset; 202 Rectangle srcWeaponCooldownRect; 203 Vector2 srcWeaponCooldownOffset; 204 } SpriteUnit; 205 206 #define PROJECTILE_MAX_COUNT 1200 207 #define PROJECTILE_TYPE_NONE 0 208 #define PROJECTILE_TYPE_ARROW 1 209 #define PROJECTILE_TYPE_CATAPULT 2 210 #define PROJECTILE_TYPE_BALLISTA 3 211 212 typedef struct Projectile 213 { 214 uint8_t projectileType; 215 float shootTime; 216 float arrivalTime; 217 float distance; 218 Vector3 position; 219 Vector3 target; 220 Vector3 directionNormal; 221 EnemyId targetEnemy; 222 HitEffectConfig hitEffectConfig; 223 } Projectile; 224 225 //# Function declarations 226 float TowerGetMaxHealth(Tower *tower); 227 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 228 int EnemyAddDamageRange(Vector2 position, float range, float damage); 229 int EnemyAddDamage(Enemy *enemy, float damage); 230 231 //# Enemy functions 232 void EnemyInit(); 233 void EnemyDraw(); 234 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 235 void EnemyUpdate(); 236 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 237 float EnemyGetMaxHealth(Enemy *enemy); 238 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 239 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 240 EnemyId EnemyGetId(Enemy *enemy); 241 Enemy *EnemyTryResolve(EnemyId enemyId); 242 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 243 int EnemyAddDamage(Enemy *enemy, float damage); 244 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 245 int EnemyCount(); 246 void EnemyDrawHealthbars(Camera3D camera); 247 248 //# Tower functions 249 void TowerInit(); 250 Tower *TowerGetAt(int16_t x, int16_t y); 251 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 252 Tower *GetTowerByType(uint8_t towerType); 253 int GetTowerCosts(uint8_t towerType); 254 float TowerGetMaxHealth(Tower *tower); 255 void TowerDraw(); 256 void TowerUpdate(); 257 void TowerDrawHealthBars(Camera3D camera); 258 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 259 260 //# Particles 261 void ParticleInit(); 262 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime); 263 void ParticleUpdate(); 264 void ParticleDraw(); 265 266 //# Projectiles 267 void ProjectileInit(); 268 void ProjectileDraw(); 269 void ProjectileUpdate(); 270 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig); 271 272 //# Pathfinding map 273 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 274 float PathFindingGetDistance(int mapX, int mapY); 275 Vector2 PathFindingGetGradient(Vector3 world); 276 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 277 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells); 278 void PathFindingMapDraw(); 279 280 //# UI 281 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth); 282 283 //# Level 284 void DrawLevelGround(Level *level); 285 void DrawEnemyPath(Level *level, Color arrowColor); 286 287 //# variables 288 extern Level *currentLevel; 289 extern Enemy enemies[ENEMY_MAX_COUNT]; 290 extern int enemyCount; 291 extern EnemyClassConfig enemyClassConfigs[]; 292 293 extern GUIState guiState; 294 extern GameTime gameTime; 295 extern Tower towers[TOWER_MAX_COUNT]; 296 extern int towerCount; 297 298 extern Texture2D palette, spriteSheet; 299 300 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 // The queue is a simple array of nodes, we add nodes to the end and remove
  5 // nodes from the front. We keep the array around to avoid unnecessary allocations
  6 static PathfindingNode *pathfindingNodeQueue = 0;
  7 static int pathfindingNodeQueueCount = 0;
  8 static int pathfindingNodeQueueCapacity = 0;
  9 
 10 // The pathfinding map stores the distances from the castle to each cell in the map.
 11 static PathfindingMap pathfindingMap = {0};
 12 
 13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
 14 {
 15   // transforming between map space and world space allows us to adapt 
 16   // position and scale of the map without changing the pathfinding data
 17   pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
 18   pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
 19   pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
 20   pathfindingMap.width = width;
 21   pathfindingMap.height = height;
 22   pathfindingMap.scale = scale;
 23   pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
 24   for (int i = 0; i < width * height; i++)
 25   {
 26     pathfindingMap.distances[i] = -1.0f;
 27   }
 28 
 29   pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
 30   pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
 31 }
 32 
 33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
 34 {
 35   if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
 36   {
 37     pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
 38     // we use MemAlloc/MemRealloc to allocate memory for the queue
 39     // I am not entirely sure if MemRealloc allows passing a null pointer
 40     // so we check if the pointer is null and use MemAlloc in that case
 41     if (pathfindingNodeQueue == 0)
 42     {
 43       pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 44     }
 45     else
 46     {
 47       pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 48     }
 49   }
 50 
 51   PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
 52   node->x = x;
 53   node->y = y;
 54   node->fromX = fromX;
 55   node->fromY = fromY;
 56   node->distance = distance;
 57 }
 58 
 59 static PathfindingNode *PathFindingNodePop()
 60 {
 61   if (pathfindingNodeQueueCount == 0)
 62   {
 63     return 0;
 64   }
 65   // we return the first node in the queue; we want to return a pointer to the node
 66   // so we can return 0 if the queue is empty. 
 67   // We should _not_ return a pointer to the element in the list, because the list
 68   // may be reallocated and the pointer would become invalid. Or the 
 69   // popped element is overwritten by the next push operation.
 70   // Using static here means that the variable is permanently allocated.
 71   static PathfindingNode node;
 72   node = pathfindingNodeQueue[0];
 73   // we shift all nodes one position to the front
 74   for (int i = 1; i < pathfindingNodeQueueCount; i++)
 75   {
 76     pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
 77   }
 78   --pathfindingNodeQueueCount;
 79   return &node;
 80 }
 81 
 82 float PathFindingGetDistance(int mapX, int mapY)
 83 {
 84   if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
 85   {
 86     // when outside the map, we return the manhattan distance to the castle (0,0)
 87     return fabsf((float)mapX) + fabsf((float)mapY);
 88   }
 89 
 90   return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
 91 }
 92 
 93 // transform a world position to a map position in the array; 
 94 // returns true if the position is inside the map
 95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
 96 {
 97   Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
 98   *mapX = (int16_t)mapPosition.x;
 99   *mapY = (int16_t)mapPosition.z;
100   return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102 
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105   const int castleX = 0, castleY = 0;
106   int16_t castleMapX, castleMapY;
107   if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108   {
109     return;
110   }
111   int width = pathfindingMap.width, height = pathfindingMap.height;
112 
113   // reset the distances to -1
114   for (int i = 0; i < width * height; i++)
115   {
116     pathfindingMap.distances[i] = -1.0f;
117   }
118   // reset the tower indices
119   for (int i = 0; i < width * height; i++)
120   {
121     pathfindingMap.towerIndex[i] = -1;
122   }
123   // reset the delta src
124   for (int i = 0; i < width * height; i++)
125   {
126     pathfindingMap.deltaSrc[i].x = 0;
127     pathfindingMap.deltaSrc[i].y = 0;
128   }
129 
130   for (int i = 0; i < blockedCellCount; i++)
131   {
132     int16_t mapX, mapY;
133     if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134     {
135       continue;
136     }
137     int index = mapY * width + mapX;
138     pathfindingMap.towerIndex[index] = -2;
139   }
140 
141   for (int i = 0; i < towerCount; i++)
142   {
143     Tower *tower = &towers[i];
144     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145     {
146       continue;
147     }
148     int16_t mapX, mapY;
149     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150     // this would not work correctly and needs to be refined to allow towers covering multiple cells
151     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152     // one cell. For now.
153     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154     {
155       continue;
156     }
157     int index = mapY * width + mapX;
158     pathfindingMap.towerIndex[index] = i;
159   }
160 
161   // we start at the castle and add the castle to the queue
162   pathfindingMap.maxDistance = 0.0f;
163   pathfindingNodeQueueCount = 0;
164   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165   PathfindingNode *node = 0;
166   while ((node = PathFindingNodePop()))
167   {
168     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169     {
170       continue;
171     }
172     int index = node->y * width + node->x;
173     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174     {
175       continue;
176     }
177 
178     int deltaX = node->x - node->fromX;
179     int deltaY = node->y - node->fromY;
180     // even if the cell is blocked by a tower, we still may want to store the direction
181     // (though this might not be needed, IDK right now)
182     pathfindingMap.deltaSrc[index].x = (char) deltaX;
183     pathfindingMap.deltaSrc[index].y = (char) deltaY;
184 
185     // we skip nodes that are blocked by towers or by the provided blocked cells
186     if (pathfindingMap.towerIndex[index] != -1)
187     {
188       node->distance += 8.0f;
189     }
190     pathfindingMap.distances[index] = node->distance;
191     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196   }
197 }
198 
199 void PathFindingMapDraw()
200 {
201   float cellSize = pathfindingMap.scale * 0.9f;
202   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203   for (int x = 0; x < pathfindingMap.width; x++)
204   {
205     for (int y = 0; y < pathfindingMap.height; y++)
206     {
207       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211       // animate the distance "wave" to show how the pathfinding algorithm expands
212       // from the castle
213       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214       {
215         color = BLACK;
216       }
217       DrawCube(position, cellSize, 0.1f, cellSize, color);
218     }
219   }
220 }
221 
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224   int16_t mapX, mapY;
225   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226   {
227     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228     return (Vector2){(float)-delta.x, (float)-delta.y};
229   }
230   // fallback to a simple gradient calculation
231   float n = PathFindingGetDistance(mapX, mapY - 1);
232   float s = PathFindingGetDistance(mapX, mapY + 1);
233   float w = PathFindingGetDistance(mapX - 1, mapY);
234   float e = PathFindingGetDistance(mapX + 1, mapY);
235   return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .maxHealth = 10,
  7     },
  8     [TOWER_TYPE_ARCHER] = {
  9         .cooldown = 0.5f,
 10         .range = 3.0f,
 11         .cost = 6,
 12         .maxHealth = 10,
 13         .projectileSpeed = 4.0f,
 14         .projectileType = PROJECTILE_TYPE_ARROW,
 15         .hitEffect = {
 16           .damage = 3.0f,
 17         }
 18     },
 19     [TOWER_TYPE_BALLISTA] = {
 20         .cooldown = 1.5f,
 21         .range = 6.0f,
 22         .cost = 9,
 23         .maxHealth = 10,
 24         .projectileSpeed = 10.0f,
 25         .projectileType = PROJECTILE_TYPE_BALLISTA,
 26         .hitEffect = {
 27           .damage = 6.0f,
 28           .pushbackPowerDistance = 0.25f,
 29         }
 30     },
 31     [TOWER_TYPE_CATAPULT] = {
 32         .cooldown = 1.7f,
 33         .range = 5.0f,
 34         .cost = 10,
 35         .maxHealth = 10,
 36         .projectileSpeed = 3.0f,
 37         .projectileType = PROJECTILE_TYPE_CATAPULT,
 38         .hitEffect = {
 39           .damage = 2.0f,
 40           .areaDamageRadius = 1.75f,
 41         }
 42     },
 43     [TOWER_TYPE_WALL] = {
 44         .cost = 2,
 45         .maxHealth = 10,
 46     },
 47 };
 48 
 49 Tower towers[TOWER_MAX_COUNT];
 50 int towerCount = 0;
 51 
 52 Model towerModels[TOWER_TYPE_COUNT];
 53 
 54 // definition of our archer unit
 55 SpriteUnit archerUnit = {
 56     .srcRect = {0, 0, 16, 16},
 57     .offset = {7, 1},
 58     .frameCount = 1,
 59     .frameDuration = 0.0f,
 60     .srcWeaponIdleRect = {16, 0, 6, 16},
 61     .srcWeaponIdleOffset = {8, 0},
 62     .srcWeaponCooldownRect = {22, 0, 11, 16},
 63     .srcWeaponCooldownOffset = {10, 0},
 64 };
 65 
 66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 67 {
 68   float xScale = flip ? -1.0f : 1.0f;
 69   Camera3D camera = currentLevel->camera;
 70   float size = 0.5f;
 71   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 72   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 73   // we want the sprite to face the camera, so we need to calculate the up vector
 74   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 75   Vector3 up = {0, 1, 0};
 76   Vector3 right = Vector3CrossProduct(forward, up);
 77   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 78 
 79   Rectangle srcRect = unit.srcRect;
 80   if (unit.frameCount > 1)
 81   {
 82     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 83   }
 84   if (flip)
 85   {
 86     srcRect.x += srcRect.width;
 87     srcRect.width = -srcRect.width;
 88   }
 89   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 90 
 91   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 92   {
 93     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 94     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 95     srcRect = unit.srcWeaponCooldownRect;
 96     if (flip)
 97     {
 98       // position.x = flip * scale.x * 0.5f;
 99       srcRect.x += srcRect.width;
100       srcRect.width = -srcRect.width;
101       offset.x = scale.x - offset.x;
102     }
103     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104   }
105   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106   {
107     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109     srcRect = unit.srcWeaponIdleRect;
110     if (flip)
111     {
112       // position.x = flip * scale.x * 0.5f;
113       srcRect.x += srcRect.width;
114       srcRect.width = -srcRect.width;
115       offset.x = scale.x - offset.x;
116     }
117     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118   }
119 }
120 
121 void TowerInit()
122 {
123   for (int i = 0; i < TOWER_MAX_COUNT; i++)
124   {
125     towers[i] = (Tower){0};
126   }
127   towerCount = 0;
128 
129   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131 
132   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133   {
134     if (towerModels[i].materials)
135     {
136       // assign the palette texture to the material of the model (0 is not used afaik)
137       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138     }
139   }
140 }
141 
142 static void TowerGunUpdate(Tower *tower)
143 {
144   TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145   if (tower->cooldown <= 0.0f)
146   {
147     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148     if (enemy)
149     {
150       tower->cooldown = config.cooldown;
151       // shoot the enemy; determine future position of the enemy
152       float bulletSpeed = config.projectileSpeed;
153       Vector2 velocity = enemy->simVelocity;
154       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155       Vector2 towerPosition = {tower->x, tower->y};
156       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157       for (int i = 0; i < 8; i++) {
158         velocity = enemy->simVelocity;
159         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160         float distance = Vector2Distance(towerPosition, futurePosition);
161         float eta2 = distance / bulletSpeed;
162         if (fabs(eta - eta2) < 0.01f) {
163           break;
164         }
165         eta = (eta2 + eta) * 0.5f;
166       }
167 
168       ProjectileTryAdd(config.projectileType, enemy, 
169         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
170         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171         bulletSpeed, config.hitEffect);
172       enemy->futureDamage += config.hitEffect.damage;
173       tower->lastTargetPosition = futurePosition;
174     }
175   }
176   else
177   {
178     tower->cooldown -= gameTime.deltaTime;
179   }
180 }
181 
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184   for (int i = 0; i < towerCount; i++)
185   {
186     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187     {
188       return &towers[i];
189     }
190   }
191   return 0;
192 }
193 
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196   if (towerCount >= TOWER_MAX_COUNT)
197   {
198     return 0;
199   }
200 
201   Tower *tower = TowerGetAt(x, y);
202   if (tower)
203   {
204     return 0;
205   }
206 
207   tower = &towers[towerCount++];
208   tower->x = x;
209   tower->y = y;
210   tower->towerType = towerType;
211   tower->cooldown = 0.0f;
212   tower->damage = 0.0f;
213   return tower;
214 }
215 
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218   for (int i = 0; i < towerCount; i++)
219   {
220     if (towers[i].towerType == towerType)
221     {
222       return &towers[i];
223     }
224   }
225   return 0;
226 }
227 
228 int GetTowerCosts(uint8_t towerType)
229 {
230   return towerTypeConfigs[towerType].cost;
231 }
232 
233 float TowerGetMaxHealth(Tower *tower)
234 {
235   return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237 
238 void TowerDraw()
239 {
240   for (int i = 0; i < towerCount; i++)
241   {
242     Tower tower = towers[i];
243     if (tower.towerType == TOWER_TYPE_NONE)
244     {
245       continue;
246     }
247 
248     switch (tower.towerType)
249     {
250     case TOWER_TYPE_ARCHER:
251       {
252         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
256           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257       }
258       break;
259     case TOWER_TYPE_BALLISTA:
260       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261       break;
262     case TOWER_TYPE_CATAPULT:
263       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264       break;
265     default:
266       if (towerModels[tower.towerType].materials)
267       {
268         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269       } else {
270         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271       }
272       break;
273     }
274   }
275 }
276 
277 void TowerUpdate()
278 {
279   for (int i = 0; i < towerCount; i++)
280   {
281     Tower *tower = &towers[i];
282     switch (tower->towerType)
283     {
284     case TOWER_TYPE_CATAPULT:
285     case TOWER_TYPE_BALLISTA:
286     case TOWER_TYPE_ARCHER:
287       TowerGunUpdate(tower);
288       break;
289     }
290   }
291 }
292 
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295   for (int i = 0; i < towerCount; i++)
296   {
297     Tower *tower = &towers[i];
298     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299     {
300       continue;
301     }
302     
303     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304     float maxHealth = TowerGetMaxHealth(tower);
305     float health = maxHealth - tower->damage;
306     float healthRatio = health / maxHealth;
307     
308     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309   }
310 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 EnemyClassConfig enemyClassConfigs[] = {
  7     [ENEMY_TYPE_MINION] = {
  8       .health = 10.0f, 
  9       .speed = 0.6f, 
 10       .radius = 0.25f, 
 11       .maxAcceleration = 1.0f,
 12       .explosionDamage = 1.0f,
 13       .requiredContactTime = 0.5f,
 14       .explosionRange = 1.0f,
 15       .explosionPushbackPower = 0.25f,
 16       .goldValue = 1,
 17     },
 18 };
 19 
 20 Enemy enemies[ENEMY_MAX_COUNT];
 21 int enemyCount = 0;
 22 
 23 SpriteUnit enemySprites[] = {
 24     [ENEMY_TYPE_MINION] = {
 25       .srcRect = {0, 16, 16, 16},
 26       .offset = {8.0f, 0.0f},
 27       .frameCount = 6,
 28       .frameDuration = 0.1f,
 29     },
 30 };
 31 
 32 void EnemyInit()
 33 {
 34   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 35   {
 36     enemies[i] = (Enemy){0};
 37   }
 38   enemyCount = 0;
 39 }
 40 
 41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 42 {
 43   return enemyClassConfigs[enemy->enemyType].speed;
 44 }
 45 
 46 float EnemyGetMaxHealth(Enemy *enemy)
 47 {
 48   return enemyClassConfigs[enemy->enemyType].health;
 49 }
 50 
 51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 52 {
 53   int16_t castleX = 0;
 54   int16_t castleY = 0;
 55   int16_t dx = castleX - currentX;
 56   int16_t dy = castleY - currentY;
 57   if (dx == 0 && dy == 0)
 58   {
 59     *nextX = currentX;
 60     *nextY = currentY;
 61     return 1;
 62   }
 63   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 64 
 65   if (gradient.x == 0 && gradient.y == 0)
 66   {
 67     *nextX = currentX;
 68     *nextY = currentY;
 69     return 1;
 70   }
 71 
 72   if (fabsf(gradient.x) > fabsf(gradient.y))
 73   {
 74     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 75     *nextY = currentY;
 76     return 0;
 77   }
 78   *nextX = currentX;
 79   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 80   return 0;
 81 }
 82 
 83 
 84 // this function predicts the movement of the unit for the next deltaT seconds
 85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 86 {
 87   const float pointReachedDistance = 0.25f;
 88   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 89   const float maxSimStepTime = 0.015625f;
 90   
 91   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 92   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 93   int16_t nextX = enemy->nextX;
 94   int16_t nextY = enemy->nextY;
 95   Vector2 position = enemy->simPosition;
 96   int passedCount = 0;
 97   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 98   {
 99     float stepTime = fminf(deltaT - t, maxSimStepTime);
100     Vector2 target = (Vector2){nextX, nextY};
101     float speed = Vector2Length(*velocity);
102     // draw the target position for debugging
103     //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106     {
107       // we reached the target position, let's move to the next waypoint
108       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109       target = (Vector2){nextX, nextY};
110       // track how many waypoints we passed
111       passedCount++;
112     }
113     
114     // acceleration towards the target
115     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117     *velocity = Vector2Add(*velocity, acceleration);
118 
119     // limit the speed to the maximum speed
120     if (speed > maxSpeed)
121     {
122       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123     }
124 
125     // move the enemy
126     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127   }
128 
129   if (waypointPassedCount)
130   {
131     (*waypointPassedCount) = passedCount;
132   }
133 
134   return position;
135 }
136 
137 void EnemyDraw()
138 {
139   for (int i = 0; i < enemyCount; i++)
140   {
141     Enemy enemy = enemies[i];
142     if (enemy.enemyType == ENEMY_TYPE_NONE)
143     {
144       continue;
145     }
146 
147     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148     
149     // don't draw any trails for now; might replace this with footprints later
150     // if (enemy.movePathCount > 0)
151     // {
152     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154     // }
155     // for (int j = 1; j < enemy.movePathCount; j++)
156     // {
157     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159     //   DrawLine3D(p, q, GREEN);
160     // }
161 
162     switch (enemy.enemyType)
163     {
164     case ENEMY_TYPE_MINION:
165       DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y}, 
166         enemy.walkedDistance, 0, 0);
167       break;
168     }
169   }
170 }
171 
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174   // damage the tower
175   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178   float explosionRange2 = explosionRange * explosionRange;
179   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180   // explode the enemy
181   if (tower->damage >= TowerGetMaxHealth(tower))
182   {
183     tower->towerType = TOWER_TYPE_NONE;
184   }
185 
186   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
187     explosionSource, 
188     (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189 
190   enemy->enemyType = ENEMY_TYPE_NONE;
191 
192   // push back enemies & dealing damage
193   for (int i = 0; i < enemyCount; i++)
194   {
195     Enemy *other = &enemies[i];
196     if (other->enemyType == ENEMY_TYPE_NONE)
197     {
198       continue;
199     }
200     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201     if (distanceSqr > 0 && distanceSqr < explosionRange2)
202     {
203       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205       EnemyAddDamage(other, explosionDamge);
206     }
207   }
208 }
209 
210 void EnemyUpdate()
211 {
212   const float castleX = 0;
213   const float castleY = 0;
214   const float maxPathDistance2 = 0.25f * 0.25f;
215   
216   for (int i = 0; i < enemyCount; i++)
217   {
218     Enemy *enemy = &enemies[i];
219     if (enemy->enemyType == ENEMY_TYPE_NONE)
220     {
221       continue;
222     }
223 
224     int waypointPassedCount = 0;
225     Vector2 prevPosition = enemy->simPosition;
226     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227     enemy->startMovingTime = gameTime.time;
228     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229     // track path of unit
230     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231     {
232       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233       {
234         enemy->movePath[j] = enemy->movePath[j - 1];
235       }
236       enemy->movePath[0] = enemy->simPosition;
237       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238       {
239         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240       }
241     }
242 
243     if (waypointPassedCount > 0)
244     {
245       enemy->currentX = enemy->nextX;
246       enemy->currentY = enemy->nextY;
247       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249       {
250         // enemy reached the castle; remove it
251         enemy->enemyType = ENEMY_TYPE_NONE;
252         continue;
253       }
254     }
255   }
256 
257   // handle collisions between enemies
258   for (int i = 0; i < enemyCount - 1; i++)
259   {
260     Enemy *enemyA = &enemies[i];
261     if (enemyA->enemyType == ENEMY_TYPE_NONE)
262     {
263       continue;
264     }
265     for (int j = i + 1; j < enemyCount; j++)
266     {
267       Enemy *enemyB = &enemies[j];
268       if (enemyB->enemyType == ENEMY_TYPE_NONE)
269       {
270         continue;
271       }
272       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275       float radiusSum = radiusA + radiusB;
276       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277       {
278         // collision
279         float distance = sqrtf(distanceSqr);
280         float overlap = radiusSum - distance;
281         // move the enemies apart, but softly; if we have a clog of enemies,
282         // moving them perfectly apart can cause them to jitter
283         float positionCorrection = overlap / 5.0f;
284         Vector2 direction = (Vector2){
285             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289       }
290     }
291   }
292 
293   // handle collisions between enemies and towers
294   for (int i = 0; i < enemyCount; i++)
295   {
296     Enemy *enemy = &enemies[i];
297     if (enemy->enemyType == ENEMY_TYPE_NONE)
298     {
299       continue;
300     }
301     enemy->contactTime -= gameTime.deltaTime;
302     if (enemy->contactTime < 0.0f)
303     {
304       enemy->contactTime = 0.0f;
305     }
306 
307     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308     // linear search over towers; could be optimized by using path finding tower map,
309     // but for now, we keep it simple
310     for (int j = 0; j < towerCount; j++)
311     {
312       Tower *tower = &towers[j];
313       if (tower->towerType == TOWER_TYPE_NONE)
314       {
315         continue;
316       }
317       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319       if (distanceSqr > combinedRadius * combinedRadius)
320       {
321         continue;
322       }
323       // potential collision; square / circle intersection
324       float dx = tower->x - enemy->simPosition.x;
325       float dy = tower->y - enemy->simPosition.y;
326       float absDx = fabsf(dx);
327       float absDy = fabsf(dy);
328       Vector3 contactPoint = {0};
329       if (absDx <= 0.5f && absDx <= absDy) {
330         // vertical collision; push the enemy out horizontally
331         float overlap = enemyRadius + 0.5f - absDy;
332         if (overlap < 0.0f)
333         {
334           continue;
335         }
336         float direction = dy > 0.0f ? -1.0f : 1.0f;
337         enemy->simPosition.y += direction * overlap;
338         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339       }
340       else if (absDy <= 0.5f && absDy <= absDx)
341       {
342         // horizontal collision; push the enemy out vertically
343         float overlap = enemyRadius + 0.5f - absDx;
344         if (overlap < 0.0f)
345         {
346           continue;
347         }
348         float direction = dx > 0.0f ? -1.0f : 1.0f;
349         enemy->simPosition.x += direction * overlap;
350         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351       }
352       else
353       {
354         // possible collision with a corner
355         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357         float cornerX = tower->x + cornerDX;
358         float cornerY = tower->y + cornerDY;
359         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360         if (cornerDistanceSqr > enemyRadius * enemyRadius)
361         {
362           continue;
363         }
364         // push the enemy out along the diagonal
365         float cornerDistance = sqrtf(cornerDistanceSqr);
366         float overlap = enemyRadius - cornerDistance;
367         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369         enemy->simPosition.x -= directionX * overlap;
370         enemy->simPosition.y -= directionY * overlap;
371         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372       }
373 
374       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375       {
376         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378         {
379           EnemyTriggerExplode(enemy, tower, contactPoint);
380         }
381       }
382     }
383   }
384 }
385 
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388   return (EnemyId){enemy - enemies, enemy->generation};
389 }
390 
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393   if (enemyId.index >= ENEMY_MAX_COUNT)
394   {
395     return 0;
396   }
397   Enemy *enemy = &enemies[enemyId.index];
398   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399   {
400     return 0;
401   }
402   return enemy;
403 }
404 
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407   Enemy *spawn = 0;
408   for (int i = 0; i < enemyCount; i++)
409   {
410     Enemy *enemy = &enemies[i];
411     if (enemy->enemyType == ENEMY_TYPE_NONE)
412     {
413       spawn = enemy;
414       break;
415     }
416   }
417 
418   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419   {
420     spawn = &enemies[enemyCount++];
421   }
422 
423   if (spawn)
424   {
425     spawn->currentX = currentX;
426     spawn->currentY = currentY;
427     spawn->nextX = currentX;
428     spawn->nextY = currentY;
429     spawn->simPosition = (Vector2){currentX, currentY};
430     spawn->simVelocity = (Vector2){0, 0};
431     spawn->enemyType = enemyType;
432     spawn->startMovingTime = gameTime.time;
433     spawn->damage = 0.0f;
434     spawn->futureDamage = 0.0f;
435     spawn->generation++;
436     spawn->movePathCount = 0;
437     spawn->walkedDistance = 0.0f;
438   }
439 
440   return spawn;
441 }
442 
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445   int count = 0;
446   float range2 = range * range;
447   for (int i = 0; i < enemyCount; i++)
448   {
449     Enemy *enemy = &enemies[i];
450     if (enemy->enemyType == ENEMY_TYPE_NONE)
451     {
452       continue;
453     }
454     float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455     if (distance2 <= range2)
456     {
457       EnemyAddDamage(enemy, damage);
458       count++;
459     }
460   }
461   return count;
462 }
463 
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466   enemy->damage += damage;
467   if (enemy->damage >= EnemyGetMaxHealth(enemy))
468   {
469     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470     enemy->enemyType = ENEMY_TYPE_NONE;
471     return 1;
472   }
473 
474   return 0;
475 }
476 
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479   int16_t castleX = 0;
480   int16_t castleY = 0;
481   Enemy* closest = 0;
482   int16_t closestDistance = 0;
483   float range2 = range * range;
484   for (int i = 0; i < enemyCount; i++)
485   {
486     Enemy* enemy = &enemies[i];
487     if (enemy->enemyType == ENEMY_TYPE_NONE)
488     {
489       continue;
490     }
491     float maxHealth = EnemyGetMaxHealth(enemy);
492     if (enemy->futureDamage >= maxHealth)
493     {
494       // ignore enemies that will die soon
495       continue;
496     }
497     int16_t dx = castleX - enemy->currentX;
498     int16_t dy = castleY - enemy->currentY;
499     int16_t distance = abs(dx) + abs(dy);
500     if (!closest || distance < closestDistance)
501     {
502       float tdx = towerX - enemy->currentX;
503       float tdy = towerY - enemy->currentY;
504       float tdistance2 = tdx * tdx + tdy * tdy;
505       if (tdistance2 <= range2)
506       {
507         closest = enemy;
508         closestDistance = distance;
509       }
510     }
511   }
512   return closest;
513 }
514 
515 int EnemyCount()
516 {
517   int count = 0;
518   for (int i = 0; i < enemyCount; i++)
519   {
520     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521     {
522       count++;
523     }
524   }
525   return count;
526 }
527 
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530   for (int i = 0; i < enemyCount; i++)
531   {
532     Enemy *enemy = &enemies[i];
533     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534     {
535       continue;
536     }
537     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538     float maxHealth = EnemyGetMaxHealth(enemy);
539     float health = maxHealth - enemy->damage;
540     float healthRatio = health / maxHealth;
541     
542     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543   }
544 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 typedef struct ProjectileConfig
  8 {
  9   float arcFactor;
 10   Color color;
 11   Color trailColor;
 12 } ProjectileConfig;
 13 
 14 ProjectileConfig projectileConfigs[] = {
 15     [PROJECTILE_TYPE_ARROW] = {
 16         .arcFactor = 0.15f,
 17         .color = RED,
 18         .trailColor = BROWN,
 19     },
 20     [PROJECTILE_TYPE_CATAPULT] = {
 21         .arcFactor = 0.5f,
 22         .color = RED,
 23         .trailColor = GRAY,
 24     },
 25     [PROJECTILE_TYPE_BALLISTA] = {
 26         .arcFactor = 0.025f,
 27         .color = RED,
 28         .trailColor = BROWN,
 29     },
 30 };
 31 
 32 void ProjectileInit()
 33 {
 34   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 35   {
 36     projectiles[i] = (Projectile){0};
 37   }
 38 }
 39 
 40 void ProjectileDraw()
 41 {
 42   for (int i = 0; i < projectileCount; i++)
 43   {
 44     Projectile projectile = projectiles[i];
 45     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 46     {
 47       continue;
 48     }
 49     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 50     if (transition >= 1.0f)
 51     {
 52       continue;
 53     }
 54 
 55     ProjectileConfig config = projectileConfigs[projectile.projectileType];
 56     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 57     {
 58       float t = transition + transitionOffset * 0.3f;
 59       if (t > 1.0f)
 60       {
 61         break;
 62       }
 63       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 64       Color color = config.color;
 65       color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
 66       // fake a ballista flight path using parabola equation
 67       float parabolaT = t - 0.5f;
 68       parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 69       position.y += config.arcFactor * parabolaT * projectile.distance;
 70       
 71       float size = 0.06f * (transitionOffset + 0.25f);
 72       DrawCube(position, size, size, size, color);
 73     }
 74   }
 75 }
 76 
 77 void ProjectileUpdate()
 78 {
 79   for (int i = 0; i < projectileCount; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 87     if (transition >= 1.0f)
 88     {
 89       projectile->projectileType = PROJECTILE_TYPE_NONE;
 90       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 91       if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
 92       {
 93           Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
 94           enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
 95       }
 96       
 97       if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
 98       {
 99         EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100         // pancaked sphere explosion
101         float r = projectile->hitEffectConfig.areaDamageRadius;
102         ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103       }
104       else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105       {
106         EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107       }
108       continue;
109     }
110   }
111 }
112 
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116   {
117     Projectile *projectile = &projectiles[i];
118     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119     {
120       projectile->projectileType = projectileType;
121       projectile->shootTime = gameTime.time;
122       float distance = Vector3Distance(position, target);
123       projectile->arrivalTime = gameTime.time + distance / speed;
124       projectile->position = position;
125       projectile->target = target;
126       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127       projectile->distance = distance;
128       projectile->targetEnemy = EnemyGetId(enemy);
129       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130       projectile->hitEffectConfig = hitEffectConfig;
131       return projectile;
132     }
133   }
134   return 0;
135 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 
  5 static Particle particles[PARTICLE_MAX_COUNT];
  6 static int particleCount = 0;
  7 
  8 void ParticleInit()
  9 {
 10   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 11   {
 12     particles[i] = (Particle){0};
 13   }
 14   particleCount = 0;
 15 }
 16 
 17 static void DrawExplosionParticle(Particle *particle, float transition)
 18 {
 19   Vector3 scale = particle->scale;
 20   float size = 1.0f * (1.0f - transition);
 21   Color startColor = WHITE;
 22   Color endColor = RED;
 23   Color color = ColorLerp(startColor, endColor, transition);
 24 
 25   rlPushMatrix();
 26   rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
 27   rlScalef(scale.x, scale.y, scale.z);
 28   DrawSphere(Vector3Zero(), size, color);
 29   rlPopMatrix();
 30 }
 31 
 32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
 33 {
 34   if (particleCount >= PARTICLE_MAX_COUNT)
 35   {
 36     return;
 37   }
 38 
 39   int index = -1;
 40   for (int i = 0; i < particleCount; i++)
 41   {
 42     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 43     {
 44       index = i;
 45       break;
 46     }
 47   }
 48 
 49   if (index == -1)
 50   {
 51     index = particleCount++;
 52   }
 53 
 54   Particle *particle = &particles[index];
 55   particle->particleType = particleType;
 56   particle->spawnTime = gameTime.time;
 57   particle->lifetime = lifetime;
 58   particle->position = position;
 59   particle->velocity = velocity;
 60   particle->scale = scale;
 61 }
 62 
 63 void ParticleUpdate()
 64 {
 65   for (int i = 0; i < particleCount; i++)
 66   {
 67     Particle *particle = &particles[i];
 68     if (particle->particleType == PARTICLE_TYPE_NONE)
 69     {
 70       continue;
 71     }
 72 
 73     float age = gameTime.time - particle->spawnTime;
 74 
 75     if (particle->lifetime > age)
 76     {
 77       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 78     }
 79     else {
 80       particle->particleType = PARTICLE_TYPE_NONE;
 81     }
 82   }
 83 }
 84 
 85 void ParticleDraw()
 86 {
 87   for (int i = 0; i < particleCount; i++)
 88   {
 89     Particle particle = particles[i];
 90     if (particle.particleType == PARTICLE_TYPE_NONE)
 91     {
 92       continue;
 93     }
 94 
 95     float age = gameTime.time - particle.spawnTime;
 96     float transition = age / particle.lifetime;
 97     switch (particle.particleType)
 98     {
 99     case PARTICLE_TYPE_EXPLOSION:
100       DrawExplosionParticle(&particle, transition);
101       break;
102     default:
103       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104       break;
105     }
106   }
107 }
  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
Building placement involves now confirming the placement with the "Build" button.

The build mode is entered when selecting a tower to build. The OK and Cancel buttons are shown on the screen and we can drag and click on the map to move the wireframe tower around as long as we want. Accidental builds are prevented by the OK button.

Prototyping-wise this could be considered done. But let's polish this!

Polishing the tower placement

The first thing to improve is to show the tower that is to be built at the current location. To communicate that the tower can be moved around, we will add a set of animated arrows that bounce around the tower.

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 #include <stdlib.h>
  5 #include <math.h>
  6 
  7 //# Variables
  8 GUIState guiState = {0};
  9 GameTime gameTime = {0};
 10 
 11 Model floorTileAModel = {0};
 12 Model floorTileBModel = {0};
 13 Model treeModel[2] = {0};
 14 Model firTreeModel[2] = {0};
 15 Model rockModels[5] = {0};
 16 Model grassPatchModel[1] = {0};
 17 
 18 Model pathArrowModel = {0};
19 Model greenArrowModel = {0};
20 21 Texture2D palette, spriteSheet; 22 23 Level levels[] = { 24 [0] = { 25 .state = LEVEL_STATE_BUILDING, 26 .initialGold = 20, 27 .waves[0] = { 28 .enemyType = ENEMY_TYPE_MINION, 29 .wave = 0, 30 .count = 5, 31 .interval = 2.5f, 32 .delay = 1.0f, 33 .spawnPosition = {2, 6}, 34 }, 35 .waves[1] = { 36 .enemyType = ENEMY_TYPE_MINION, 37 .wave = 0, 38 .count = 5, 39 .interval = 2.5f, 40 .delay = 1.0f, 41 .spawnPosition = {-2, 6}, 42 }, 43 .waves[2] = { 44 .enemyType = ENEMY_TYPE_MINION, 45 .wave = 1, 46 .count = 20, 47 .interval = 1.5f, 48 .delay = 1.0f, 49 .spawnPosition = {0, 6}, 50 }, 51 .waves[3] = { 52 .enemyType = ENEMY_TYPE_MINION, 53 .wave = 2, 54 .count = 30, 55 .interval = 1.2f, 56 .delay = 1.0f, 57 .spawnPosition = {0, 6}, 58 } 59 }, 60 }; 61 62 Level *currentLevel = levels; 63 64 //# Game 65 66 static Model LoadGLBModel(char *filename) 67 { 68 Model model = LoadModel(TextFormat("data/%s.glb",filename)); 69 for (int i = 0; i < model.materialCount; i++) 70 { 71 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 72 } 73 return model; 74 } 75 76 void LoadAssets() 77 { 78 // load a sprite sheet that contains all units 79 spriteSheet = LoadTexture("data/spritesheet.png"); 80 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR); 81 82 // we'll use a palette texture to colorize the all buildings and environment art 83 palette = LoadTexture("data/palette.png"); 84 // The texture uses gradients on very small space, so we'll enable bilinear filtering 85 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 86 87 floorTileAModel = LoadGLBModel("floor-tile-a"); 88 floorTileBModel = LoadGLBModel("floor-tile-b"); 89 treeModel[0] = LoadGLBModel("leaftree-large-1-a"); 90 treeModel[1] = LoadGLBModel("leaftree-large-1-b"); 91 firTreeModel[0] = LoadGLBModel("firtree-1-a"); 92 firTreeModel[1] = LoadGLBModel("firtree-1-b"); 93 rockModels[0] = LoadGLBModel("rock-1"); 94 rockModels[1] = LoadGLBModel("rock-2"); 95 rockModels[2] = LoadGLBModel("rock-3"); 96 rockModels[3] = LoadGLBModel("rock-4"); 97 rockModels[4] = LoadGLBModel("rock-5"); 98 grassPatchModel[0] = LoadGLBModel("grass-patch-1"); 99
100 pathArrowModel = LoadGLBModel("direction-arrow-x"); 101 greenArrowModel = LoadGLBModel("green-arrow");
102 } 103 104 void InitLevel(Level *level) 105 { 106 level->seed = (int)(GetTime() * 100.0f); 107 108 TowerInit(); 109 EnemyInit(); 110 ProjectileInit(); 111 ParticleInit(); 112 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 113 114 level->placementMode = 0; 115 level->state = LEVEL_STATE_BUILDING; 116 level->nextState = LEVEL_STATE_NONE; 117 level->playerGold = level->initialGold; 118 level->currentWave = 0; 119 120 Camera *camera = &level->camera; 121 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 122 camera->target = (Vector3){0.0f, 0.0f, 0.0f}; 123 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 124 camera->fovy = 10.0f; 125 camera->projection = CAMERA_ORTHOGRAPHIC; 126 } 127 128 void DrawLevelHud(Level *level) 129 { 130 const char *text = TextFormat("Gold: %d", level->playerGold); 131 Font font = GetFontDefault(); 132 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 133 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 134 } 135 136 void DrawLevelReportLostWave(Level *level) 137 { 138 BeginMode3D(level->camera); 139 DrawLevelGround(level); 140 TowerDraw(); 141 EnemyDraw(); 142 ProjectileDraw(); 143 ParticleDraw(); 144 guiState.isBlocked = 0; 145 EndMode3D(); 146 147 TowerDrawHealthBars(level->camera); 148 149 const char *text = "Wave lost"; 150 int textWidth = MeasureText(text, 20); 151 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 152 153 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 154 { 155 level->nextState = LEVEL_STATE_RESET; 156 } 157 } 158 159 int HasLevelNextWave(Level *level) 160 { 161 for (int i = 0; i < 10; i++) 162 { 163 EnemyWave *wave = &level->waves[i]; 164 if (wave->wave == level->currentWave) 165 { 166 return 1; 167 } 168 } 169 return 0; 170 } 171 172 void DrawLevelReportWonWave(Level *level) 173 { 174 BeginMode3D(level->camera); 175 DrawLevelGround(level); 176 TowerDraw(); 177 EnemyDraw(); 178 ProjectileDraw(); 179 ParticleDraw(); 180 guiState.isBlocked = 0; 181 EndMode3D(); 182 183 TowerDrawHealthBars(level->camera); 184 185 const char *text = "Wave won"; 186 int textWidth = MeasureText(text, 20); 187 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 188 189 190 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 191 { 192 level->nextState = LEVEL_STATE_RESET; 193 } 194 195 if (HasLevelNextWave(level)) 196 { 197 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 198 { 199 level->nextState = LEVEL_STATE_BUILDING; 200 } 201 } 202 else { 203 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 204 { 205 level->nextState = LEVEL_STATE_WON_LEVEL; 206 } 207 } 208 } 209 210 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 211 { 212 static ButtonState buttonStates[8] = {0}; 213 int cost = GetTowerCosts(towerType); 214 const char *text = TextFormat("%s: %d", name, cost); 215 buttonStates[towerType].isSelected = level->placementMode == towerType; 216 buttonStates[towerType].isDisabled = level->playerGold < cost; 217 if (Button(text, x, y, width, height, &buttonStates[towerType])) 218 { 219 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 220 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT; 221 } 222 } 223 224 float GetRandomFloat(float min, float max) 225 { 226 int random = GetRandomValue(0, 0xfffffff); 227 return ((float)random / (float)0xfffffff) * (max - min) + min; 228 } 229 230 void DrawLevelGround(Level *level) 231 { 232 // draw checkerboard ground pattern 233 for (int x = -5; x <= 5; x += 1) 234 { 235 for (int y = -5; y <= 5; y += 1) 236 { 237 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 238 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 239 } 240 } 241 242 int oldSeed = GetRandomValue(0, 0xfffffff); 243 SetRandomSeed(level->seed); 244 // increase probability for trees via duplicated entries 245 Model borderModels[64]; 246 int maxRockCount = GetRandomValue(2, 6); 247 int maxTreeCount = GetRandomValue(10, 20); 248 int maxFirTreeCount = GetRandomValue(5, 10); 249 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount; 250 int grassPatchCount = GetRandomValue(5, 30); 251 252 int modelCount = 0; 253 for (int i = 0; i < maxRockCount && modelCount < 63; i++) 254 { 255 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)]; 256 } 257 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++) 258 { 259 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)]; 260 } 261 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++) 262 { 263 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)]; 264 } 265 for (int i = 0; i < grassPatchCount && modelCount < 63; i++) 266 { 267 borderModels[modelCount++] = grassPatchModel[0]; 268 } 269 270 // draw some objects around the border of the map 271 Vector3 up = {0, 1, 0}; 272 // a pseudo random number generator to get the same result every time 273 const float wiggle = 0.75f; 274 const int layerCount = 3; 275 for (int layer = 0; layer < layerCount; layer++) 276 { 277 int layerPos = 6 + layer; 278 for (int x = -6 + layer; x <= 6 + layer; x += 1) 279 { 280 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 281 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 282 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 283 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 284 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 285 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 286 } 287 288 for (int z = -5 + layer; z <= 5 + layer; z += 1) 289 { 290 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 291 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 292 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 293 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 294 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 295 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 296 } 297 } 298 299 SetRandomSeed(oldSeed); 300 } 301 302 void DrawEnemyPath(Level *level, Color arrowColor) 303 { 304 const int castleX = 0, castleY = 0; 305 const int maxWaypointCount = 200; 306 const float timeStep = 1.0f; 307 Vector3 arrowScale = {0.75f, 0.75f, 0.75f}; 308 309 // we start with a time offset to simulate the path, 310 // this way the arrows are animated in a forward moving direction 311 // The time is wrapped around the time step to get a smooth animation 312 float timeOffset = fmodf(GetTime(), timeStep); 313 314 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 315 { 316 EnemyWave *wave = &level->waves[i]; 317 if (wave->wave != level->currentWave) 318 { 319 continue; 320 } 321 322 // use this dummy enemy to simulate the path 323 Enemy dummy = { 324 .enemyType = ENEMY_TYPE_MINION, 325 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y}, 326 .nextX = wave->spawnPosition.x, 327 .nextY = wave->spawnPosition.y, 328 .currentX = wave->spawnPosition.x, 329 .currentY = wave->spawnPosition.y, 330 }; 331 332 float deltaTime = timeOffset; 333 for (int j = 0; j < maxWaypointCount; j++) 334 { 335 int waypointPassedCount = 0; 336 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount); 337 // after the initial variable starting offset, we use a fixed time step 338 deltaTime = timeStep; 339 dummy.simPosition = pos; 340 341 // Update the dummy's position just like we do in the regular enemy update loop 342 for (int k = 0; k < waypointPassedCount; k++) 343 { 344 dummy.currentX = dummy.nextX; 345 dummy.currentY = dummy.nextY; 346 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) && 347 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 348 { 349 break; 350 } 351 } 352 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 353 { 354 break; 355 } 356 357 // get the angle we need to rotate the arrow model. The velocity is just fine for this. 358 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f; 359 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor); 360 } 361 } 362 } 363 364 void DrawEnemyPaths(Level *level) 365 { 366 // disable depth testing for the path arrows 367 // flush the 3D batch to draw the arrows on top of everything 368 rlDrawRenderBatchActive(); 369 rlDisableDepthTest(); 370 DrawEnemyPath(level, (Color){64, 64, 64, 160}); 371 372 rlDrawRenderBatchActive(); 373 rlEnableDepthTest(); 374 DrawEnemyPath(level, WHITE); 375 } 376 377 void DrawLevelBuildingPlacementState(Level *level) 378 { 379 BeginMode3D(level->camera); 380 DrawLevelGround(level); 381 382 int blockedCellCount = 0; 383 Vector2 blockedCells[1]; 384 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 385 float planeDistance = ray.position.y / -ray.direction.y; 386 float planeX = ray.direction.x * planeDistance + ray.position.x; 387 float planeY = ray.direction.z * planeDistance + ray.position.z; 388 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 389 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 390 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) 391 { 392 level->placementX = mapX; 393 level->placementY = mapY; 394 } 395 else 396 { 397 mapX = level->placementX;
398 mapY = level->placementY; 399 }
400 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY}; 401 PathFindingMapUpdate(blockedCellCount, blockedCells); 402 403 TowerDraw(); 404 EnemyDraw(); 405 ProjectileDraw();
406 ParticleDraw(); 407 DrawEnemyPaths(level); 408 409 rlPushMatrix(); 410 rlTranslatef(mapX, 0, mapY); 411 DrawCubeWires((Vector3){0.0f, 0.0f, 0.0f}, 1.0f, 0.0f, 1.0f, RED); 412 Tower dummy = { 413 .towerType = level->placementMode, 414 }; 415 TowerDrawSingle(dummy); 416 417 float bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f; 418 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f; 419 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f; 420 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f; 421 422 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE); 423 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE); 424 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE); 425 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
426 rlPopMatrix(); 427 428 guiState.isBlocked = 0; 429 430 EndMode3D(); 431 432 TowerDrawHealthBars(level->camera); 433 434 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0)) 435 { 436 level->nextState = LEVEL_STATE_BUILDING; 437 level->placementMode = TOWER_TYPE_NONE; 438 TraceLog(LOG_INFO, "Cancel building"); 439 } 440 441 if (Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0)) 442 { 443 level->nextState = LEVEL_STATE_BUILDING; 444 if (TowerTryAdd(level->placementMode, mapX, mapY)) 445 { 446 level->playerGold -= GetTowerCosts(level->placementMode); 447 level->placementMode = TOWER_TYPE_NONE; 448 } 449 } 450 } 451 452 void DrawLevelBuildingState(Level *level) 453 { 454 BeginMode3D(level->camera); 455 DrawLevelGround(level); 456 457 PathFindingMapUpdate(0, 0); 458 TowerDraw(); 459 EnemyDraw(); 460 ProjectileDraw(); 461 ParticleDraw(); 462 DrawEnemyPaths(level); 463 464 guiState.isBlocked = 0; 465 466 EndMode3D(); 467 468 TowerDrawHealthBars(level->camera); 469 470 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 471 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer"); 472 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 473 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult"); 474 475 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 476 { 477 level->nextState = LEVEL_STATE_RESET; 478 } 479 480 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 481 { 482 level->nextState = LEVEL_STATE_BATTLE; 483 } 484 485 const char *text = "Building phase"; 486 int textWidth = MeasureText(text, 20); 487 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 488 } 489 490 void InitBattleStateConditions(Level *level) 491 { 492 level->state = LEVEL_STATE_BATTLE; 493 level->nextState = LEVEL_STATE_NONE; 494 level->waveEndTimer = 0.0f; 495 for (int i = 0; i < 10; i++) 496 { 497 EnemyWave *wave = &level->waves[i]; 498 wave->spawned = 0; 499 wave->timeToSpawnNext = wave->delay; 500 } 501 } 502 503 void DrawLevelBattleState(Level *level) 504 { 505 BeginMode3D(level->camera); 506 DrawLevelGround(level); 507 TowerDraw(); 508 EnemyDraw(); 509 ProjectileDraw(); 510 ParticleDraw(); 511 guiState.isBlocked = 0; 512 EndMode3D(); 513 514 EnemyDrawHealthbars(level->camera); 515 TowerDrawHealthBars(level->camera); 516 517 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 518 { 519 level->nextState = LEVEL_STATE_RESET; 520 } 521 522 int maxCount = 0; 523 int remainingCount = 0; 524 for (int i = 0; i < 10; i++) 525 { 526 EnemyWave *wave = &level->waves[i]; 527 if (wave->wave != level->currentWave) 528 { 529 continue; 530 } 531 maxCount += wave->count; 532 remainingCount += wave->count - wave->spawned; 533 } 534 int aliveCount = EnemyCount(); 535 remainingCount += aliveCount; 536 537 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 538 int textWidth = MeasureText(text, 20); 539 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 540 } 541 542 void DrawLevel(Level *level) 543 { 544 switch (level->state) 545 { 546 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 547 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break; 548 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 549 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 550 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 551 default: break; 552 } 553 554 DrawLevelHud(level); 555 } 556 557 void UpdateLevel(Level *level) 558 { 559 if (level->state == LEVEL_STATE_BATTLE) 560 { 561 int activeWaves = 0; 562 for (int i = 0; i < 10; i++) 563 { 564 EnemyWave *wave = &level->waves[i]; 565 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 566 { 567 continue; 568 } 569 activeWaves++; 570 wave->timeToSpawnNext -= gameTime.deltaTime; 571 if (wave->timeToSpawnNext <= 0.0f) 572 { 573 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 574 if (enemy) 575 { 576 wave->timeToSpawnNext = wave->interval; 577 wave->spawned++; 578 } 579 } 580 } 581 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 582 level->waveEndTimer += gameTime.deltaTime; 583 if (level->waveEndTimer >= 2.0f) 584 { 585 level->nextState = LEVEL_STATE_LOST_WAVE; 586 } 587 } 588 else if (activeWaves == 0 && EnemyCount() == 0) 589 { 590 level->waveEndTimer += gameTime.deltaTime; 591 if (level->waveEndTimer >= 2.0f) 592 { 593 level->nextState = LEVEL_STATE_WON_WAVE; 594 } 595 } 596 } 597 598 PathFindingMapUpdate(0, 0); 599 EnemyUpdate(); 600 TowerUpdate(); 601 ProjectileUpdate(); 602 ParticleUpdate(); 603 604 if (level->nextState == LEVEL_STATE_RESET) 605 { 606 InitLevel(level); 607 } 608 609 if (level->nextState == LEVEL_STATE_BATTLE) 610 { 611 InitBattleStateConditions(level); 612 } 613 614 if (level->nextState == LEVEL_STATE_WON_WAVE) 615 { 616 level->currentWave++; 617 level->state = LEVEL_STATE_WON_WAVE; 618 } 619 620 if (level->nextState == LEVEL_STATE_LOST_WAVE) 621 { 622 level->state = LEVEL_STATE_LOST_WAVE; 623 } 624 625 if (level->nextState == LEVEL_STATE_BUILDING) 626 { 627 level->state = LEVEL_STATE_BUILDING; 628 } 629 630 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT) 631 { 632 level->state = LEVEL_STATE_BUILDING_PLACEMENT; 633 } 634 635 if (level->nextState == LEVEL_STATE_WON_LEVEL) 636 { 637 // make something of this later 638 InitLevel(level); 639 } 640 641 level->nextState = LEVEL_STATE_NONE; 642 } 643 644 float nextSpawnTime = 0.0f; 645 646 void ResetGame() 647 { 648 InitLevel(currentLevel); 649 } 650 651 void InitGame() 652 { 653 TowerInit(); 654 EnemyInit(); 655 ProjectileInit(); 656 ParticleInit(); 657 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 658 659 currentLevel = levels; 660 InitLevel(currentLevel); 661 } 662 663 //# Immediate GUI functions 664 665 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 666 { 667 const float healthBarHeight = 6.0f; 668 const float healthBarOffset = 15.0f; 669 const float inset = 2.0f; 670 const float innerWidth = healthBarWidth - inset * 2; 671 const float innerHeight = healthBarHeight - inset * 2; 672 673 Vector2 screenPos = GetWorldToScreen(position, camera); 674 float centerX = screenPos.x - healthBarWidth * 0.5f; 675 float topY = screenPos.y - healthBarOffset; 676 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 677 float healthWidth = innerWidth * healthRatio; 678 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 679 } 680 681 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 682 { 683 Rectangle bounds = {x, y, width, height}; 684 int isPressed = 0; 685 int isSelected = state && state->isSelected; 686 int isDisabled = state && state->isDisabled; 687 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 688 { 689 Color color = isSelected ? DARKGRAY : GRAY; 690 DrawRectangle(x, y, width, height, color); 691 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 692 { 693 isPressed = 1; 694 } 695 guiState.isBlocked = 1; 696 } 697 else 698 { 699 Color color = isSelected ? WHITE : LIGHTGRAY; 700 DrawRectangle(x, y, width, height, color); 701 } 702 Font font = GetFontDefault(); 703 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 704 Color textColor = isDisabled ? GRAY : BLACK; 705 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 706 return isPressed; 707 } 708 709 //# Main game loop 710 711 void GameUpdate() 712 { 713 float dt = GetFrameTime(); 714 // cap maximum delta time to 0.1 seconds to prevent large time steps 715 if (dt > 0.1f) dt = 0.1f; 716 gameTime.time += dt; 717 gameTime.deltaTime = dt; 718 719 UpdateLevel(currentLevel); 720 } 721 722 int main(void) 723 { 724 int screenWidth, screenHeight; 725 GetPreferredSize(&screenWidth, &screenHeight); 726 InitWindow(screenWidth, screenHeight, "Tower defense"); 727 SetTargetFPS(30); 728 729 LoadAssets(); 730 InitGame(); 731 732 while (!WindowShouldClose()) 733 { 734 if (IsPaused()) { 735 // canvas is not visible in browser - do nothing 736 continue; 737 } 738 739 BeginDrawing(); 740 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 741 742 GameUpdate(); 743 DrawLevel(currentLevel); 744 745 EndDrawing(); 746 } 747 748 CloseWindow(); 749 750 return 0; 751 }
  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 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27   Vector3 scale;
 28 } Particle;
 29 
 30 #define TOWER_MAX_COUNT 400
 31 enum TowerType
 32 {
 33   TOWER_TYPE_NONE,
 34   TOWER_TYPE_BASE,
 35   TOWER_TYPE_ARCHER,
 36   TOWER_TYPE_BALLISTA,
 37   TOWER_TYPE_CATAPULT,
 38   TOWER_TYPE_WALL,
 39   TOWER_TYPE_COUNT
 40 };
 41 
 42 typedef struct HitEffectConfig
 43 {
 44   float damage;
 45   float areaDamageRadius;
 46   float pushbackPowerDistance;
 47 } HitEffectConfig;
 48 
 49 typedef struct TowerTypeConfig
 50 {
 51   float cooldown;
 52   float range;
 53   float projectileSpeed;
 54   
 55   uint8_t cost;
 56   uint8_t projectileType;
 57   uint16_t maxHealth;
 58 
 59   HitEffectConfig hitEffect;
 60 } TowerTypeConfig;
 61 
 62 typedef struct Tower
 63 {
 64   int16_t x, y;
 65   uint8_t towerType;
 66   Vector2 lastTargetPosition;
 67   float cooldown;
 68   float damage;
 69 } Tower;
 70 
 71 typedef struct GameTime
 72 {
 73   float time;
 74   float deltaTime;
 75 } GameTime;
 76 
 77 typedef struct ButtonState {
 78   char isSelected;
 79   char isDisabled;
 80 } ButtonState;
 81 
 82 typedef struct GUIState {
 83   int isBlocked;
 84 } GUIState;
 85 
 86 typedef enum LevelState
 87 {
 88   LEVEL_STATE_NONE,
 89   LEVEL_STATE_BUILDING,
 90   LEVEL_STATE_BUILDING_PLACEMENT,
 91   LEVEL_STATE_BATTLE,
 92   LEVEL_STATE_WON_WAVE,
 93   LEVEL_STATE_LOST_WAVE,
 94   LEVEL_STATE_WON_LEVEL,
 95   LEVEL_STATE_RESET,
 96 } LevelState;
 97 
 98 typedef struct EnemyWave {
 99   uint8_t enemyType;
100   uint8_t wave;
101   uint16_t count;
102   float interval;
103   float delay;
104   Vector2 spawnPosition;
105 
106   uint16_t spawned;
107   float timeToSpawnNext;
108 } EnemyWave;
109 
110 #define ENEMY_MAX_WAVE_COUNT 10
111 
112 typedef struct Level
113 {
114   int seed;
115   LevelState state;
116   LevelState nextState;
117   Camera3D camera;
118   int placementMode;
119   int16_t placementX;
120   int16_t placementY;
121 Vector2 placementTransitionPosition;
122 123 int initialGold; 124 int playerGold; 125 126 EnemyWave waves[ENEMY_MAX_WAVE_COUNT]; 127 int currentWave; 128 float waveEndTimer; 129 } Level; 130 131 typedef struct DeltaSrc 132 { 133 char x, y; 134 } DeltaSrc; 135 136 typedef struct PathfindingMap 137 { 138 int width, height; 139 float scale; 140 float *distances; 141 long *towerIndex; 142 DeltaSrc *deltaSrc; 143 float maxDistance; 144 Matrix toMapSpace; 145 Matrix toWorldSpace; 146 } PathfindingMap; 147 148 // when we execute the pathfinding algorithm, we need to store the active nodes 149 // in a queue. Each node has a position, a distance from the start, and the 150 // position of the node that we came from. 151 typedef struct PathfindingNode 152 { 153 int16_t x, y, fromX, fromY; 154 float distance; 155 } PathfindingNode; 156 157 typedef struct EnemyId 158 { 159 uint16_t index; 160 uint16_t generation; 161 } EnemyId; 162 163 typedef struct EnemyClassConfig 164 { 165 float speed; 166 float health; 167 float radius; 168 float maxAcceleration; 169 float requiredContactTime; 170 float explosionDamage; 171 float explosionRange; 172 float explosionPushbackPower; 173 int goldValue; 174 } EnemyClassConfig; 175 176 typedef struct Enemy 177 { 178 int16_t currentX, currentY; 179 int16_t nextX, nextY; 180 Vector2 simPosition; 181 Vector2 simVelocity; 182 uint16_t generation; 183 float walkedDistance; 184 float startMovingTime; 185 float damage, futureDamage; 186 float contactTime; 187 uint8_t enemyType; 188 uint8_t movePathCount; 189 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 190 } Enemy; 191 192 // a unit that uses sprites to be drawn 193 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0 194 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1 195 typedef struct SpriteUnit 196 { 197 Rectangle srcRect; 198 Vector2 offset; 199 int frameCount; 200 float frameDuration; 201 Rectangle srcWeaponIdleRect; 202 Vector2 srcWeaponIdleOffset; 203 Rectangle srcWeaponCooldownRect; 204 Vector2 srcWeaponCooldownOffset; 205 } SpriteUnit; 206 207 #define PROJECTILE_MAX_COUNT 1200 208 #define PROJECTILE_TYPE_NONE 0 209 #define PROJECTILE_TYPE_ARROW 1 210 #define PROJECTILE_TYPE_CATAPULT 2 211 #define PROJECTILE_TYPE_BALLISTA 3 212 213 typedef struct Projectile 214 { 215 uint8_t projectileType; 216 float shootTime; 217 float arrivalTime; 218 float distance; 219 Vector3 position; 220 Vector3 target; 221 Vector3 directionNormal; 222 EnemyId targetEnemy; 223 HitEffectConfig hitEffectConfig; 224 } Projectile; 225 226 //# Function declarations 227 float TowerGetMaxHealth(Tower *tower); 228 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 229 int EnemyAddDamageRange(Vector2 position, float range, float damage); 230 int EnemyAddDamage(Enemy *enemy, float damage); 231 232 //# Enemy functions 233 void EnemyInit(); 234 void EnemyDraw(); 235 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 236 void EnemyUpdate(); 237 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 238 float EnemyGetMaxHealth(Enemy *enemy); 239 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 240 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 241 EnemyId EnemyGetId(Enemy *enemy); 242 Enemy *EnemyTryResolve(EnemyId enemyId); 243 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 244 int EnemyAddDamage(Enemy *enemy, float damage); 245 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 246 int EnemyCount(); 247 void EnemyDrawHealthbars(Camera3D camera); 248 249 //# Tower functions 250 void TowerInit(); 251 Tower *TowerGetAt(int16_t x, int16_t y); 252 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 253 Tower *GetTowerByType(uint8_t towerType); 254 int GetTowerCosts(uint8_t towerType); 255 float TowerGetMaxHealth(Tower *tower);
256 void TowerDraw(); 257 void TowerDrawSingle(Tower tower);
258 void TowerUpdate(); 259 void TowerDrawHealthBars(Camera3D camera); 260 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 261 262 //# Particles 263 void ParticleInit(); 264 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime); 265 void ParticleUpdate(); 266 void ParticleDraw(); 267 268 //# Projectiles 269 void ProjectileInit(); 270 void ProjectileDraw(); 271 void ProjectileUpdate(); 272 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig); 273 274 //# Pathfinding map 275 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 276 float PathFindingGetDistance(int mapX, int mapY); 277 Vector2 PathFindingGetGradient(Vector3 world); 278 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 279 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells); 280 void PathFindingMapDraw(); 281 282 //# UI 283 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth); 284 285 //# Level 286 void DrawLevelGround(Level *level); 287 void DrawEnemyPath(Level *level, Color arrowColor); 288 289 //# variables 290 extern Level *currentLevel; 291 extern Enemy enemies[ENEMY_MAX_COUNT]; 292 extern int enemyCount; 293 extern EnemyClassConfig enemyClassConfigs[]; 294 295 extern GUIState guiState; 296 extern GameTime gameTime; 297 extern Tower towers[TOWER_MAX_COUNT]; 298 extern int towerCount; 299 300 extern Texture2D palette, spriteSheet; 301 302 #endif
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 // The queue is a simple array of nodes, we add nodes to the end and remove
  5 // nodes from the front. We keep the array around to avoid unnecessary allocations
  6 static PathfindingNode *pathfindingNodeQueue = 0;
  7 static int pathfindingNodeQueueCount = 0;
  8 static int pathfindingNodeQueueCapacity = 0;
  9 
 10 // The pathfinding map stores the distances from the castle to each cell in the map.
 11 static PathfindingMap pathfindingMap = {0};
 12 
 13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
 14 {
 15   // transforming between map space and world space allows us to adapt 
 16   // position and scale of the map without changing the pathfinding data
 17   pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
 18   pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
 19   pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
 20   pathfindingMap.width = width;
 21   pathfindingMap.height = height;
 22   pathfindingMap.scale = scale;
 23   pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
 24   for (int i = 0; i < width * height; i++)
 25   {
 26     pathfindingMap.distances[i] = -1.0f;
 27   }
 28 
 29   pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
 30   pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
 31 }
 32 
 33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
 34 {
 35   if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
 36   {
 37     pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
 38     // we use MemAlloc/MemRealloc to allocate memory for the queue
 39     // I am not entirely sure if MemRealloc allows passing a null pointer
 40     // so we check if the pointer is null and use MemAlloc in that case
 41     if (pathfindingNodeQueue == 0)
 42     {
 43       pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 44     }
 45     else
 46     {
 47       pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 48     }
 49   }
 50 
 51   PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
 52   node->x = x;
 53   node->y = y;
 54   node->fromX = fromX;
 55   node->fromY = fromY;
 56   node->distance = distance;
 57 }
 58 
 59 static PathfindingNode *PathFindingNodePop()
 60 {
 61   if (pathfindingNodeQueueCount == 0)
 62   {
 63     return 0;
 64   }
 65   // we return the first node in the queue; we want to return a pointer to the node
 66   // so we can return 0 if the queue is empty. 
 67   // We should _not_ return a pointer to the element in the list, because the list
 68   // may be reallocated and the pointer would become invalid. Or the 
 69   // popped element is overwritten by the next push operation.
 70   // Using static here means that the variable is permanently allocated.
 71   static PathfindingNode node;
 72   node = pathfindingNodeQueue[0];
 73   // we shift all nodes one position to the front
 74   for (int i = 1; i < pathfindingNodeQueueCount; i++)
 75   {
 76     pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
 77   }
 78   --pathfindingNodeQueueCount;
 79   return &node;
 80 }
 81 
 82 float PathFindingGetDistance(int mapX, int mapY)
 83 {
 84   if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
 85   {
 86     // when outside the map, we return the manhattan distance to the castle (0,0)
 87     return fabsf((float)mapX) + fabsf((float)mapY);
 88   }
 89 
 90   return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
 91 }
 92 
 93 // transform a world position to a map position in the array; 
 94 // returns true if the position is inside the map
 95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
 96 {
 97   Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
 98   *mapX = (int16_t)mapPosition.x;
 99   *mapY = (int16_t)mapPosition.z;
100   return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102 
103 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells)
104 {
105   const int castleX = 0, castleY = 0;
106   int16_t castleMapX, castleMapY;
107   if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108   {
109     return;
110   }
111   int width = pathfindingMap.width, height = pathfindingMap.height;
112 
113   // reset the distances to -1
114   for (int i = 0; i < width * height; i++)
115   {
116     pathfindingMap.distances[i] = -1.0f;
117   }
118   // reset the tower indices
119   for (int i = 0; i < width * height; i++)
120   {
121     pathfindingMap.towerIndex[i] = -1;
122   }
123   // reset the delta src
124   for (int i = 0; i < width * height; i++)
125   {
126     pathfindingMap.deltaSrc[i].x = 0;
127     pathfindingMap.deltaSrc[i].y = 0;
128   }
129 
130   for (int i = 0; i < blockedCellCount; i++)
131   {
132     int16_t mapX, mapY;
133     if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134     {
135       continue;
136     }
137     int index = mapY * width + mapX;
138     pathfindingMap.towerIndex[index] = -2;
139   }
140 
141   for (int i = 0; i < towerCount; i++)
142   {
143     Tower *tower = &towers[i];
144     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145     {
146       continue;
147     }
148     int16_t mapX, mapY;
149     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150     // this would not work correctly and needs to be refined to allow towers covering multiple cells
151     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152     // one cell. For now.
153     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154     {
155       continue;
156     }
157     int index = mapY * width + mapX;
158     pathfindingMap.towerIndex[index] = i;
159   }
160 
161   // we start at the castle and add the castle to the queue
162   pathfindingMap.maxDistance = 0.0f;
163   pathfindingNodeQueueCount = 0;
164   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165   PathfindingNode *node = 0;
166   while ((node = PathFindingNodePop()))
167   {
168     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169     {
170       continue;
171     }
172     int index = node->y * width + node->x;
173     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174     {
175       continue;
176     }
177 
178     int deltaX = node->x - node->fromX;
179     int deltaY = node->y - node->fromY;
180     // even if the cell is blocked by a tower, we still may want to store the direction
181     // (though this might not be needed, IDK right now)
182     pathfindingMap.deltaSrc[index].x = (char) deltaX;
183     pathfindingMap.deltaSrc[index].y = (char) deltaY;
184 
185     // we skip nodes that are blocked by towers or by the provided blocked cells
186     if (pathfindingMap.towerIndex[index] != -1)
187     {
188       node->distance += 8.0f;
189     }
190     pathfindingMap.distances[index] = node->distance;
191     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196   }
197 }
198 
199 void PathFindingMapDraw()
200 {
201   float cellSize = pathfindingMap.scale * 0.9f;
202   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203   for (int x = 0; x < pathfindingMap.width; x++)
204   {
205     for (int y = 0; y < pathfindingMap.height; y++)
206     {
207       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211       // animate the distance "wave" to show how the pathfinding algorithm expands
212       // from the castle
213       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214       {
215         color = BLACK;
216       }
217       DrawCube(position, cellSize, 0.1f, cellSize, color);
218     }
219   }
220 }
221 
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224   int16_t mapX, mapY;
225   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226   {
227     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228     return (Vector2){(float)-delta.x, (float)-delta.y};
229   }
230   // fallback to a simple gradient calculation
231   float n = PathFindingGetDistance(mapX, mapY - 1);
232   float s = PathFindingGetDistance(mapX, mapY + 1);
233   float w = PathFindingGetDistance(mapX - 1, mapY);
234   float e = PathFindingGetDistance(mapX + 1, mapY);
235   return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
  5     [TOWER_TYPE_BASE] = {
  6         .maxHealth = 10,
  7     },
  8     [TOWER_TYPE_ARCHER] = {
  9         .cooldown = 0.5f,
 10         .range = 3.0f,
 11         .cost = 6,
 12         .maxHealth = 10,
 13         .projectileSpeed = 4.0f,
 14         .projectileType = PROJECTILE_TYPE_ARROW,
 15         .hitEffect = {
 16           .damage = 3.0f,
 17         }
 18     },
 19     [TOWER_TYPE_BALLISTA] = {
 20         .cooldown = 1.5f,
 21         .range = 6.0f,
 22         .cost = 9,
 23         .maxHealth = 10,
 24         .projectileSpeed = 10.0f,
 25         .projectileType = PROJECTILE_TYPE_BALLISTA,
 26         .hitEffect = {
 27           .damage = 6.0f,
 28           .pushbackPowerDistance = 0.25f,
 29         }
 30     },
 31     [TOWER_TYPE_CATAPULT] = {
 32         .cooldown = 1.7f,
 33         .range = 5.0f,
 34         .cost = 10,
 35         .maxHealth = 10,
 36         .projectileSpeed = 3.0f,
 37         .projectileType = PROJECTILE_TYPE_CATAPULT,
 38         .hitEffect = {
 39           .damage = 2.0f,
 40           .areaDamageRadius = 1.75f,
 41         }
 42     },
 43     [TOWER_TYPE_WALL] = {
 44         .cost = 2,
 45         .maxHealth = 10,
 46     },
 47 };
 48 
 49 Tower towers[TOWER_MAX_COUNT];
 50 int towerCount = 0;
 51 
 52 Model towerModels[TOWER_TYPE_COUNT];
 53 
 54 // definition of our archer unit
 55 SpriteUnit archerUnit = {
 56     .srcRect = {0, 0, 16, 16},
 57     .offset = {7, 1},
 58     .frameCount = 1,
 59     .frameDuration = 0.0f,
 60     .srcWeaponIdleRect = {16, 0, 6, 16},
 61     .srcWeaponIdleOffset = {8, 0},
 62     .srcWeaponCooldownRect = {22, 0, 11, 16},
 63     .srcWeaponCooldownOffset = {10, 0},
 64 };
 65 
 66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 67 {
 68   float xScale = flip ? -1.0f : 1.0f;
 69   Camera3D camera = currentLevel->camera;
 70   float size = 0.5f;
 71   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 72   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 73   // we want the sprite to face the camera, so we need to calculate the up vector
 74   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 75   Vector3 up = {0, 1, 0};
 76   Vector3 right = Vector3CrossProduct(forward, up);
 77   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 78 
 79   Rectangle srcRect = unit.srcRect;
 80   if (unit.frameCount > 1)
 81   {
 82     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 83   }
 84   if (flip)
 85   {
 86     srcRect.x += srcRect.width;
 87     srcRect.width = -srcRect.width;
 88   }
 89   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 90 
 91   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 92   {
 93     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 94     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 95     srcRect = unit.srcWeaponCooldownRect;
 96     if (flip)
 97     {
 98       // position.x = flip * scale.x * 0.5f;
 99       srcRect.x += srcRect.width;
100       srcRect.width = -srcRect.width;
101       offset.x = scale.x - offset.x;
102     }
103     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104   }
105   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106   {
107     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109     srcRect = unit.srcWeaponIdleRect;
110     if (flip)
111     {
112       // position.x = flip * scale.x * 0.5f;
113       srcRect.x += srcRect.width;
114       srcRect.width = -srcRect.width;
115       offset.x = scale.x - offset.x;
116     }
117     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118   }
119 }
120 
121 void TowerInit()
122 {
123   for (int i = 0; i < TOWER_MAX_COUNT; i++)
124   {
125     towers[i] = (Tower){0};
126   }
127   towerCount = 0;
128 
129   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131 
132   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133   {
134     if (towerModels[i].materials)
135     {
136       // assign the palette texture to the material of the model (0 is not used afaik)
137       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138     }
139   }
140 }
141 
142 static void TowerGunUpdate(Tower *tower)
143 {
144   TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145   if (tower->cooldown <= 0.0f)
146   {
147     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148     if (enemy)
149     {
150       tower->cooldown = config.cooldown;
151       // shoot the enemy; determine future position of the enemy
152       float bulletSpeed = config.projectileSpeed;
153       Vector2 velocity = enemy->simVelocity;
154       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155       Vector2 towerPosition = {tower->x, tower->y};
156       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157       for (int i = 0; i < 8; i++) {
158         velocity = enemy->simVelocity;
159         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160         float distance = Vector2Distance(towerPosition, futurePosition);
161         float eta2 = distance / bulletSpeed;
162         if (fabs(eta - eta2) < 0.01f) {
163           break;
164         }
165         eta = (eta2 + eta) * 0.5f;
166       }
167 
168       ProjectileTryAdd(config.projectileType, enemy, 
169         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
170         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171         bulletSpeed, config.hitEffect);
172       enemy->futureDamage += config.hitEffect.damage;
173       tower->lastTargetPosition = futurePosition;
174     }
175   }
176   else
177   {
178     tower->cooldown -= gameTime.deltaTime;
179   }
180 }
181 
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184   for (int i = 0; i < towerCount; i++)
185   {
186     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187     {
188       return &towers[i];
189     }
190   }
191   return 0;
192 }
193 
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196   if (towerCount >= TOWER_MAX_COUNT)
197   {
198     return 0;
199   }
200 
201   Tower *tower = TowerGetAt(x, y);
202   if (tower)
203   {
204     return 0;
205   }
206 
207   tower = &towers[towerCount++];
208   tower->x = x;
209   tower->y = y;
210   tower->towerType = towerType;
211   tower->cooldown = 0.0f;
212   tower->damage = 0.0f;
213   return tower;
214 }
215 
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218   for (int i = 0; i < towerCount; i++)
219   {
220     if (towers[i].towerType == towerType)
221     {
222       return &towers[i];
223     }
224   }
225   return 0;
226 }
227 
228 int GetTowerCosts(uint8_t towerType)
229 {
230   return towerTypeConfigs[towerType].cost;
231 }
232 
233 float TowerGetMaxHealth(Tower *tower)
234 {
235   return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237 
238 void TowerDrawSingle(Tower tower) 239 { 240 if (tower.towerType == TOWER_TYPE_NONE) 241 { 242 return;
243 } 244 245 switch (tower.towerType) 246 { 247 case TOWER_TYPE_ARCHER: 248 { 249 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera); 250 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera); 251 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 252 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 253 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE); 254 } 255 break; 256 case TOWER_TYPE_BALLISTA: 257 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN); 258 break; 259 case TOWER_TYPE_CATAPULT: 260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY); 261 break; 262 default: 263 if (towerModels[tower.towerType].materials) 264 { 265 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 266 } else {
267 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 268 } 269 break; 270 } 271 } 272 273 void TowerDraw() 274 {
275 for (int i = 0; i < towerCount; i++) 276 { 277 TowerDrawSingle(towers[i]); 278 } 279 } 280 281 void TowerUpdate() 282 { 283 for (int i = 0; i < towerCount; i++) 284 { 285 Tower *tower = &towers[i]; 286 switch (tower->towerType) 287 { 288 case TOWER_TYPE_CATAPULT: 289 case TOWER_TYPE_BALLISTA: 290 case TOWER_TYPE_ARCHER: 291 TowerGunUpdate(tower); 292 break; 293 } 294 } 295 } 296 297 void TowerDrawHealthBars(Camera3D camera) 298 { 299 for (int i = 0; i < towerCount; i++) 300 { 301 Tower *tower = &towers[i]; 302 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 303 { 304 continue; 305 } 306 307 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 308 float maxHealth = TowerGetMaxHealth(tower); 309 float health = maxHealth - tower->damage; 310 float healthRatio = health / maxHealth; 311 312 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f); 313 } 314 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 EnemyClassConfig enemyClassConfigs[] = {
  7     [ENEMY_TYPE_MINION] = {
  8       .health = 10.0f, 
  9       .speed = 0.6f, 
 10       .radius = 0.25f, 
 11       .maxAcceleration = 1.0f,
 12       .explosionDamage = 1.0f,
 13       .requiredContactTime = 0.5f,
 14       .explosionRange = 1.0f,
 15       .explosionPushbackPower = 0.25f,
 16       .goldValue = 1,
 17     },
 18 };
 19 
 20 Enemy enemies[ENEMY_MAX_COUNT];
 21 int enemyCount = 0;
 22 
 23 SpriteUnit enemySprites[] = {
 24     [ENEMY_TYPE_MINION] = {
 25       .srcRect = {0, 16, 16, 16},
 26       .offset = {8.0f, 0.0f},
 27       .frameCount = 6,
 28       .frameDuration = 0.1f,
 29     },
 30 };
 31 
 32 void EnemyInit()
 33 {
 34   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 35   {
 36     enemies[i] = (Enemy){0};
 37   }
 38   enemyCount = 0;
 39 }
 40 
 41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 42 {
 43   return enemyClassConfigs[enemy->enemyType].speed;
 44 }
 45 
 46 float EnemyGetMaxHealth(Enemy *enemy)
 47 {
 48   return enemyClassConfigs[enemy->enemyType].health;
 49 }
 50 
 51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 52 {
 53   int16_t castleX = 0;
 54   int16_t castleY = 0;
 55   int16_t dx = castleX - currentX;
 56   int16_t dy = castleY - currentY;
 57   if (dx == 0 && dy == 0)
 58   {
 59     *nextX = currentX;
 60     *nextY = currentY;
 61     return 1;
 62   }
 63   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 64 
 65   if (gradient.x == 0 && gradient.y == 0)
 66   {
 67     *nextX = currentX;
 68     *nextY = currentY;
 69     return 1;
 70   }
 71 
 72   if (fabsf(gradient.x) > fabsf(gradient.y))
 73   {
 74     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 75     *nextY = currentY;
 76     return 0;
 77   }
 78   *nextX = currentX;
 79   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 80   return 0;
 81 }
 82 
 83 
 84 // this function predicts the movement of the unit for the next deltaT seconds
 85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 86 {
 87   const float pointReachedDistance = 0.25f;
 88   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 89   const float maxSimStepTime = 0.015625f;
 90   
 91   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 92   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 93   int16_t nextX = enemy->nextX;
 94   int16_t nextY = enemy->nextY;
 95   Vector2 position = enemy->simPosition;
 96   int passedCount = 0;
 97   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 98   {
 99     float stepTime = fminf(deltaT - t, maxSimStepTime);
100     Vector2 target = (Vector2){nextX, nextY};
101     float speed = Vector2Length(*velocity);
102     // draw the target position for debugging
103     //DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106     {
107       // we reached the target position, let's move to the next waypoint
108       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109       target = (Vector2){nextX, nextY};
110       // track how many waypoints we passed
111       passedCount++;
112     }
113     
114     // acceleration towards the target
115     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117     *velocity = Vector2Add(*velocity, acceleration);
118 
119     // limit the speed to the maximum speed
120     if (speed > maxSpeed)
121     {
122       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123     }
124 
125     // move the enemy
126     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127   }
128 
129   if (waypointPassedCount)
130   {
131     (*waypointPassedCount) = passedCount;
132   }
133 
134   return position;
135 }
136 
137 void EnemyDraw()
138 {
139   for (int i = 0; i < enemyCount; i++)
140   {
141     Enemy enemy = enemies[i];
142     if (enemy.enemyType == ENEMY_TYPE_NONE)
143     {
144       continue;
145     }
146 
147     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148     
149     // don't draw any trails for now; might replace this with footprints later
150     // if (enemy.movePathCount > 0)
151     // {
152     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154     // }
155     // for (int j = 1; j < enemy.movePathCount; j++)
156     // {
157     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159     //   DrawLine3D(p, q, GREEN);
160     // }
161 
162     switch (enemy.enemyType)
163     {
164     case ENEMY_TYPE_MINION:
165       DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y}, 
166         enemy.walkedDistance, 0, 0);
167       break;
168     }
169   }
170 }
171 
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174   // damage the tower
175   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178   float explosionRange2 = explosionRange * explosionRange;
179   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180   // explode the enemy
181   if (tower->damage >= TowerGetMaxHealth(tower))
182   {
183     tower->towerType = TOWER_TYPE_NONE;
184   }
185 
186   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
187     explosionSource, 
188     (Vector3){0, 0.1f, 0}, (Vector3){1.0f, 1.0f, 1.0f}, 1.0f);
189 
190   enemy->enemyType = ENEMY_TYPE_NONE;
191 
192   // push back enemies & dealing damage
193   for (int i = 0; i < enemyCount; i++)
194   {
195     Enemy *other = &enemies[i];
196     if (other->enemyType == ENEMY_TYPE_NONE)
197     {
198       continue;
199     }
200     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201     if (distanceSqr > 0 && distanceSqr < explosionRange2)
202     {
203       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205       EnemyAddDamage(other, explosionDamge);
206     }
207   }
208 }
209 
210 void EnemyUpdate()
211 {
212   const float castleX = 0;
213   const float castleY = 0;
214   const float maxPathDistance2 = 0.25f * 0.25f;
215   
216   for (int i = 0; i < enemyCount; i++)
217   {
218     Enemy *enemy = &enemies[i];
219     if (enemy->enemyType == ENEMY_TYPE_NONE)
220     {
221       continue;
222     }
223 
224     int waypointPassedCount = 0;
225     Vector2 prevPosition = enemy->simPosition;
226     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227     enemy->startMovingTime = gameTime.time;
228     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229     // track path of unit
230     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231     {
232       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233       {
234         enemy->movePath[j] = enemy->movePath[j - 1];
235       }
236       enemy->movePath[0] = enemy->simPosition;
237       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238       {
239         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240       }
241     }
242 
243     if (waypointPassedCount > 0)
244     {
245       enemy->currentX = enemy->nextX;
246       enemy->currentY = enemy->nextY;
247       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249       {
250         // enemy reached the castle; remove it
251         enemy->enemyType = ENEMY_TYPE_NONE;
252         continue;
253       }
254     }
255   }
256 
257   // handle collisions between enemies
258   for (int i = 0; i < enemyCount - 1; i++)
259   {
260     Enemy *enemyA = &enemies[i];
261     if (enemyA->enemyType == ENEMY_TYPE_NONE)
262     {
263       continue;
264     }
265     for (int j = i + 1; j < enemyCount; j++)
266     {
267       Enemy *enemyB = &enemies[j];
268       if (enemyB->enemyType == ENEMY_TYPE_NONE)
269       {
270         continue;
271       }
272       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275       float radiusSum = radiusA + radiusB;
276       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277       {
278         // collision
279         float distance = sqrtf(distanceSqr);
280         float overlap = radiusSum - distance;
281         // move the enemies apart, but softly; if we have a clog of enemies,
282         // moving them perfectly apart can cause them to jitter
283         float positionCorrection = overlap / 5.0f;
284         Vector2 direction = (Vector2){
285             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289       }
290     }
291   }
292 
293   // handle collisions between enemies and towers
294   for (int i = 0; i < enemyCount; i++)
295   {
296     Enemy *enemy = &enemies[i];
297     if (enemy->enemyType == ENEMY_TYPE_NONE)
298     {
299       continue;
300     }
301     enemy->contactTime -= gameTime.deltaTime;
302     if (enemy->contactTime < 0.0f)
303     {
304       enemy->contactTime = 0.0f;
305     }
306 
307     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308     // linear search over towers; could be optimized by using path finding tower map,
309     // but for now, we keep it simple
310     for (int j = 0; j < towerCount; j++)
311     {
312       Tower *tower = &towers[j];
313       if (tower->towerType == TOWER_TYPE_NONE)
314       {
315         continue;
316       }
317       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319       if (distanceSqr > combinedRadius * combinedRadius)
320       {
321         continue;
322       }
323       // potential collision; square / circle intersection
324       float dx = tower->x - enemy->simPosition.x;
325       float dy = tower->y - enemy->simPosition.y;
326       float absDx = fabsf(dx);
327       float absDy = fabsf(dy);
328       Vector3 contactPoint = {0};
329       if (absDx <= 0.5f && absDx <= absDy) {
330         // vertical collision; push the enemy out horizontally
331         float overlap = enemyRadius + 0.5f - absDy;
332         if (overlap < 0.0f)
333         {
334           continue;
335         }
336         float direction = dy > 0.0f ? -1.0f : 1.0f;
337         enemy->simPosition.y += direction * overlap;
338         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339       }
340       else if (absDy <= 0.5f && absDy <= absDx)
341       {
342         // horizontal collision; push the enemy out vertically
343         float overlap = enemyRadius + 0.5f - absDx;
344         if (overlap < 0.0f)
345         {
346           continue;
347         }
348         float direction = dx > 0.0f ? -1.0f : 1.0f;
349         enemy->simPosition.x += direction * overlap;
350         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351       }
352       else
353       {
354         // possible collision with a corner
355         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357         float cornerX = tower->x + cornerDX;
358         float cornerY = tower->y + cornerDY;
359         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360         if (cornerDistanceSqr > enemyRadius * enemyRadius)
361         {
362           continue;
363         }
364         // push the enemy out along the diagonal
365         float cornerDistance = sqrtf(cornerDistanceSqr);
366         float overlap = enemyRadius - cornerDistance;
367         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369         enemy->simPosition.x -= directionX * overlap;
370         enemy->simPosition.y -= directionY * overlap;
371         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372       }
373 
374       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375       {
376         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378         {
379           EnemyTriggerExplode(enemy, tower, contactPoint);
380         }
381       }
382     }
383   }
384 }
385 
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388   return (EnemyId){enemy - enemies, enemy->generation};
389 }
390 
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393   if (enemyId.index >= ENEMY_MAX_COUNT)
394   {
395     return 0;
396   }
397   Enemy *enemy = &enemies[enemyId.index];
398   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399   {
400     return 0;
401   }
402   return enemy;
403 }
404 
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407   Enemy *spawn = 0;
408   for (int i = 0; i < enemyCount; i++)
409   {
410     Enemy *enemy = &enemies[i];
411     if (enemy->enemyType == ENEMY_TYPE_NONE)
412     {
413       spawn = enemy;
414       break;
415     }
416   }
417 
418   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419   {
420     spawn = &enemies[enemyCount++];
421   }
422 
423   if (spawn)
424   {
425     spawn->currentX = currentX;
426     spawn->currentY = currentY;
427     spawn->nextX = currentX;
428     spawn->nextY = currentY;
429     spawn->simPosition = (Vector2){currentX, currentY};
430     spawn->simVelocity = (Vector2){0, 0};
431     spawn->enemyType = enemyType;
432     spawn->startMovingTime = gameTime.time;
433     spawn->damage = 0.0f;
434     spawn->futureDamage = 0.0f;
435     spawn->generation++;
436     spawn->movePathCount = 0;
437     spawn->walkedDistance = 0.0f;
438   }
439 
440   return spawn;
441 }
442 
443 int EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445   int count = 0;
446   float range2 = range * range;
447   for (int i = 0; i < enemyCount; i++)
448   {
449     Enemy *enemy = &enemies[i];
450     if (enemy->enemyType == ENEMY_TYPE_NONE)
451     {
452       continue;
453     }
454     float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455     if (distance2 <= range2)
456     {
457       EnemyAddDamage(enemy, damage);
458       count++;
459     }
460   }
461   return count;
462 }
463 
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466   enemy->damage += damage;
467   if (enemy->damage >= EnemyGetMaxHealth(enemy))
468   {
469     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470     enemy->enemyType = ENEMY_TYPE_NONE;
471     return 1;
472   }
473 
474   return 0;
475 }
476 
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479   int16_t castleX = 0;
480   int16_t castleY = 0;
481   Enemy* closest = 0;
482   int16_t closestDistance = 0;
483   float range2 = range * range;
484   for (int i = 0; i < enemyCount; i++)
485   {
486     Enemy* enemy = &enemies[i];
487     if (enemy->enemyType == ENEMY_TYPE_NONE)
488     {
489       continue;
490     }
491     float maxHealth = EnemyGetMaxHealth(enemy);
492     if (enemy->futureDamage >= maxHealth)
493     {
494       // ignore enemies that will die soon
495       continue;
496     }
497     int16_t dx = castleX - enemy->currentX;
498     int16_t dy = castleY - enemy->currentY;
499     int16_t distance = abs(dx) + abs(dy);
500     if (!closest || distance < closestDistance)
501     {
502       float tdx = towerX - enemy->currentX;
503       float tdy = towerY - enemy->currentY;
504       float tdistance2 = tdx * tdx + tdy * tdy;
505       if (tdistance2 <= range2)
506       {
507         closest = enemy;
508         closestDistance = distance;
509       }
510     }
511   }
512   return closest;
513 }
514 
515 int EnemyCount()
516 {
517   int count = 0;
518   for (int i = 0; i < enemyCount; i++)
519   {
520     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521     {
522       count++;
523     }
524   }
525   return count;
526 }
527 
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530   for (int i = 0; i < enemyCount; i++)
531   {
532     Enemy *enemy = &enemies[i];
533     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534     {
535       continue;
536     }
537     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538     float maxHealth = EnemyGetMaxHealth(enemy);
539     float health = maxHealth - enemy->damage;
540     float healthRatio = health / maxHealth;
541     
542     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543   }
544 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 typedef struct ProjectileConfig
  8 {
  9   float arcFactor;
 10   Color color;
 11   Color trailColor;
 12 } ProjectileConfig;
 13 
 14 ProjectileConfig projectileConfigs[] = {
 15     [PROJECTILE_TYPE_ARROW] = {
 16         .arcFactor = 0.15f,
 17         .color = RED,
 18         .trailColor = BROWN,
 19     },
 20     [PROJECTILE_TYPE_CATAPULT] = {
 21         .arcFactor = 0.5f,
 22         .color = RED,
 23         .trailColor = GRAY,
 24     },
 25     [PROJECTILE_TYPE_BALLISTA] = {
 26         .arcFactor = 0.025f,
 27         .color = RED,
 28         .trailColor = BROWN,
 29     },
 30 };
 31 
 32 void ProjectileInit()
 33 {
 34   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 35   {
 36     projectiles[i] = (Projectile){0};
 37   }
 38 }
 39 
 40 void ProjectileDraw()
 41 {
 42   for (int i = 0; i < projectileCount; i++)
 43   {
 44     Projectile projectile = projectiles[i];
 45     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 46     {
 47       continue;
 48     }
 49     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 50     if (transition >= 1.0f)
 51     {
 52       continue;
 53     }
 54 
 55     ProjectileConfig config = projectileConfigs[projectile.projectileType];
 56     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 57     {
 58       float t = transition + transitionOffset * 0.3f;
 59       if (t > 1.0f)
 60       {
 61         break;
 62       }
 63       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 64       Color color = config.color;
 65       color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
 66       // fake a ballista flight path using parabola equation
 67       float parabolaT = t - 0.5f;
 68       parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 69       position.y += config.arcFactor * parabolaT * projectile.distance;
 70       
 71       float size = 0.06f * (transitionOffset + 0.25f);
 72       DrawCube(position, size, size, size, color);
 73     }
 74   }
 75 }
 76 
 77 void ProjectileUpdate()
 78 {
 79   for (int i = 0; i < projectileCount; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 87     if (transition >= 1.0f)
 88     {
 89       projectile->projectileType = PROJECTILE_TYPE_NONE;
 90       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 91       if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
 92       {
 93           Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
 94           enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
 95       }
 96       
 97       if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
 98       {
 99         EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100         // pancaked sphere explosion
101         float r = projectile->hitEffectConfig.areaDamageRadius;
102         ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103       }
104       else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105       {
106         EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107       }
108       continue;
109     }
110   }
111 }
112 
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116   {
117     Projectile *projectile = &projectiles[i];
118     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119     {
120       projectile->projectileType = projectileType;
121       projectile->shootTime = gameTime.time;
122       float distance = Vector3Distance(position, target);
123       projectile->arrivalTime = gameTime.time + distance / speed;
124       projectile->position = position;
125       projectile->target = target;
126       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127       projectile->distance = distance;
128       projectile->targetEnemy = EnemyGetId(enemy);
129       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130       projectile->hitEffectConfig = hitEffectConfig;
131       return projectile;
132     }
133   }
134   return 0;
135 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <rlgl.h>
  4 
  5 static Particle particles[PARTICLE_MAX_COUNT];
  6 static int particleCount = 0;
  7 
  8 void ParticleInit()
  9 {
 10   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 11   {
 12     particles[i] = (Particle){0};
 13   }
 14   particleCount = 0;
 15 }
 16 
 17 static void DrawExplosionParticle(Particle *particle, float transition)
 18 {
 19   Vector3 scale = particle->scale;
 20   float size = 1.0f * (1.0f - transition);
 21   Color startColor = WHITE;
 22   Color endColor = RED;
 23   Color color = ColorLerp(startColor, endColor, transition);
 24 
 25   rlPushMatrix();
 26   rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
 27   rlScalef(scale.x, scale.y, scale.z);
 28   DrawSphere(Vector3Zero(), size, color);
 29   rlPopMatrix();
 30 }
 31 
 32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
 33 {
 34   if (particleCount >= PARTICLE_MAX_COUNT)
 35   {
 36     return;
 37   }
 38 
 39   int index = -1;
 40   for (int i = 0; i < particleCount; i++)
 41   {
 42     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 43     {
 44       index = i;
 45       break;
 46     }
 47   }
 48 
 49   if (index == -1)
 50   {
 51     index = particleCount++;
 52   }
 53 
 54   Particle *particle = &particles[index];
 55   particle->particleType = particleType;
 56   particle->spawnTime = gameTime.time;
 57   particle->lifetime = lifetime;
 58   particle->position = position;
 59   particle->velocity = velocity;
 60   particle->scale = scale;
 61 }
 62 
 63 void ParticleUpdate()
 64 {
 65   for (int i = 0; i < particleCount; i++)
 66   {
 67     Particle *particle = &particles[i];
 68     if (particle->particleType == PARTICLE_TYPE_NONE)
 69     {
 70       continue;
 71     }
 72 
 73     float age = gameTime.time - particle->spawnTime;
 74 
 75     if (particle->lifetime > age)
 76     {
 77       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 78     }
 79     else {
 80       particle->particleType = PARTICLE_TYPE_NONE;
 81     }
 82   }
 83 }
 84 
 85 void ParticleDraw()
 86 {
 87   for (int i = 0; i < particleCount; i++)
 88   {
 89     Particle particle = particles[i];
 90     if (particle.particleType == PARTICLE_TYPE_NONE)
 91     {
 92       continue;
 93     }
 94 
 95     float age = gameTime.time - particle.spawnTime;
 96     float transition = age / particle.lifetime;
 97     switch (particle.particleType)
 98     {
 99     case PARTICLE_TYPE_EXPLOSION:
100       DrawExplosionParticle(&particle, transition);
101       break;
102     default:
103       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104       break;
105     }
106   }
107 }
  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 building a tower, there is now a proper preview of the tower and green arrows indicate that the tower can be moved around.
The tower has visual indicators that it can be moved around.

When selecting a tower for placement, we see the tower graphics drawn in place and 4 animated arrows are bouncing around. The building is rendered by creating a dummy tower struct and calling the function that is drawing the tower.

How the arrow animations work

The arrows are animated by using a sine wave function that is mapped to the range [0, 1]. An additional squeeze and stretch effect is added to the arrows to make them look more playful. The formulas for doing this are quite compact and not very readable unless you are familiar with the functions used. The relevant code is here:

  1 float bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f;
  2 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
  3 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
  4 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
  5 
  6 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f,  0.0f}, 
  7   (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);

The bounce and stretch effect is based both on a single sine wave that is mapped to the range [0, 1]. The offset (bounce movement), the squeeze and stretch are derived from this value.

Animating elements via code/math requires an understanding of the functions that can be used to create the desired effect. Such code is usually not liked by many because it is hard to read and understand, as it requires experience on how these functions work.

Still, let's give it a try and decipher the meanings, I think it is a good exercise:

bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f

sinf(GetTime()) would produce a sine wave that oscillates between -1 and 1 within a period of roughly 6.28 seconds (2*π). When multiplying this with 8.0f, the period is shortened to ~0.8 seconds. The rest of the formula ( x * 0.5f + 0.5f ) halves the amplitude (range [-0.5, 0.5]) and adds 0.5 so the range is [0, 1]. So bounce is a value that oscillates between 0 and 1 within a period of 0.8 seconds.

Let's visualize what this code does by plotting a graph of the bounce value over time:

1.5 -1.5 0 t 1 2 3 4 sin(t * 8) sin(t * 8) * 0.5 bounce(t) = sin(t * 8) * 0.5 + 0.5

Sine waves are very useful for animating pulses and smooth oscillations, like for positions, scales, or colors of objects.

Let's move on:

offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f

fmaxf(0.0f, bounce - 0.4f) ensures that the value is at least 0. Since the bounce value oscillates between 0 and 1 and we subtract 0.4, the value is staying 0 for 40% of the time. The rest of the time, the value is increasing from 0 to 0.6 with the sine wave oscillation. This capping is responsible for stopping the arrow's movement for a short time when it is on its low. The * 0.35f + 0.7f is mapping the range of the result to a desireable range. 0.35 is the bounce range and 0.7 is the offset.

This is how it looks like:

1 0 t 1 2 3 4 bounce(t) offset(t) = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f

The squeeze effect works very similar to the offset effect:

squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f

We want to squeeze the arrow when it is on its low at the resting point. The fminf function ensures that the value is at most 0. The rest is similar to the offset.

Let's add this to the plot:

1 0 t 1 2 3 4 bounce(t) offset(t) squeeze(t) = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f

When looking at the graph you can see that the bumps in the squeeze happen when the offset is at its lowest point and is running flat. So while the offset makes the arrow rest, the squeeze will scale the arrow, which looks as if the arrow is squashed due to its momentum.

To emphasize the movement, the stretch factor is added so that when the arrow is moving to its high point, it is stretched:

stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f

The stretch is similar to the offset as well, but the arrow is stretched when it is moving to its high point.

Let's add this to the plot, too:

1.5 0 t 1 2 3 4 bounce(t) offset(t) squeeze(t) stretch(t)

We can see here, that the stretching goes along with the offset function and amplifies its effect.

For the untrained eye, the readability of this code is not great. However, when seeing the formulas on a graph, animators recognize that this is very similar to how animation tools display animation curves. It's essentially the same underlaying principle.

The readability could be somewhat improved by replacing the magic numbers with well named constant. Finding such names is however difficult, as it is hard to describe the individual effects in a single word and thus, in my experience, makes the code not really so much easier to read but certainly more difficult to change when needed. When coming up with the calculations and values, there is a lot of trial and error involved to get things right and when the variables are in a different scope, I find it difficult to understand the effect of a change.

That said, there are also other ways to animate the arrow. For example, the animation could be done in a 3D modeling tool and exported and played back. As said, animators use similar curves. The advantage of using an animation software is of course, that the hand tweaking of the values is much more convenient and the results quickly look better, especially since it is done by actual artists.

Another approach can be to use premade tweening functions and using the formulas as a starting point for the desired effect. A lot of coders prefer tweens since they provide simple premade building blocks to create such animations. If you look inside tweening library functions, you will find that the code looks very similar to the code above. The abstraction into dedicated functions makes it just more approachable for everyone.

The biggest advantage of using code is, that it is possible to adapt the function to other parameters, like the distance of the arrow bounce could depend on whether the tower can be placed at the current location or not. The downside is that artists can't change the animation unless certain values are exposed to them and even then, modifications offer only very limited control.

It is important to note that all approaches are valid here. Personally, I like to implement such effects via code because it allows animating the parameters based on the game state, but similar effects can be achieved with a tweening library or when loading an animation, albeit with less control over the animation itself.

In bigger teams, using animations is typically the way to go, because it is easier to understand and modify for artists and designers. At work, I actually often replace existing tweens in code with animation clips when I work together with artists.

In any case, I would still recommend to anyone to learn doing some basic animations via code to understand how it works and what is possible. Both, tweening and model animations, utilize the same principles, hence I believe that learning how to animate via code is therefore a good foundation, regardless which approach you will use in the end.

In the next parts, this will become more clear, as I will tweak the behavior of the tower when the building is moved, which entirely depends on user inputs.

However, in general it also depends a lot on personal preference. I don't think it makes sense to force oneself to learn animating this way. With this implementation I only wanted to show what can be done with fairly few lines of code and in case you like to learn more about this topic, you can start from here by modifying the values and see what happens.

Conclusion

In this part, the building placement UX works now on touch screens. There is an indicator on the building that can be moved around and the player can confirm the placement with the OK button.

In the next part, I will improve the movement of the tower when dragged around, since it currently snaps to the grid and doesn't feel smooth at all.

I hope you enjoyed this part and see you in the next one!

🍪