Simple tower defense tutorial, part 14: Cartoonish tower movement (1/2)

While the movement looks now smooth, the motion is still static since the tower is staying upright all the time. When watching cartoons, motion is highly exaggerated and playful. Squash and stretch and anticipation are common techniques to make the motion more lively. There is a good youtube series on the 12 principles of animation that is worth watching if you are interested in this topic.

Applying these principles to the behavior of a game is what people often refer to as "juicing" the game.

I want to excercise this here by making the tower placement mode more lively and showing what it involves. The fundamental idea of juicing up gameplay elements is to make every action a joyful interaction with the game. While it won't make a boring game more fun, it can carry the player over more tedious parts of a game.

To make this blog post a little less dry, I will give a glimpse on what we are going to achieve at the end of the next part - this part is covering the math for the simulation, the next part how to apply it when drawing the tower model:

The tower wobbling around when moved.
How the tower placement will look like at the end of the next part!

I hope this is a good motivation to follow through this post that is going to be more math heavy. In the previous part, I pointed out that animating via math functions can be quite difficult, but learning how to animate via code and math is going to be crucial for what we want to achieve in this part:

Like we've seen for the animated arrows on the tower placement, these animation principles can be expressed as formulas, which enables us to let the calculations interact with the game state and making them respond to the player's actions. Since the player can interact with the game at any time, having adaptable formulas that describe the animation is practically the only way how to achieve the goal of making the behavior of the tower placement cartoonish.

This constant uncertainty is a great challenge and the reason why using formulas is so powerful. Premade animations or tweens won't cut it here, because they are not flexible enough to adapt to the player's actions.

The solution I will use here a spring based simulation to make the tower wobble around when it is moved but also let it squash and stretch when it is accelerated or decelerated. Imagine it as a bouncy spring that bounces when you move it around.

When using an engine like Unity, you could use the physics engine to achieve a similar effect, though doing the calculations ourselves will give us more control over the behavior and make it easier to adapt to the game state.

Here's the plan: When we place the tower, we initialize a struct that holds two values: A position of a spring tip and its velocity. The spring has a rest length and we will use a function to synchronize the spring tip in an upright position with the tower position.

The effect will be this: When the tower is moved, the spring tip will rest in its previous position for a moment before the momentum is transported through the spring to the tip. This will induce a wobbling effect. We use the spring tip to rotate the tower around its base so it looks like we grabbed it at its base and pull it to a new location. Furthermore, when the spring gets stretched or compressed, we will scale the tower to make it look like it is squashed or stretched. This should provide us with a nice cartoonish effect that makes the tower look more lively.

Let's start with displaying an abstraction of the spring simulation:

  • 💾
  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 level->placementX = -1; 120 level->placementY = 0;
121 122 Camera *camera = &level->camera; 123 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 124 camera->target = (Vector3){0.0f, 0.0f, 0.0f}; 125 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 126 camera->fovy = 10.0f; 127 camera->projection = CAMERA_ORTHOGRAPHIC; 128 } 129 130 void DrawLevelHud(Level *level) 131 { 132 const char *text = TextFormat("Gold: %d", level->playerGold); 133 Font font = GetFontDefault(); 134 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 135 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 136 } 137 138 void DrawLevelReportLostWave(Level *level) 139 { 140 BeginMode3D(level->camera); 141 DrawLevelGround(level); 142 TowerDraw(); 143 EnemyDraw(); 144 ProjectileDraw(); 145 ParticleDraw(); 146 guiState.isBlocked = 0; 147 EndMode3D(); 148 149 TowerDrawHealthBars(level->camera); 150 151 const char *text = "Wave lost"; 152 int textWidth = MeasureText(text, 20); 153 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 154 155 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 156 { 157 level->nextState = LEVEL_STATE_RESET; 158 } 159 } 160 161 int HasLevelNextWave(Level *level) 162 { 163 for (int i = 0; i < 10; i++) 164 { 165 EnemyWave *wave = &level->waves[i]; 166 if (wave->wave == level->currentWave) 167 { 168 return 1; 169 } 170 } 171 return 0; 172 } 173 174 void DrawLevelReportWonWave(Level *level) 175 { 176 BeginMode3D(level->camera); 177 DrawLevelGround(level); 178 TowerDraw(); 179 EnemyDraw(); 180 ProjectileDraw(); 181 ParticleDraw(); 182 guiState.isBlocked = 0; 183 EndMode3D(); 184 185 TowerDrawHealthBars(level->camera); 186 187 const char *text = "Wave won"; 188 int textWidth = MeasureText(text, 20); 189 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 190 191 192 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 193 { 194 level->nextState = LEVEL_STATE_RESET; 195 } 196 197 if (HasLevelNextWave(level)) 198 { 199 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 200 { 201 level->nextState = LEVEL_STATE_BUILDING; 202 } 203 } 204 else { 205 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 206 { 207 level->nextState = LEVEL_STATE_WON_LEVEL; 208 } 209 } 210 } 211 212 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 213 { 214 static ButtonState buttonStates[8] = {0}; 215 int cost = GetTowerCosts(towerType); 216 const char *text = TextFormat("%s: %d", name, cost); 217 buttonStates[towerType].isSelected = level->placementMode == towerType; 218 buttonStates[towerType].isDisabled = level->playerGold < cost; 219 if (Button(text, x, y, width, height, &buttonStates[towerType])) 220 { 221 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 222 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT; 223 } 224 } 225 226 float GetRandomFloat(float min, float max) 227 { 228 int random = GetRandomValue(0, 0xfffffff); 229 return ((float)random / (float)0xfffffff) * (max - min) + min; 230 } 231 232 void DrawLevelGround(Level *level) 233 { 234 // draw checkerboard ground pattern 235 for (int x = -5; x <= 5; x += 1) 236 { 237 for (int y = -5; y <= 5; y += 1) 238 { 239 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 240 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 241 } 242 } 243 244 int oldSeed = GetRandomValue(0, 0xfffffff); 245 SetRandomSeed(level->seed); 246 // increase probability for trees via duplicated entries 247 Model borderModels[64]; 248 int maxRockCount = GetRandomValue(2, 6); 249 int maxTreeCount = GetRandomValue(10, 20); 250 int maxFirTreeCount = GetRandomValue(5, 10); 251 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount; 252 int grassPatchCount = GetRandomValue(5, 30); 253 254 int modelCount = 0; 255 for (int i = 0; i < maxRockCount && modelCount < 63; i++) 256 { 257 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)]; 258 } 259 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++) 260 { 261 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)]; 262 } 263 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++) 264 { 265 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)]; 266 } 267 for (int i = 0; i < grassPatchCount && modelCount < 63; i++) 268 { 269 borderModels[modelCount++] = grassPatchModel[0]; 270 } 271 272 // draw some objects around the border of the map 273 Vector3 up = {0, 1, 0}; 274 // a pseudo random number generator to get the same result every time 275 const float wiggle = 0.75f; 276 const int layerCount = 3; 277 for (int layer = 0; layer < layerCount; layer++) 278 { 279 int layerPos = 6 + layer; 280 for (int x = -6 + layer; x <= 6 + layer; x += 1) 281 { 282 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 283 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 284 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 285 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 286 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 287 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 288 } 289 290 for (int z = -5 + layer; z <= 5 + layer; z += 1) 291 { 292 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 293 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 294 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 295 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 296 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 297 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 298 } 299 } 300 301 SetRandomSeed(oldSeed); 302 } 303 304 void DrawEnemyPath(Level *level, Color arrowColor) 305 { 306 const int castleX = 0, castleY = 0; 307 const int maxWaypointCount = 200; 308 const float timeStep = 1.0f; 309 Vector3 arrowScale = {0.75f, 0.75f, 0.75f}; 310 311 // we start with a time offset to simulate the path, 312 // this way the arrows are animated in a forward moving direction 313 // The time is wrapped around the time step to get a smooth animation 314 float timeOffset = fmodf(GetTime(), timeStep); 315 316 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 317 { 318 EnemyWave *wave = &level->waves[i]; 319 if (wave->wave != level->currentWave) 320 { 321 continue; 322 } 323 324 // use this dummy enemy to simulate the path 325 Enemy dummy = { 326 .enemyType = ENEMY_TYPE_MINION, 327 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y}, 328 .nextX = wave->spawnPosition.x, 329 .nextY = wave->spawnPosition.y, 330 .currentX = wave->spawnPosition.x, 331 .currentY = wave->spawnPosition.y, 332 }; 333 334 float deltaTime = timeOffset; 335 for (int j = 0; j < maxWaypointCount; j++) 336 { 337 int waypointPassedCount = 0; 338 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount); 339 // after the initial variable starting offset, we use a fixed time step 340 deltaTime = timeStep; 341 dummy.simPosition = pos; 342 343 // Update the dummy's position just like we do in the regular enemy update loop 344 for (int k = 0; k < waypointPassedCount; k++) 345 { 346 dummy.currentX = dummy.nextX; 347 dummy.currentY = dummy.nextY; 348 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) && 349 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 350 { 351 break; 352 } 353 } 354 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 355 { 356 break; 357 } 358 359 // get the angle we need to rotate the arrow model. The velocity is just fine for this. 360 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f; 361 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor); 362 } 363 } 364 } 365 366 void DrawEnemyPaths(Level *level) 367 { 368 // disable depth testing for the path arrows 369 // flush the 3D batch to draw the arrows on top of everything 370 rlDrawRenderBatchActive(); 371 rlDisableDepthTest(); 372 DrawEnemyPath(level, (Color){64, 64, 64, 160}); 373 374 rlDrawRenderBatchActive(); 375 rlEnableDepthTest(); 376 DrawEnemyPath(level, WHITE); 377 } 378 379 void DrawLevelBuildingPlacementState(Level *level) 380 { 381 BeginMode3D(level->camera); 382 DrawLevelGround(level); 383 384 int blockedCellCount = 0; 385 Vector2 blockedCells[1]; 386 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 387 float planeDistance = ray.position.y / -ray.direction.y; 388 float planeX = ray.direction.x * planeDistance + ray.position.x; 389 float planeY = ray.direction.z * planeDistance + ray.position.z; 390 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 391 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 392 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) 393 { 394 level->placementX = mapX; 395 level->placementY = mapY; 396 } 397 else 398 { 399 mapX = level->placementX; 400 mapY = level->placementY; 401 } 402 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY}; 403 PathFindingMapUpdate(blockedCellCount, blockedCells); 404 405 TowerDraw(); 406 EnemyDraw(); 407 ProjectileDraw(); 408 ParticleDraw(); 409 DrawEnemyPaths(level);
410 411 // smooth transition for the placement position using exponential decay
412 const float lambda = 15.0f; 413 const float deltaTime = fmin(GetFrameTime(), 0.3f); 414 float factor = 1.0f - expf(-lambda * deltaTime); 415 416 level->placementTransitionPosition = 417 Vector2Lerp( 418 level->placementTransitionPosition,
419 (Vector2){mapX, mapY}, factor); 420 421 // draw the spring position for debugging the spring simulation 422 // first step: stiff spring, no simulation 423 Vector3 worldPlacementPosition = (Vector3){ 424 level->placementTransitionPosition.x, 0.0f, level->placementTransitionPosition.y}; 425 level->placementTowerSpring.position = (Vector3){ 426 worldPlacementPosition.x, 1.0f, worldPlacementPosition.z}; 427 DrawCube(level->placementTowerSpring.position, 0.1f, 0.1f, 0.1f, RED); 428 DrawLine3D(level->placementTowerSpring.position, worldPlacementPosition, YELLOW);
429 430 rlPushMatrix(); 431 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y); 432 DrawCubeWires((Vector3){0.0f, 0.0f, 0.0f}, 1.0f, 0.0f, 1.0f, RED);
433 434 435 // deactivated for now to debug the spring simulation 436 // Tower dummy = { 437 // .towerType = level->placementMode, 438 // }; 439 // TowerDrawSingle(dummy);
440 441 float bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f; 442 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f; 443 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f; 444 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f; 445 446 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE); 447 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE); 448 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE); 449 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE); 450 rlPopMatrix(); 451 452 guiState.isBlocked = 0; 453 454 EndMode3D(); 455 456 TowerDrawHealthBars(level->camera); 457 458 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0)) 459 { 460 level->nextState = LEVEL_STATE_BUILDING; 461 level->placementMode = TOWER_TYPE_NONE; 462 TraceLog(LOG_INFO, "Cancel building"); 463 } 464 465 if (Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0)) 466 { 467 level->nextState = LEVEL_STATE_BUILDING; 468 if (TowerTryAdd(level->placementMode, mapX, mapY)) 469 { 470 level->playerGold -= GetTowerCosts(level->placementMode); 471 level->placementMode = TOWER_TYPE_NONE; 472 } 473 } 474 } 475 476 void DrawLevelBuildingState(Level *level) 477 { 478 BeginMode3D(level->camera); 479 DrawLevelGround(level); 480 481 PathFindingMapUpdate(0, 0); 482 TowerDraw(); 483 EnemyDraw(); 484 ProjectileDraw(); 485 ParticleDraw(); 486 DrawEnemyPaths(level); 487 488 guiState.isBlocked = 0; 489 490 EndMode3D(); 491 492 TowerDrawHealthBars(level->camera); 493 494 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 495 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer"); 496 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 497 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult"); 498 499 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 500 { 501 level->nextState = LEVEL_STATE_RESET; 502 } 503 504 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 505 { 506 level->nextState = LEVEL_STATE_BATTLE; 507 } 508 509 const char *text = "Building phase"; 510 int textWidth = MeasureText(text, 20); 511 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 512 } 513 514 void InitBattleStateConditions(Level *level) 515 { 516 level->state = LEVEL_STATE_BATTLE; 517 level->nextState = LEVEL_STATE_NONE; 518 level->waveEndTimer = 0.0f; 519 for (int i = 0; i < 10; i++) 520 { 521 EnemyWave *wave = &level->waves[i]; 522 wave->spawned = 0; 523 wave->timeToSpawnNext = wave->delay; 524 } 525 } 526 527 void DrawLevelBattleState(Level *level) 528 { 529 BeginMode3D(level->camera); 530 DrawLevelGround(level); 531 TowerDraw(); 532 EnemyDraw(); 533 ProjectileDraw(); 534 ParticleDraw(); 535 guiState.isBlocked = 0; 536 EndMode3D(); 537 538 EnemyDrawHealthbars(level->camera); 539 TowerDrawHealthBars(level->camera); 540 541 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 542 { 543 level->nextState = LEVEL_STATE_RESET; 544 } 545 546 int maxCount = 0; 547 int remainingCount = 0; 548 for (int i = 0; i < 10; i++) 549 { 550 EnemyWave *wave = &level->waves[i]; 551 if (wave->wave != level->currentWave) 552 { 553 continue; 554 } 555 maxCount += wave->count; 556 remainingCount += wave->count - wave->spawned; 557 } 558 int aliveCount = EnemyCount(); 559 remainingCount += aliveCount; 560 561 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 562 int textWidth = MeasureText(text, 20); 563 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 564 } 565 566 void DrawLevel(Level *level) 567 { 568 switch (level->state) 569 { 570 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 571 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break; 572 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 573 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 574 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 575 default: break; 576 } 577 578 DrawLevelHud(level); 579 } 580 581 void UpdateLevel(Level *level) 582 { 583 if (level->state == LEVEL_STATE_BATTLE) 584 { 585 int activeWaves = 0; 586 for (int i = 0; i < 10; i++) 587 { 588 EnemyWave *wave = &level->waves[i]; 589 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 590 { 591 continue; 592 } 593 activeWaves++; 594 wave->timeToSpawnNext -= gameTime.deltaTime; 595 if (wave->timeToSpawnNext <= 0.0f) 596 { 597 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 598 if (enemy) 599 { 600 wave->timeToSpawnNext = wave->interval; 601 wave->spawned++; 602 } 603 } 604 } 605 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 606 level->waveEndTimer += gameTime.deltaTime; 607 if (level->waveEndTimer >= 2.0f) 608 { 609 level->nextState = LEVEL_STATE_LOST_WAVE; 610 } 611 } 612 else if (activeWaves == 0 && EnemyCount() == 0) 613 { 614 level->waveEndTimer += gameTime.deltaTime; 615 if (level->waveEndTimer >= 2.0f) 616 { 617 level->nextState = LEVEL_STATE_WON_WAVE; 618 } 619 } 620 } 621 622 PathFindingMapUpdate(0, 0); 623 EnemyUpdate(); 624 TowerUpdate(); 625 ProjectileUpdate(); 626 ParticleUpdate(); 627 628 if (level->nextState == LEVEL_STATE_RESET) 629 { 630 InitLevel(level); 631 } 632 633 if (level->nextState == LEVEL_STATE_BATTLE) 634 { 635 InitBattleStateConditions(level); 636 } 637 638 if (level->nextState == LEVEL_STATE_WON_WAVE) 639 { 640 level->currentWave++; 641 level->state = LEVEL_STATE_WON_WAVE; 642 } 643 644 if (level->nextState == LEVEL_STATE_LOST_WAVE) 645 { 646 level->state = LEVEL_STATE_LOST_WAVE; 647 } 648 649 if (level->nextState == LEVEL_STATE_BUILDING) 650 { 651 level->state = LEVEL_STATE_BUILDING; 652 } 653 654 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT) 655 { 656 level->state = LEVEL_STATE_BUILDING_PLACEMENT; 657 level->placementTransitionPosition = (Vector2){
658 level->placementX, level->placementY}; 659 // initialize the spring to the current position 660 level->placementTowerSpring = (PhysicsPoint){ 661 .position = (Vector3){level->placementX, 1.0f, level->placementY}, 662 .velocity = (Vector3){0.0f, 0.0f, 0.0f}, 663 };
664 } 665 666 if (level->nextState == LEVEL_STATE_WON_LEVEL) 667 { 668 // make something of this later 669 InitLevel(level); 670 } 671 672 level->nextState = LEVEL_STATE_NONE; 673 } 674 675 float nextSpawnTime = 0.0f; 676 677 void ResetGame() 678 { 679 InitLevel(currentLevel); 680 } 681 682 void InitGame() 683 { 684 TowerInit(); 685 EnemyInit(); 686 ProjectileInit(); 687 ParticleInit(); 688 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 689 690 currentLevel = levels; 691 InitLevel(currentLevel); 692 } 693 694 //# Immediate GUI functions 695 696 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 697 { 698 const float healthBarHeight = 6.0f; 699 const float healthBarOffset = 15.0f; 700 const float inset = 2.0f; 701 const float innerWidth = healthBarWidth - inset * 2; 702 const float innerHeight = healthBarHeight - inset * 2; 703 704 Vector2 screenPos = GetWorldToScreen(position, camera); 705 float centerX = screenPos.x - healthBarWidth * 0.5f; 706 float topY = screenPos.y - healthBarOffset; 707 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 708 float healthWidth = innerWidth * healthRatio; 709 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 710 } 711 712 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 713 { 714 Rectangle bounds = {x, y, width, height}; 715 int isPressed = 0; 716 int isSelected = state && state->isSelected; 717 int isDisabled = state && state->isDisabled; 718 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 719 { 720 Color color = isSelected ? DARKGRAY : GRAY; 721 DrawRectangle(x, y, width, height, color); 722 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 723 { 724 isPressed = 1; 725 } 726 guiState.isBlocked = 1; 727 } 728 else 729 { 730 Color color = isSelected ? WHITE : LIGHTGRAY; 731 DrawRectangle(x, y, width, height, color); 732 } 733 Font font = GetFontDefault(); 734 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 735 Color textColor = isDisabled ? GRAY : BLACK; 736 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 737 return isPressed; 738 } 739 740 //# Main game loop 741 742 void GameUpdate() 743 { 744 float dt = GetFrameTime(); 745 // cap maximum delta time to 0.1 seconds to prevent large time steps 746 if (dt > 0.1f) dt = 0.1f; 747 gameTime.time += dt; 748 gameTime.deltaTime = dt; 749 750 UpdateLevel(currentLevel); 751 } 752 753 int main(void) 754 { 755 int screenWidth, screenHeight; 756 GetPreferredSize(&screenWidth, &screenHeight); 757 InitWindow(screenWidth, screenHeight, "Tower defense"); 758 SetTargetFPS(30); 759 760 LoadAssets(); 761 InitGame(); 762 763 while (!WindowShouldClose()) 764 { 765 if (IsPaused()) { 766 // canvas is not visible in browser - do nothing 767 continue; 768 } 769 770 BeginDrawing(); 771 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 772 773 GameUpdate(); 774 DrawLevel(currentLevel); 775 776 EndDrawing(); 777 } 778 779 CloseWindow(); 780 781 return 0; 782 }
  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 typedef struct PhysicsPoint 12 { 13 Vector3 position; 14 Vector3 velocity; 15 } PhysicsPoint; 16
17 #define ENEMY_MAX_PATH_COUNT 8 18 #define ENEMY_MAX_COUNT 400 19 #define ENEMY_TYPE_NONE 0 20 #define ENEMY_TYPE_MINION 1 21 22 #define PARTICLE_MAX_COUNT 400 23 #define PARTICLE_TYPE_NONE 0 24 #define PARTICLE_TYPE_EXPLOSION 1 25 26 typedef struct Particle 27 { 28 uint8_t particleType; 29 float spawnTime; 30 float lifetime; 31 Vector3 position; 32 Vector3 velocity; 33 Vector3 scale; 34 } Particle; 35 36 #define TOWER_MAX_COUNT 400 37 enum TowerType 38 { 39 TOWER_TYPE_NONE, 40 TOWER_TYPE_BASE, 41 TOWER_TYPE_ARCHER, 42 TOWER_TYPE_BALLISTA, 43 TOWER_TYPE_CATAPULT, 44 TOWER_TYPE_WALL, 45 TOWER_TYPE_COUNT 46 }; 47 48 typedef struct HitEffectConfig 49 { 50 float damage; 51 float areaDamageRadius; 52 float pushbackPowerDistance; 53 } HitEffectConfig; 54 55 typedef struct TowerTypeConfig 56 { 57 float cooldown; 58 float range; 59 float projectileSpeed; 60 61 uint8_t cost; 62 uint8_t projectileType; 63 uint16_t maxHealth; 64 65 HitEffectConfig hitEffect; 66 } TowerTypeConfig; 67 68 typedef struct Tower 69 { 70 int16_t x, y; 71 uint8_t towerType; 72 Vector2 lastTargetPosition; 73 float cooldown; 74 float damage; 75 } Tower; 76 77 typedef struct GameTime 78 { 79 float time; 80 float deltaTime; 81 } GameTime; 82 83 typedef struct ButtonState { 84 char isSelected; 85 char isDisabled; 86 } ButtonState; 87 88 typedef struct GUIState { 89 int isBlocked; 90 } GUIState; 91 92 typedef enum LevelState 93 { 94 LEVEL_STATE_NONE, 95 LEVEL_STATE_BUILDING, 96 LEVEL_STATE_BUILDING_PLACEMENT, 97 LEVEL_STATE_BATTLE, 98 LEVEL_STATE_WON_WAVE, 99 LEVEL_STATE_LOST_WAVE, 100 LEVEL_STATE_WON_LEVEL, 101 LEVEL_STATE_RESET, 102 } LevelState; 103 104 typedef struct EnemyWave { 105 uint8_t enemyType; 106 uint8_t wave; 107 uint16_t count; 108 float interval; 109 float delay; 110 Vector2 spawnPosition; 111 112 uint16_t spawned; 113 float timeToSpawnNext; 114 } EnemyWave; 115 116 #define ENEMY_MAX_WAVE_COUNT 10 117 118 typedef struct Level 119 { 120 int seed; 121 LevelState state; 122 LevelState nextState; 123 Camera3D camera; 124 int placementMode; 125 int16_t placementX; 126 int16_t placementY;
127 Vector2 placementTransitionPosition; 128 PhysicsPoint placementTowerSpring;
129 130 int initialGold; 131 int playerGold; 132 133 EnemyWave waves[ENEMY_MAX_WAVE_COUNT]; 134 int currentWave; 135 float waveEndTimer; 136 } Level; 137 138 typedef struct DeltaSrc 139 { 140 char x, y; 141 } DeltaSrc; 142 143 typedef struct PathfindingMap 144 { 145 int width, height; 146 float scale; 147 float *distances; 148 long *towerIndex; 149 DeltaSrc *deltaSrc; 150 float maxDistance; 151 Matrix toMapSpace; 152 Matrix toWorldSpace; 153 } PathfindingMap; 154 155 // when we execute the pathfinding algorithm, we need to store the active nodes 156 // in a queue. Each node has a position, a distance from the start, and the 157 // position of the node that we came from. 158 typedef struct PathfindingNode 159 { 160 int16_t x, y, fromX, fromY; 161 float distance; 162 } PathfindingNode; 163 164 typedef struct EnemyId 165 { 166 uint16_t index; 167 uint16_t generation; 168 } EnemyId; 169 170 typedef struct EnemyClassConfig 171 { 172 float speed; 173 float health; 174 float radius; 175 float maxAcceleration; 176 float requiredContactTime; 177 float explosionDamage; 178 float explosionRange; 179 float explosionPushbackPower; 180 int goldValue; 181 } EnemyClassConfig; 182 183 typedef struct Enemy 184 { 185 int16_t currentX, currentY; 186 int16_t nextX, nextY; 187 Vector2 simPosition; 188 Vector2 simVelocity; 189 uint16_t generation; 190 float walkedDistance; 191 float startMovingTime; 192 float damage, futureDamage; 193 float contactTime; 194 uint8_t enemyType; 195 uint8_t movePathCount; 196 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 197 } Enemy; 198 199 // a unit that uses sprites to be drawn 200 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0 201 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1 202 typedef struct SpriteUnit 203 { 204 Rectangle srcRect; 205 Vector2 offset; 206 int frameCount; 207 float frameDuration; 208 Rectangle srcWeaponIdleRect; 209 Vector2 srcWeaponIdleOffset; 210 Rectangle srcWeaponCooldownRect; 211 Vector2 srcWeaponCooldownOffset; 212 } SpriteUnit; 213 214 #define PROJECTILE_MAX_COUNT 1200 215 #define PROJECTILE_TYPE_NONE 0 216 #define PROJECTILE_TYPE_ARROW 1 217 #define PROJECTILE_TYPE_CATAPULT 2 218 #define PROJECTILE_TYPE_BALLISTA 3 219 220 typedef struct Projectile 221 { 222 uint8_t projectileType; 223 float shootTime; 224 float arrivalTime; 225 float distance; 226 Vector3 position; 227 Vector3 target; 228 Vector3 directionNormal; 229 EnemyId targetEnemy; 230 HitEffectConfig hitEffectConfig; 231 } Projectile; 232 233 //# Function declarations 234 float TowerGetMaxHealth(Tower *tower); 235 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 236 int EnemyAddDamageRange(Vector2 position, float range, float damage); 237 int EnemyAddDamage(Enemy *enemy, float damage); 238 239 //# Enemy functions 240 void EnemyInit(); 241 void EnemyDraw(); 242 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 243 void EnemyUpdate(); 244 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 245 float EnemyGetMaxHealth(Enemy *enemy); 246 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 247 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 248 EnemyId EnemyGetId(Enemy *enemy); 249 Enemy *EnemyTryResolve(EnemyId enemyId); 250 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 251 int EnemyAddDamage(Enemy *enemy, float damage); 252 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 253 int EnemyCount(); 254 void EnemyDrawHealthbars(Camera3D camera); 255 256 //# Tower functions 257 void TowerInit(); 258 Tower *TowerGetAt(int16_t x, int16_t y); 259 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 260 Tower *GetTowerByType(uint8_t towerType); 261 int GetTowerCosts(uint8_t towerType); 262 float TowerGetMaxHealth(Tower *tower); 263 void TowerDraw(); 264 void TowerDrawSingle(Tower tower); 265 void TowerUpdate(); 266 void TowerDrawHealthBars(Camera3D camera); 267 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 268 269 //# Particles 270 void ParticleInit(); 271 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime); 272 void ParticleUpdate(); 273 void ParticleDraw(); 274 275 //# Projectiles 276 void ProjectileInit(); 277 void ProjectileDraw(); 278 void ProjectileUpdate(); 279 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig); 280 281 //# Pathfinding map 282 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 283 float PathFindingGetDistance(int mapX, int mapY); 284 Vector2 PathFindingGetGradient(Vector3 world); 285 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 286 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells); 287 void PathFindingMapDraw(); 288 289 //# UI 290 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth); 291 292 //# Level 293 void DrawLevelGround(Level *level); 294 void DrawEnemyPath(Level *level, Color arrowColor); 295 296 //# variables 297 extern Level *currentLevel; 298 extern Enemy enemies[ENEMY_MAX_COUNT]; 299 extern int enemyCount; 300 extern EnemyClassConfig enemyClassConfigs[]; 301 302 extern GUIState guiState; 303 extern GameTime gameTime; 304 extern Tower towers[TOWER_MAX_COUNT]; 305 extern int towerCount; 306 307 extern Texture2D palette, spriteSheet; 308 309 #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
A stiff spring setup
A spring simulation

This is just the visualization of the spring tip's position (red dot) and a yellow line that represents the spring that connects the tip with the base. Currently, the spring is stiff. The next steps will add the simulated behavior aspect:

  • 💾
  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   level->placementX = -1;
120   level->placementY = 0;
121 
122   Camera *camera = &level->camera;
123   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
124   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
125   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
126   camera->fovy = 10.0f;
127   camera->projection = CAMERA_ORTHOGRAPHIC;
128 }
129 
130 void DrawLevelHud(Level *level)
131 {
132   const char *text = TextFormat("Gold: %d", level->playerGold);
133   Font font = GetFontDefault();
134   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
135   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
136 }
137 
138 void DrawLevelReportLostWave(Level *level)
139 {
140   BeginMode3D(level->camera);
141   DrawLevelGround(level);
142   TowerDraw();
143   EnemyDraw();
144   ProjectileDraw();
145   ParticleDraw();
146   guiState.isBlocked = 0;
147   EndMode3D();
148 
149   TowerDrawHealthBars(level->camera);
150 
151   const char *text = "Wave lost";
152   int textWidth = MeasureText(text, 20);
153   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
154 
155   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
156   {
157     level->nextState = LEVEL_STATE_RESET;
158   }
159 }
160 
161 int HasLevelNextWave(Level *level)
162 {
163   for (int i = 0; i < 10; i++)
164   {
165     EnemyWave *wave = &level->waves[i];
166     if (wave->wave == level->currentWave)
167     {
168       return 1;
169     }
170   }
171   return 0;
172 }
173 
174 void DrawLevelReportWonWave(Level *level)
175 {
176   BeginMode3D(level->camera);
177   DrawLevelGround(level);
178   TowerDraw();
179   EnemyDraw();
180   ProjectileDraw();
181   ParticleDraw();
182   guiState.isBlocked = 0;
183   EndMode3D();
184 
185   TowerDrawHealthBars(level->camera);
186 
187   const char *text = "Wave won";
188   int textWidth = MeasureText(text, 20);
189   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
190 
191 
192   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
193   {
194     level->nextState = LEVEL_STATE_RESET;
195   }
196 
197   if (HasLevelNextWave(level))
198   {
199     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
200     {
201       level->nextState = LEVEL_STATE_BUILDING;
202     }
203   }
204   else {
205     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
206     {
207       level->nextState = LEVEL_STATE_WON_LEVEL;
208     }
209   }
210 }
211 
212 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
213 {
214   static ButtonState buttonStates[8] = {0};
215   int cost = GetTowerCosts(towerType);
216   const char *text = TextFormat("%s: %d", name, cost);
217   buttonStates[towerType].isSelected = level->placementMode == towerType;
218   buttonStates[towerType].isDisabled = level->playerGold < cost;
219   if (Button(text, x, y, width, height, &buttonStates[towerType]))
220   {
221     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
222     level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
223   }
224 }
225 
226 float GetRandomFloat(float min, float max)
227 {
228   int random = GetRandomValue(0, 0xfffffff);
229   return ((float)random / (float)0xfffffff) * (max - min) + min;
230 }
231 
232 void DrawLevelGround(Level *level)
233 {
234   // draw checkerboard ground pattern
235   for (int x = -5; x <= 5; x += 1)
236   {
237     for (int y = -5; y <= 5; y += 1)
238     {
239       Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
240       DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
241     }
242   }
243 
244   int oldSeed = GetRandomValue(0, 0xfffffff);
245   SetRandomSeed(level->seed);
246   // increase probability for trees via duplicated entries
247   Model borderModels[64];
248   int maxRockCount = GetRandomValue(2, 6);
249   int maxTreeCount = GetRandomValue(10, 20);
250   int maxFirTreeCount = GetRandomValue(5, 10);
251   int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
252   int grassPatchCount = GetRandomValue(5, 30);
253 
254   int modelCount = 0;
255   for (int i = 0; i < maxRockCount && modelCount < 63; i++)
256   {
257     borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
258   }
259   for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
260   {
261     borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
262   }
263   for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
264   {
265     borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
266   }
267   for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
268   {
269     borderModels[modelCount++] = grassPatchModel[0];
270   }
271 
272   // draw some objects around the border of the map
273   Vector3 up = {0, 1, 0};
274   // a pseudo random number generator to get the same result every time
275   const float wiggle = 0.75f;
276   const int layerCount = 3;
277   for (int layer = 0; layer < layerCount; layer++)
278   {
279     int layerPos = 6 + layer;
280     for (int x = -6 + layer; x <= 6 + layer; x += 1)
281     {
282       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
283         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 
284         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
285       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
286         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 
287         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
288     }
289 
290     for (int z = -5 + layer; z <= 5 + layer; z += 1)
291     {
292       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
293         (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
294         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
295       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
296         (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
297         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
298     }
299   }
300 
301   SetRandomSeed(oldSeed);
302 }
303 
304 void DrawEnemyPath(Level *level, Color arrowColor)
305 {
306   const int castleX = 0, castleY = 0;
307   const int maxWaypointCount = 200;
308   const float timeStep = 1.0f;
309   Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
310 
311   // we start with a time offset to simulate the path, 
312   // this way the arrows are animated in a forward moving direction
313   // The time is wrapped around the time step to get a smooth animation
314   float timeOffset = fmodf(GetTime(), timeStep);
315 
316   for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
317   {
318     EnemyWave *wave = &level->waves[i];
319     if (wave->wave != level->currentWave)
320     {
321       continue;
322     }
323 
324     // use this dummy enemy to simulate the path
325     Enemy dummy = {
326       .enemyType = ENEMY_TYPE_MINION,
327       .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
328       .nextX = wave->spawnPosition.x,
329       .nextY = wave->spawnPosition.y,
330       .currentX = wave->spawnPosition.x,
331       .currentY = wave->spawnPosition.y,
332     };
333 
334     float deltaTime = timeOffset;
335     for (int j = 0; j < maxWaypointCount; j++)
336     {
337       int waypointPassedCount = 0;
338       Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
339       // after the initial variable starting offset, we use a fixed time step
340       deltaTime = timeStep;
341       dummy.simPosition = pos;
342 
343       // Update the dummy's position just like we do in the regular enemy update loop
344       for (int k = 0; k < waypointPassedCount; k++)
345       {
346         dummy.currentX = dummy.nextX;
347         dummy.currentY = dummy.nextY;
348         if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
349           Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
350         {
351           break;
352         }
353       }
354       if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
355       {
356         break;
357       }
358       
359       // get the angle we need to rotate the arrow model. The velocity is just fine for this.
360       float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
361       DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
362     }
363   }
364 }
365 
366 void DrawEnemyPaths(Level *level)
367 {
368   // disable depth testing for the path arrows
369   // flush the 3D batch to draw the arrows on top of everything
370   rlDrawRenderBatchActive();
371   rlDisableDepthTest();
372   DrawEnemyPath(level, (Color){64, 64, 64, 160});
373 
374   rlDrawRenderBatchActive();
375   rlEnableDepthTest();
376   DrawEnemyPath(level, WHITE);
377 }
378 
379 void DrawLevelBuildingPlacementState(Level *level)
380 {
381   BeginMode3D(level->camera);
382   DrawLevelGround(level);
383 
384   int blockedCellCount = 0;
385   Vector2 blockedCells[1];
386   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
387   float planeDistance = ray.position.y / -ray.direction.y;
388   float planeX = ray.direction.x * planeDistance + ray.position.x;
389   float planeY = ray.direction.z * planeDistance + ray.position.z;
390   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
391   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
392   if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
393   {
394     level->placementX = mapX;
395     level->placementY = mapY;
396   }
397   else
398   {
399     mapX = level->placementX;
400     mapY = level->placementY;
401   }
402   blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
403   PathFindingMapUpdate(blockedCellCount, blockedCells);
404 
405   TowerDraw();
406   EnemyDraw();
407   ProjectileDraw();
408   ParticleDraw();
409   DrawEnemyPaths(level);
410 
411 float dt = gameTime.deltaTime; 412
413 // smooth transition for the placement position using exponential decay
414 const float lambda = 15.0f; 415 float factor = 1.0f - expf(-lambda * dt);
416 417 level->placementTransitionPosition = 418 Vector2Lerp( 419 level->placementTransitionPosition, 420 (Vector2){mapX, mapY}, factor); 421 422 // draw the spring position for debugging the spring simulation 423 // first step: stiff spring, no simulation 424 Vector3 worldPlacementPosition = (Vector3){
425 level->placementTransitionPosition.x, 0.0f, level->placementTransitionPosition.y}; 426 Vector3 springTargetPosition = (Vector3){ 427 worldPlacementPosition.x, 1.0f, worldPlacementPosition.z}; 428 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, level->placementTowerSpring.position); 429 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * 200.0f); 430 // decay velocity 431 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-30.0f * dt)); 432 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange); 433 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position, 434 Vector3Scale(level->placementTowerSpring.velocity, dt));
435 436 DrawCube(level->placementTowerSpring.position, 0.1f, 0.1f, 0.1f, RED); 437 DrawLine3D(level->placementTowerSpring.position, worldPlacementPosition, YELLOW); 438 439 rlPushMatrix(); 440 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y); 441 DrawCubeWires((Vector3){0.0f, 0.0f, 0.0f}, 1.0f, 0.0f, 1.0f, RED); 442 443 444 // deactivated for now to debug the spring simulation 445 // Tower dummy = { 446 // .towerType = level->placementMode, 447 // }; 448 // TowerDrawSingle(dummy); 449 450 float bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f; 451 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f; 452 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f; 453 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f; 454 455 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE); 456 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE); 457 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE); 458 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE); 459 rlPopMatrix(); 460 461 guiState.isBlocked = 0; 462 463 EndMode3D(); 464 465 TowerDrawHealthBars(level->camera); 466 467 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0)) 468 { 469 level->nextState = LEVEL_STATE_BUILDING; 470 level->placementMode = TOWER_TYPE_NONE; 471 TraceLog(LOG_INFO, "Cancel building"); 472 } 473 474 if (Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0)) 475 { 476 level->nextState = LEVEL_STATE_BUILDING; 477 if (TowerTryAdd(level->placementMode, mapX, mapY)) 478 { 479 level->playerGold -= GetTowerCosts(level->placementMode); 480 level->placementMode = TOWER_TYPE_NONE; 481 } 482 } 483 } 484 485 void DrawLevelBuildingState(Level *level) 486 { 487 BeginMode3D(level->camera); 488 DrawLevelGround(level); 489 490 PathFindingMapUpdate(0, 0); 491 TowerDraw(); 492 EnemyDraw(); 493 ProjectileDraw(); 494 ParticleDraw(); 495 DrawEnemyPaths(level); 496 497 guiState.isBlocked = 0; 498 499 EndMode3D(); 500 501 TowerDrawHealthBars(level->camera); 502 503 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 504 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer"); 505 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 506 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult"); 507 508 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 509 { 510 level->nextState = LEVEL_STATE_RESET; 511 } 512 513 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 514 { 515 level->nextState = LEVEL_STATE_BATTLE; 516 } 517 518 const char *text = "Building phase"; 519 int textWidth = MeasureText(text, 20); 520 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 521 } 522 523 void InitBattleStateConditions(Level *level) 524 { 525 level->state = LEVEL_STATE_BATTLE; 526 level->nextState = LEVEL_STATE_NONE; 527 level->waveEndTimer = 0.0f; 528 for (int i = 0; i < 10; i++) 529 { 530 EnemyWave *wave = &level->waves[i]; 531 wave->spawned = 0; 532 wave->timeToSpawnNext = wave->delay; 533 } 534 } 535 536 void DrawLevelBattleState(Level *level) 537 { 538 BeginMode3D(level->camera); 539 DrawLevelGround(level); 540 TowerDraw(); 541 EnemyDraw(); 542 ProjectileDraw(); 543 ParticleDraw(); 544 guiState.isBlocked = 0; 545 EndMode3D(); 546 547 EnemyDrawHealthbars(level->camera); 548 TowerDrawHealthBars(level->camera); 549 550 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 551 { 552 level->nextState = LEVEL_STATE_RESET; 553 } 554 555 int maxCount = 0; 556 int remainingCount = 0; 557 for (int i = 0; i < 10; i++) 558 { 559 EnemyWave *wave = &level->waves[i]; 560 if (wave->wave != level->currentWave) 561 { 562 continue; 563 } 564 maxCount += wave->count; 565 remainingCount += wave->count - wave->spawned; 566 } 567 int aliveCount = EnemyCount(); 568 remainingCount += aliveCount; 569 570 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 571 int textWidth = MeasureText(text, 20); 572 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 573 } 574 575 void DrawLevel(Level *level) 576 { 577 switch (level->state) 578 { 579 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 580 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break; 581 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 582 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 583 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 584 default: break; 585 } 586 587 DrawLevelHud(level); 588 } 589 590 void UpdateLevel(Level *level) 591 { 592 if (level->state == LEVEL_STATE_BATTLE) 593 { 594 int activeWaves = 0; 595 for (int i = 0; i < 10; i++) 596 { 597 EnemyWave *wave = &level->waves[i]; 598 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 599 { 600 continue; 601 } 602 activeWaves++; 603 wave->timeToSpawnNext -= gameTime.deltaTime; 604 if (wave->timeToSpawnNext <= 0.0f) 605 { 606 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 607 if (enemy) 608 { 609 wave->timeToSpawnNext = wave->interval; 610 wave->spawned++; 611 } 612 } 613 } 614 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 615 level->waveEndTimer += gameTime.deltaTime; 616 if (level->waveEndTimer >= 2.0f) 617 { 618 level->nextState = LEVEL_STATE_LOST_WAVE; 619 } 620 } 621 else if (activeWaves == 0 && EnemyCount() == 0) 622 { 623 level->waveEndTimer += gameTime.deltaTime; 624 if (level->waveEndTimer >= 2.0f) 625 { 626 level->nextState = LEVEL_STATE_WON_WAVE; 627 } 628 } 629 } 630 631 PathFindingMapUpdate(0, 0); 632 EnemyUpdate(); 633 TowerUpdate(); 634 ProjectileUpdate(); 635 ParticleUpdate(); 636 637 if (level->nextState == LEVEL_STATE_RESET) 638 { 639 InitLevel(level); 640 } 641 642 if (level->nextState == LEVEL_STATE_BATTLE) 643 { 644 InitBattleStateConditions(level); 645 } 646 647 if (level->nextState == LEVEL_STATE_WON_WAVE) 648 { 649 level->currentWave++; 650 level->state = LEVEL_STATE_WON_WAVE; 651 } 652 653 if (level->nextState == LEVEL_STATE_LOST_WAVE) 654 { 655 level->state = LEVEL_STATE_LOST_WAVE; 656 } 657 658 if (level->nextState == LEVEL_STATE_BUILDING) 659 { 660 level->state = LEVEL_STATE_BUILDING; 661 } 662 663 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT) 664 { 665 level->state = LEVEL_STATE_BUILDING_PLACEMENT; 666 level->placementTransitionPosition = (Vector2){ 667 level->placementX, level->placementY}; 668 // initialize the spring to the current position 669 level->placementTowerSpring = (PhysicsPoint){ 670 .position = (Vector3){level->placementX, 1.0f, level->placementY}, 671 .velocity = (Vector3){0.0f, 0.0f, 0.0f}, 672 }; 673 } 674 675 if (level->nextState == LEVEL_STATE_WON_LEVEL) 676 { 677 // make something of this later 678 InitLevel(level); 679 } 680 681 level->nextState = LEVEL_STATE_NONE; 682 } 683 684 float nextSpawnTime = 0.0f; 685 686 void ResetGame() 687 { 688 InitLevel(currentLevel); 689 } 690 691 void InitGame() 692 { 693 TowerInit(); 694 EnemyInit(); 695 ProjectileInit(); 696 ParticleInit(); 697 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 698 699 currentLevel = levels; 700 InitLevel(currentLevel); 701 } 702 703 //# Immediate GUI functions 704 705 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 706 { 707 const float healthBarHeight = 6.0f; 708 const float healthBarOffset = 15.0f; 709 const float inset = 2.0f; 710 const float innerWidth = healthBarWidth - inset * 2; 711 const float innerHeight = healthBarHeight - inset * 2; 712 713 Vector2 screenPos = GetWorldToScreen(position, camera); 714 float centerX = screenPos.x - healthBarWidth * 0.5f; 715 float topY = screenPos.y - healthBarOffset; 716 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 717 float healthWidth = innerWidth * healthRatio; 718 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 719 } 720 721 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 722 { 723 Rectangle bounds = {x, y, width, height}; 724 int isPressed = 0; 725 int isSelected = state && state->isSelected; 726 int isDisabled = state && state->isDisabled; 727 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 728 { 729 Color color = isSelected ? DARKGRAY : GRAY; 730 DrawRectangle(x, y, width, height, color); 731 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 732 { 733 isPressed = 1; 734 } 735 guiState.isBlocked = 1; 736 } 737 else 738 { 739 Color color = isSelected ? WHITE : LIGHTGRAY; 740 DrawRectangle(x, y, width, height, color); 741 } 742 Font font = GetFontDefault(); 743 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 744 Color textColor = isDisabled ? GRAY : BLACK; 745 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 746 return isPressed; 747 } 748 749 //# Main game loop 750 751 void GameUpdate() 752 { 753 float dt = GetFrameTime(); 754 // cap maximum delta time to 0.1 seconds to prevent large time steps 755 if (dt > 0.1f) dt = 0.1f; 756 gameTime.time += dt; 757 gameTime.deltaTime = dt; 758 759 UpdateLevel(currentLevel); 760 } 761 762 int main(void) 763 { 764 int screenWidth, screenHeight; 765 GetPreferredSize(&screenWidth, &screenHeight);
766 InitWindow(screenWidth, screenHeight, "Tower defense"); 767 int fps = 30;
768 SetTargetFPS(fps); 769 770 LoadAssets(); 771 InitGame(); 772 773 while (!WindowShouldClose()) 774 { 775 if (IsPaused()) {
776 // canvas is not visible in browser - do nothing 777 continue; 778 } 779 780 if (IsKeyPressed(KEY_F)) 781 { 782 fps += 5; 783 if (fps > 60) fps = 10; 784 SetTargetFPS(fps);
785 TraceLog(LOG_INFO, "FPS set to %d", fps); 786 } 787 788 BeginDrawing(); 789 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 790 791 GameUpdate();
792 DrawLevel(currentLevel); 793
794 DrawText(TextFormat("FPS: %.1f : %d", 1.0f / (float) GetFrameTime(), fps), GetScreenWidth() - 180, 60, 20, WHITE); 795 EndDrawing(); 796 } 797 798 CloseWindow(); 799 800 return 0; 801 }
  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 typedef struct PhysicsPoint
 12 {
 13   Vector3 position;
 14   Vector3 velocity;
 15 } PhysicsPoint;
 16 
 17 #define ENEMY_MAX_PATH_COUNT 8
 18 #define ENEMY_MAX_COUNT 400
 19 #define ENEMY_TYPE_NONE 0
 20 #define ENEMY_TYPE_MINION 1
 21 
 22 #define PARTICLE_MAX_COUNT 400
 23 #define PARTICLE_TYPE_NONE 0
 24 #define PARTICLE_TYPE_EXPLOSION 1
 25 
 26 typedef struct Particle
 27 {
 28   uint8_t particleType;
 29   float spawnTime;
 30   float lifetime;
 31   Vector3 position;
 32   Vector3 velocity;
 33   Vector3 scale;
 34 } Particle;
 35 
 36 #define TOWER_MAX_COUNT 400
 37 enum TowerType
 38 {
 39   TOWER_TYPE_NONE,
 40   TOWER_TYPE_BASE,
 41   TOWER_TYPE_ARCHER,
 42   TOWER_TYPE_BALLISTA,
 43   TOWER_TYPE_CATAPULT,
 44   TOWER_TYPE_WALL,
 45   TOWER_TYPE_COUNT
 46 };
 47 
 48 typedef struct HitEffectConfig
 49 {
 50   float damage;
 51   float areaDamageRadius;
 52   float pushbackPowerDistance;
 53 } HitEffectConfig;
 54 
 55 typedef struct TowerTypeConfig
 56 {
 57   float cooldown;
 58   float range;
 59   float projectileSpeed;
 60   
 61   uint8_t cost;
 62   uint8_t projectileType;
 63   uint16_t maxHealth;
 64 
 65   HitEffectConfig hitEffect;
 66 } TowerTypeConfig;
 67 
 68 typedef struct Tower
 69 {
 70   int16_t x, y;
 71   uint8_t towerType;
 72   Vector2 lastTargetPosition;
 73   float cooldown;
 74   float damage;
 75 } Tower;
 76 
 77 typedef struct GameTime
 78 {
 79   float time;
 80   float deltaTime;
 81 } GameTime;
 82 
 83 typedef struct ButtonState {
 84   char isSelected;
 85   char isDisabled;
 86 } ButtonState;
 87 
 88 typedef struct GUIState {
 89   int isBlocked;
 90 } GUIState;
 91 
 92 typedef enum LevelState
 93 {
 94   LEVEL_STATE_NONE,
 95   LEVEL_STATE_BUILDING,
 96   LEVEL_STATE_BUILDING_PLACEMENT,
 97   LEVEL_STATE_BATTLE,
 98   LEVEL_STATE_WON_WAVE,
 99   LEVEL_STATE_LOST_WAVE,
100   LEVEL_STATE_WON_LEVEL,
101   LEVEL_STATE_RESET,
102 } LevelState;
103 
104 typedef struct EnemyWave {
105   uint8_t enemyType;
106   uint8_t wave;
107   uint16_t count;
108   float interval;
109   float delay;
110   Vector2 spawnPosition;
111 
112   uint16_t spawned;
113   float timeToSpawnNext;
114 } EnemyWave;
115 
116 #define ENEMY_MAX_WAVE_COUNT 10
117 
118 typedef struct Level
119 {
120   int seed;
121   LevelState state;
122   LevelState nextState;
123   Camera3D camera;
124   int placementMode;
125   int16_t placementX;
126   int16_t placementY;
127   Vector2 placementTransitionPosition;
128   PhysicsPoint placementTowerSpring;
129 
130   int initialGold;
131   int playerGold;
132 
133   EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
134   int currentWave;
135   float waveEndTimer;
136 } Level;
137 
138 typedef struct DeltaSrc
139 {
140   char x, y;
141 } DeltaSrc;
142 
143 typedef struct PathfindingMap
144 {
145   int width, height;
146   float scale;
147   float *distances;
148   long *towerIndex; 
149   DeltaSrc *deltaSrc;
150   float maxDistance;
151   Matrix toMapSpace;
152   Matrix toWorldSpace;
153 } PathfindingMap;
154 
155 // when we execute the pathfinding algorithm, we need to store the active nodes
156 // in a queue. Each node has a position, a distance from the start, and the
157 // position of the node that we came from.
158 typedef struct PathfindingNode
159 {
160   int16_t x, y, fromX, fromY;
161   float distance;
162 } PathfindingNode;
163 
164 typedef struct EnemyId
165 {
166   uint16_t index;
167   uint16_t generation;
168 } EnemyId;
169 
170 typedef struct EnemyClassConfig
171 {
172   float speed;
173   float health;
174   float radius;
175   float maxAcceleration;
176   float requiredContactTime;
177   float explosionDamage;
178   float explosionRange;
179   float explosionPushbackPower;
180   int goldValue;
181 } EnemyClassConfig;
182 
183 typedef struct Enemy
184 {
185   int16_t currentX, currentY;
186   int16_t nextX, nextY;
187   Vector2 simPosition;
188   Vector2 simVelocity;
189   uint16_t generation;
190   float walkedDistance;
191   float startMovingTime;
192   float damage, futureDamage;
193   float contactTime;
194   uint8_t enemyType;
195   uint8_t movePathCount;
196   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
197 } Enemy;
198 
199 // a unit that uses sprites to be drawn
200 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
201 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
202 typedef struct SpriteUnit
203 {
204   Rectangle srcRect;
205   Vector2 offset;
206   int frameCount;
207   float frameDuration;
208   Rectangle srcWeaponIdleRect;
209   Vector2 srcWeaponIdleOffset;
210   Rectangle srcWeaponCooldownRect;
211   Vector2 srcWeaponCooldownOffset;
212 } SpriteUnit;
213 
214 #define PROJECTILE_MAX_COUNT 1200
215 #define PROJECTILE_TYPE_NONE 0
216 #define PROJECTILE_TYPE_ARROW 1
217 #define PROJECTILE_TYPE_CATAPULT 2
218 #define PROJECTILE_TYPE_BALLISTA 3
219 
220 typedef struct Projectile
221 {
222   uint8_t projectileType;
223   float shootTime;
224   float arrivalTime;
225   float distance;
226   Vector3 position;
227   Vector3 target;
228   Vector3 directionNormal;
229   EnemyId targetEnemy;
230   HitEffectConfig hitEffectConfig;
231 } Projectile;
232 
233 //# Function declarations
234 float TowerGetMaxHealth(Tower *tower);
235 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
236 int EnemyAddDamageRange(Vector2 position, float range, float damage);
237 int EnemyAddDamage(Enemy *enemy, float damage);
238 
239 //# Enemy functions
240 void EnemyInit();
241 void EnemyDraw();
242 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
243 void EnemyUpdate();
244 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
245 float EnemyGetMaxHealth(Enemy *enemy);
246 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
247 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
248 EnemyId EnemyGetId(Enemy *enemy);
249 Enemy *EnemyTryResolve(EnemyId enemyId);
250 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
251 int EnemyAddDamage(Enemy *enemy, float damage);
252 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
253 int EnemyCount();
254 void EnemyDrawHealthbars(Camera3D camera);
255 
256 //# Tower functions
257 void TowerInit();
258 Tower *TowerGetAt(int16_t x, int16_t y);
259 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
260 Tower *GetTowerByType(uint8_t towerType);
261 int GetTowerCosts(uint8_t towerType);
262 float TowerGetMaxHealth(Tower *tower);
263 void TowerDraw();
264 void TowerDrawSingle(Tower tower);
265 void TowerUpdate();
266 void TowerDrawHealthBars(Camera3D camera);
267 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
268 
269 //# Particles
270 void ParticleInit();
271 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
272 void ParticleUpdate();
273 void ParticleDraw();
274 
275 //# Projectiles
276 void ProjectileInit();
277 void ProjectileDraw();
278 void ProjectileUpdate();
279 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
280 
281 //# Pathfinding map
282 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
283 float PathFindingGetDistance(int mapX, int mapY);
284 Vector2 PathFindingGetGradient(Vector3 world);
285 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
286 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
287 void PathFindingMapDraw();
288 
289 //# UI
290 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
291 
292 //# Level
293 void DrawLevelGround(Level *level);
294 void DrawEnemyPath(Level *level, Color arrowColor);
295 
296 //# variables
297 extern Level *currentLevel;
298 extern Enemy enemies[ENEMY_MAX_COUNT];
299 extern int enemyCount;
300 extern EnemyClassConfig enemyClassConfigs[];
301 
302 extern GUIState guiState;
303 extern GameTime gameTime;
304 extern Tower towers[TOWER_MAX_COUNT];
305 extern int towerCount;
306 
307 extern Texture2D palette, spriteSheet;
308 
309 #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
First try to make spring behave as desired; Press F to change the target framerate

Even though it is only a dozen lines or so, the code is quite complex, so let's add some comments to explain what is going on:

  1 // current position of the base
  2 Vector3 worldPlacementPosition = (Vector3){
  3   level->placementTransitionPosition.x, 0.0f, level->placementTransitionPosition.y};
  4 // target position of the spring tip if at rest
  5 Vector3 springTargetPosition = (Vector3){
  6   worldPlacementPosition.x, 1.0f, worldPlacementPosition.z};
  7 
  8 // difference from last position to the new target position
  9 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, level->placementTowerSpring.position);
 10 // velocity change we want to use to bring the spring tip closer to the rest position
 11 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * 200.0f);
 12 
 13 // decay existing velocity, each frame
 14 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-30.0f * dt));
 15 // add the velocity change to the current velocity
 16 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
 17 // update the spring tip position using the velocity
 18 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position, 
 19   Vector3Scale(level->placementTowerSpring.velocity, dt));

The used operations are just basic vector maths that uses addition, subtraction and multiplication. The complexity arises from the fact that this is executed each frame and that we have to tweak the values to get the desired effect.

The result of these calculations leads to the spring tip wobbling around when the tower is moved:

The tower wobbles around when moved.
The tower wobbling at 30FPS

While it looks OK at 30 FPS, at lower frame rates, it becomes overly wobbly:

The tower wobbles around when moved.
The tower wobbling at 15FPS

So the simulation is not frame rate independent. The reason is that the calculations have feedback loops: Every frame, the velocity is updated based on the current position. The frequency of these updates depends on the frame rate and thus the wobbling frequency depends on the frame rate. Essentially, the frequency of the wobbling can't happen at a higher rate than the frame rate (at least not with the math we are using here): Imagine we have only 2 frames per second, then we have only 2 tries to counteract the current velocity, hence the wobbling will be very slow.

This is not a problem we can solve in a frame rate independent manner. It is important to recognize this. Feedback loop calculations are probably almost always frame rate dependent.

What we need to do here is to run the simulation in a fixed time step loop. This makes also makes lot of calculations easier and ensures that the simulation is frame rate independent.

We have a gameTime global struct that we can use for this (I keep forgetting its existence, but I added it for this purpose).

  • 💾
  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 = { 10 .fixedDeltaTime = 1.0f / 60.0f, 11 };
12 13 Model floorTileAModel = {0}; 14 Model floorTileBModel = {0}; 15 Model treeModel[2] = {0}; 16 Model firTreeModel[2] = {0}; 17 Model rockModels[5] = {0}; 18 Model grassPatchModel[1] = {0}; 19 20 Model pathArrowModel = {0}; 21 Model greenArrowModel = {0}; 22 23 Texture2D palette, spriteSheet; 24 25 Level levels[] = { 26 [0] = { 27 .state = LEVEL_STATE_BUILDING, 28 .initialGold = 20, 29 .waves[0] = { 30 .enemyType = ENEMY_TYPE_MINION, 31 .wave = 0, 32 .count = 5, 33 .interval = 2.5f, 34 .delay = 1.0f, 35 .spawnPosition = {2, 6}, 36 }, 37 .waves[1] = { 38 .enemyType = ENEMY_TYPE_MINION, 39 .wave = 0, 40 .count = 5, 41 .interval = 2.5f, 42 .delay = 1.0f, 43 .spawnPosition = {-2, 6}, 44 }, 45 .waves[2] = { 46 .enemyType = ENEMY_TYPE_MINION, 47 .wave = 1, 48 .count = 20, 49 .interval = 1.5f, 50 .delay = 1.0f, 51 .spawnPosition = {0, 6}, 52 }, 53 .waves[3] = { 54 .enemyType = ENEMY_TYPE_MINION, 55 .wave = 2, 56 .count = 30, 57 .interval = 1.2f, 58 .delay = 1.0f, 59 .spawnPosition = {0, 6}, 60 } 61 }, 62 }; 63 64 Level *currentLevel = levels; 65 66 //# Game 67 68 static Model LoadGLBModel(char *filename) 69 { 70 Model model = LoadModel(TextFormat("data/%s.glb",filename)); 71 for (int i = 0; i < model.materialCount; i++) 72 { 73 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 74 } 75 return model; 76 } 77 78 void LoadAssets() 79 { 80 // load a sprite sheet that contains all units 81 spriteSheet = LoadTexture("data/spritesheet.png"); 82 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR); 83 84 // we'll use a palette texture to colorize the all buildings and environment art 85 palette = LoadTexture("data/palette.png"); 86 // The texture uses gradients on very small space, so we'll enable bilinear filtering 87 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 88 89 floorTileAModel = LoadGLBModel("floor-tile-a"); 90 floorTileBModel = LoadGLBModel("floor-tile-b"); 91 treeModel[0] = LoadGLBModel("leaftree-large-1-a"); 92 treeModel[1] = LoadGLBModel("leaftree-large-1-b"); 93 firTreeModel[0] = LoadGLBModel("firtree-1-a"); 94 firTreeModel[1] = LoadGLBModel("firtree-1-b"); 95 rockModels[0] = LoadGLBModel("rock-1"); 96 rockModels[1] = LoadGLBModel("rock-2"); 97 rockModels[2] = LoadGLBModel("rock-3"); 98 rockModels[3] = LoadGLBModel("rock-4"); 99 rockModels[4] = LoadGLBModel("rock-5"); 100 grassPatchModel[0] = LoadGLBModel("grass-patch-1"); 101 102 pathArrowModel = LoadGLBModel("direction-arrow-x"); 103 greenArrowModel = LoadGLBModel("green-arrow"); 104 } 105 106 void InitLevel(Level *level) 107 { 108 level->seed = (int)(GetTime() * 100.0f); 109 110 TowerInit(); 111 EnemyInit(); 112 ProjectileInit(); 113 ParticleInit(); 114 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 115 116 level->placementMode = 0; 117 level->state = LEVEL_STATE_BUILDING; 118 level->nextState = LEVEL_STATE_NONE; 119 level->playerGold = level->initialGold; 120 level->currentWave = 0; 121 level->placementX = -1; 122 level->placementY = 0; 123 124 Camera *camera = &level->camera; 125 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 126 camera->target = (Vector3){0.0f, 0.0f, 0.0f}; 127 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 128 camera->fovy = 10.0f; 129 camera->projection = CAMERA_ORTHOGRAPHIC; 130 } 131 132 void DrawLevelHud(Level *level) 133 { 134 const char *text = TextFormat("Gold: %d", level->playerGold); 135 Font font = GetFontDefault(); 136 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 137 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 138 } 139 140 void DrawLevelReportLostWave(Level *level) 141 { 142 BeginMode3D(level->camera); 143 DrawLevelGround(level); 144 TowerDraw(); 145 EnemyDraw(); 146 ProjectileDraw(); 147 ParticleDraw(); 148 guiState.isBlocked = 0; 149 EndMode3D(); 150 151 TowerDrawHealthBars(level->camera); 152 153 const char *text = "Wave lost"; 154 int textWidth = MeasureText(text, 20); 155 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 156 157 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 158 { 159 level->nextState = LEVEL_STATE_RESET; 160 } 161 } 162 163 int HasLevelNextWave(Level *level) 164 { 165 for (int i = 0; i < 10; i++) 166 { 167 EnemyWave *wave = &level->waves[i]; 168 if (wave->wave == level->currentWave) 169 { 170 return 1; 171 } 172 } 173 return 0; 174 } 175 176 void DrawLevelReportWonWave(Level *level) 177 { 178 BeginMode3D(level->camera); 179 DrawLevelGround(level); 180 TowerDraw(); 181 EnemyDraw(); 182 ProjectileDraw(); 183 ParticleDraw(); 184 guiState.isBlocked = 0; 185 EndMode3D(); 186 187 TowerDrawHealthBars(level->camera); 188 189 const char *text = "Wave won"; 190 int textWidth = MeasureText(text, 20); 191 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 192 193 194 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 195 { 196 level->nextState = LEVEL_STATE_RESET; 197 } 198 199 if (HasLevelNextWave(level)) 200 { 201 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 202 { 203 level->nextState = LEVEL_STATE_BUILDING; 204 } 205 } 206 else { 207 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 208 { 209 level->nextState = LEVEL_STATE_WON_LEVEL; 210 } 211 } 212 } 213 214 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 215 { 216 static ButtonState buttonStates[8] = {0}; 217 int cost = GetTowerCosts(towerType); 218 const char *text = TextFormat("%s: %d", name, cost); 219 buttonStates[towerType].isSelected = level->placementMode == towerType; 220 buttonStates[towerType].isDisabled = level->playerGold < cost; 221 if (Button(text, x, y, width, height, &buttonStates[towerType])) 222 { 223 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 224 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT; 225 } 226 } 227 228 float GetRandomFloat(float min, float max) 229 { 230 int random = GetRandomValue(0, 0xfffffff); 231 return ((float)random / (float)0xfffffff) * (max - min) + min; 232 } 233 234 void DrawLevelGround(Level *level) 235 { 236 // draw checkerboard ground pattern 237 for (int x = -5; x <= 5; x += 1) 238 { 239 for (int y = -5; y <= 5; y += 1) 240 { 241 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 242 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 243 } 244 } 245 246 int oldSeed = GetRandomValue(0, 0xfffffff); 247 SetRandomSeed(level->seed); 248 // increase probability for trees via duplicated entries 249 Model borderModels[64]; 250 int maxRockCount = GetRandomValue(2, 6); 251 int maxTreeCount = GetRandomValue(10, 20); 252 int maxFirTreeCount = GetRandomValue(5, 10); 253 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount; 254 int grassPatchCount = GetRandomValue(5, 30); 255 256 int modelCount = 0; 257 for (int i = 0; i < maxRockCount && modelCount < 63; i++) 258 { 259 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)]; 260 } 261 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++) 262 { 263 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)]; 264 } 265 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++) 266 { 267 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)]; 268 } 269 for (int i = 0; i < grassPatchCount && modelCount < 63; i++) 270 { 271 borderModels[modelCount++] = grassPatchModel[0]; 272 } 273 274 // draw some objects around the border of the map 275 Vector3 up = {0, 1, 0}; 276 // a pseudo random number generator to get the same result every time 277 const float wiggle = 0.75f; 278 const int layerCount = 3; 279 for (int layer = 0; layer < layerCount; layer++) 280 { 281 int layerPos = 6 + layer; 282 for (int x = -6 + layer; x <= 6 + layer; x += 1) 283 { 284 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 285 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 286 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 287 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 288 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 289 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 290 } 291 292 for (int z = -5 + layer; z <= 5 + layer; z += 1) 293 { 294 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 295 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 296 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 297 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 298 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 299 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 300 } 301 } 302 303 SetRandomSeed(oldSeed); 304 } 305 306 void DrawEnemyPath(Level *level, Color arrowColor) 307 { 308 const int castleX = 0, castleY = 0; 309 const int maxWaypointCount = 200; 310 const float timeStep = 1.0f; 311 Vector3 arrowScale = {0.75f, 0.75f, 0.75f}; 312 313 // we start with a time offset to simulate the path, 314 // this way the arrows are animated in a forward moving direction 315 // The time is wrapped around the time step to get a smooth animation 316 float timeOffset = fmodf(GetTime(), timeStep); 317 318 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 319 { 320 EnemyWave *wave = &level->waves[i]; 321 if (wave->wave != level->currentWave) 322 { 323 continue; 324 } 325 326 // use this dummy enemy to simulate the path 327 Enemy dummy = { 328 .enemyType = ENEMY_TYPE_MINION, 329 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y}, 330 .nextX = wave->spawnPosition.x, 331 .nextY = wave->spawnPosition.y, 332 .currentX = wave->spawnPosition.x, 333 .currentY = wave->spawnPosition.y, 334 }; 335 336 float deltaTime = timeOffset; 337 for (int j = 0; j < maxWaypointCount; j++) 338 { 339 int waypointPassedCount = 0; 340 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount); 341 // after the initial variable starting offset, we use a fixed time step 342 deltaTime = timeStep; 343 dummy.simPosition = pos; 344 345 // Update the dummy's position just like we do in the regular enemy update loop 346 for (int k = 0; k < waypointPassedCount; k++) 347 { 348 dummy.currentX = dummy.nextX; 349 dummy.currentY = dummy.nextY; 350 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) && 351 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 352 { 353 break; 354 } 355 } 356 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 357 { 358 break; 359 } 360 361 // get the angle we need to rotate the arrow model. The velocity is just fine for this. 362 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f; 363 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor); 364 } 365 } 366 } 367 368 void DrawEnemyPaths(Level *level) 369 { 370 // disable depth testing for the path arrows 371 // flush the 3D batch to draw the arrows on top of everything 372 rlDrawRenderBatchActive(); 373 rlDisableDepthTest(); 374 DrawEnemyPath(level, (Color){64, 64, 64, 160}); 375 376 rlDrawRenderBatchActive(); 377 rlEnableDepthTest(); 378 DrawEnemyPath(level, WHITE); 379 } 380 381 void DrawLevelBuildingPlacementState(Level *level) 382 { 383 BeginMode3D(level->camera); 384 DrawLevelGround(level); 385 386 int blockedCellCount = 0; 387 Vector2 blockedCells[1]; 388 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 389 float planeDistance = ray.position.y / -ray.direction.y; 390 float planeX = ray.direction.x * planeDistance + ray.position.x; 391 float planeY = ray.direction.z * planeDistance + ray.position.z; 392 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 393 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 394 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) 395 { 396 level->placementX = mapX; 397 level->placementY = mapY; 398 } 399 else 400 { 401 mapX = level->placementX; 402 mapY = level->placementY; 403 } 404 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY}; 405 PathFindingMapUpdate(blockedCellCount, blockedCells); 406 407 TowerDraw(); 408 EnemyDraw(); 409 ProjectileDraw(); 410 ParticleDraw(); 411 DrawEnemyPaths(level);
412 413 float dt = gameTime.fixedDeltaTime;
414 // smooth transition for the placement position using exponential decay 415 const float lambda = 15.0f;
416 float factor = 1.0f - expf(-lambda * dt); 417 418 for (int i = 0; i < gameTime.fixedStepCount; i++)
419 { 420 level->placementTransitionPosition = 421 Vector2Lerp( 422 level->placementTransitionPosition, 423 (Vector2){mapX, mapY}, factor); 424 425 // draw the spring position for debugging the spring simulation 426 // first step: stiff spring, no simulation 427 Vector3 worldPlacementPosition = (Vector3){ 428 level->placementTransitionPosition.x, 0.0f, level->placementTransitionPosition.y}; 429 Vector3 springTargetPosition = (Vector3){ 430 worldPlacementPosition.x, 1.0f, worldPlacementPosition.z}; 431 Vector3 springPointDelta = Vector3Subtract(springTargetPosition, level->placementTowerSpring.position); 432 Vector3 velocityChange = Vector3Scale(springPointDelta, dt * 200.0f);
433 // decay velocity
434 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-120.0f * dt)); 435 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange);
436 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position, 437 Vector3Scale(level->placementTowerSpring.velocity, dt));
438 } 439
440 DrawCube(level->placementTowerSpring.position, 0.1f, 0.1f, 0.1f, RED); 441 DrawLine3D(level->placementTowerSpring.position, (Vector3){
442 level->placementTransitionPosition.x, 0.0f, level->placementTransitionPosition.y}, YELLOW); 443 444 rlPushMatrix(); 445 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y); 446 DrawCubeWires((Vector3){0.0f, 0.0f, 0.0f}, 1.0f, 0.0f, 1.0f, RED); 447 448 449 // deactivated for now to debug the spring simulation 450 // Tower dummy = { 451 // .towerType = level->placementMode, 452 // }; 453 // TowerDrawSingle(dummy); 454 455 float bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f; 456 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f; 457 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f; 458 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f; 459 460 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE); 461 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE); 462 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE); 463 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE); 464 rlPopMatrix(); 465 466 guiState.isBlocked = 0; 467 468 EndMode3D(); 469 470 TowerDrawHealthBars(level->camera); 471 472 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0)) 473 { 474 level->nextState = LEVEL_STATE_BUILDING; 475 level->placementMode = TOWER_TYPE_NONE; 476 TraceLog(LOG_INFO, "Cancel building"); 477 } 478 479 if (Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0)) 480 { 481 level->nextState = LEVEL_STATE_BUILDING; 482 if (TowerTryAdd(level->placementMode, mapX, mapY)) 483 { 484 level->playerGold -= GetTowerCosts(level->placementMode); 485 level->placementMode = TOWER_TYPE_NONE; 486 } 487 } 488 } 489 490 void DrawLevelBuildingState(Level *level) 491 { 492 BeginMode3D(level->camera); 493 DrawLevelGround(level); 494 495 PathFindingMapUpdate(0, 0); 496 TowerDraw(); 497 EnemyDraw(); 498 ProjectileDraw(); 499 ParticleDraw(); 500 DrawEnemyPaths(level); 501 502 guiState.isBlocked = 0; 503 504 EndMode3D(); 505 506 TowerDrawHealthBars(level->camera); 507 508 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 509 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer"); 510 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 511 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult"); 512 513 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 514 { 515 level->nextState = LEVEL_STATE_RESET; 516 } 517 518 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 519 { 520 level->nextState = LEVEL_STATE_BATTLE; 521 } 522 523 const char *text = "Building phase"; 524 int textWidth = MeasureText(text, 20); 525 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 526 } 527 528 void InitBattleStateConditions(Level *level) 529 { 530 level->state = LEVEL_STATE_BATTLE; 531 level->nextState = LEVEL_STATE_NONE; 532 level->waveEndTimer = 0.0f; 533 for (int i = 0; i < 10; i++) 534 { 535 EnemyWave *wave = &level->waves[i]; 536 wave->spawned = 0; 537 wave->timeToSpawnNext = wave->delay; 538 } 539 } 540 541 void DrawLevelBattleState(Level *level) 542 { 543 BeginMode3D(level->camera); 544 DrawLevelGround(level); 545 TowerDraw(); 546 EnemyDraw(); 547 ProjectileDraw(); 548 ParticleDraw(); 549 guiState.isBlocked = 0; 550 EndMode3D(); 551 552 EnemyDrawHealthbars(level->camera); 553 TowerDrawHealthBars(level->camera); 554 555 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 556 { 557 level->nextState = LEVEL_STATE_RESET; 558 } 559 560 int maxCount = 0; 561 int remainingCount = 0; 562 for (int i = 0; i < 10; i++) 563 { 564 EnemyWave *wave = &level->waves[i]; 565 if (wave->wave != level->currentWave) 566 { 567 continue; 568 } 569 maxCount += wave->count; 570 remainingCount += wave->count - wave->spawned; 571 } 572 int aliveCount = EnemyCount(); 573 remainingCount += aliveCount; 574 575 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 576 int textWidth = MeasureText(text, 20); 577 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 578 } 579 580 void DrawLevel(Level *level) 581 { 582 switch (level->state) 583 { 584 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 585 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break; 586 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 587 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 588 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 589 default: break; 590 } 591 592 DrawLevelHud(level); 593 } 594 595 void UpdateLevel(Level *level) 596 { 597 if (level->state == LEVEL_STATE_BATTLE) 598 { 599 int activeWaves = 0; 600 for (int i = 0; i < 10; i++) 601 { 602 EnemyWave *wave = &level->waves[i]; 603 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 604 { 605 continue; 606 } 607 activeWaves++; 608 wave->timeToSpawnNext -= gameTime.deltaTime; 609 if (wave->timeToSpawnNext <= 0.0f) 610 { 611 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 612 if (enemy) 613 { 614 wave->timeToSpawnNext = wave->interval; 615 wave->spawned++; 616 } 617 } 618 } 619 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 620 level->waveEndTimer += gameTime.deltaTime; 621 if (level->waveEndTimer >= 2.0f) 622 { 623 level->nextState = LEVEL_STATE_LOST_WAVE; 624 } 625 } 626 else if (activeWaves == 0 && EnemyCount() == 0) 627 { 628 level->waveEndTimer += gameTime.deltaTime; 629 if (level->waveEndTimer >= 2.0f) 630 { 631 level->nextState = LEVEL_STATE_WON_WAVE; 632 } 633 } 634 } 635 636 PathFindingMapUpdate(0, 0); 637 EnemyUpdate(); 638 TowerUpdate(); 639 ProjectileUpdate(); 640 ParticleUpdate(); 641 642 if (level->nextState == LEVEL_STATE_RESET) 643 { 644 InitLevel(level); 645 } 646 647 if (level->nextState == LEVEL_STATE_BATTLE) 648 { 649 InitBattleStateConditions(level); 650 } 651 652 if (level->nextState == LEVEL_STATE_WON_WAVE) 653 { 654 level->currentWave++; 655 level->state = LEVEL_STATE_WON_WAVE; 656 } 657 658 if (level->nextState == LEVEL_STATE_LOST_WAVE) 659 { 660 level->state = LEVEL_STATE_LOST_WAVE; 661 } 662 663 if (level->nextState == LEVEL_STATE_BUILDING) 664 { 665 level->state = LEVEL_STATE_BUILDING; 666 } 667 668 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT) 669 { 670 level->state = LEVEL_STATE_BUILDING_PLACEMENT; 671 level->placementTransitionPosition = (Vector2){ 672 level->placementX, level->placementY}; 673 // initialize the spring to the current position 674 level->placementTowerSpring = (PhysicsPoint){ 675 .position = (Vector3){level->placementX, 1.0f, level->placementY}, 676 .velocity = (Vector3){0.0f, 0.0f, 0.0f}, 677 }; 678 } 679 680 if (level->nextState == LEVEL_STATE_WON_LEVEL) 681 { 682 // make something of this later 683 InitLevel(level); 684 } 685 686 level->nextState = LEVEL_STATE_NONE; 687 } 688 689 float nextSpawnTime = 0.0f; 690 691 void ResetGame() 692 { 693 InitLevel(currentLevel); 694 } 695 696 void InitGame() 697 { 698 TowerInit(); 699 EnemyInit(); 700 ProjectileInit(); 701 ParticleInit(); 702 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 703 704 currentLevel = levels; 705 InitLevel(currentLevel); 706 } 707 708 //# Immediate GUI functions 709 710 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 711 { 712 const float healthBarHeight = 6.0f; 713 const float healthBarOffset = 15.0f; 714 const float inset = 2.0f; 715 const float innerWidth = healthBarWidth - inset * 2; 716 const float innerHeight = healthBarHeight - inset * 2; 717 718 Vector2 screenPos = GetWorldToScreen(position, camera); 719 float centerX = screenPos.x - healthBarWidth * 0.5f; 720 float topY = screenPos.y - healthBarOffset; 721 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 722 float healthWidth = innerWidth * healthRatio; 723 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 724 } 725 726 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 727 { 728 Rectangle bounds = {x, y, width, height}; 729 int isPressed = 0; 730 int isSelected = state && state->isSelected; 731 int isDisabled = state && state->isDisabled; 732 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 733 { 734 Color color = isSelected ? DARKGRAY : GRAY; 735 DrawRectangle(x, y, width, height, color); 736 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 737 { 738 isPressed = 1; 739 } 740 guiState.isBlocked = 1; 741 } 742 else 743 { 744 Color color = isSelected ? WHITE : LIGHTGRAY; 745 DrawRectangle(x, y, width, height, color); 746 } 747 Font font = GetFontDefault(); 748 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 749 Color textColor = isDisabled ? GRAY : BLACK;
750 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 751 return isPressed; 752 } 753 754 //# Main game loop 755 756 void GameUpdate()
757 { 758 UpdateLevel(currentLevel); 759 } 760 761 int main(void) 762 { 763 int screenWidth, screenHeight; 764 GetPreferredSize(&screenWidth, &screenHeight); 765 InitWindow(screenWidth, screenHeight, "Tower defense"); 766 int fps = 30; 767 SetTargetFPS(fps); 768 769 LoadAssets(); 770 InitGame(); 771 772 while (!WindowShouldClose()) 773 { 774 if (IsPaused()) { 775 // canvas is not visible in browser - do nothing 776 continue; 777 }
778 779 if (IsKeyPressed(KEY_F)) 780 { 781 fps += 5; 782 if (fps > 60) fps = 10; 783 SetTargetFPS(fps); 784 TraceLog(LOG_INFO, "FPS set to %d", fps); 785 } 786 787 float dt = GetFrameTime();
788 // cap maximum delta time to 0.1 seconds to prevent large time steps 789 if (dt > 0.1f) dt = 0.1f; 790 gameTime.time += dt; 791 gameTime.deltaTime = dt; 792 793 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart; 794 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime); 795
796 BeginDrawing(); 797 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 798
799 GameUpdate(); 800 DrawLevel(currentLevel); 801 802 DrawText(TextFormat("FPS: %.1f : %d", 1.0f / (float) GetFrameTime(), fps), GetScreenWidth() - 180, 60, 20, WHITE); 803 EndDrawing(); 804 805 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime; 806 } 807 808 CloseWindow(); 809 810 return 0; 811 }
  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 typedef struct PhysicsPoint
 12 {
 13   Vector3 position;
 14   Vector3 velocity;
 15 } PhysicsPoint;
 16 
 17 #define ENEMY_MAX_PATH_COUNT 8
 18 #define ENEMY_MAX_COUNT 400
 19 #define ENEMY_TYPE_NONE 0
 20 #define ENEMY_TYPE_MINION 1
 21 
 22 #define PARTICLE_MAX_COUNT 400
 23 #define PARTICLE_TYPE_NONE 0
 24 #define PARTICLE_TYPE_EXPLOSION 1
 25 
 26 typedef struct Particle
 27 {
 28   uint8_t particleType;
 29   float spawnTime;
 30   float lifetime;
 31   Vector3 position;
 32   Vector3 velocity;
 33   Vector3 scale;
 34 } Particle;
 35 
 36 #define TOWER_MAX_COUNT 400
 37 enum TowerType
 38 {
 39   TOWER_TYPE_NONE,
 40   TOWER_TYPE_BASE,
 41   TOWER_TYPE_ARCHER,
 42   TOWER_TYPE_BALLISTA,
 43   TOWER_TYPE_CATAPULT,
 44   TOWER_TYPE_WALL,
 45   TOWER_TYPE_COUNT
 46 };
 47 
 48 typedef struct HitEffectConfig
 49 {
 50   float damage;
 51   float areaDamageRadius;
 52   float pushbackPowerDistance;
 53 } HitEffectConfig;
 54 
 55 typedef struct TowerTypeConfig
 56 {
 57   float cooldown;
 58   float range;
 59   float projectileSpeed;
 60   
 61   uint8_t cost;
 62   uint8_t projectileType;
 63   uint16_t maxHealth;
 64 
 65   HitEffectConfig hitEffect;
 66 } TowerTypeConfig;
 67 
 68 typedef struct Tower
 69 {
 70   int16_t x, y;
 71   uint8_t towerType;
 72   Vector2 lastTargetPosition;
 73   float cooldown;
 74   float damage;
 75 } Tower;
 76 
 77 typedef struct GameTime
 78 {
 79   float time;
 80   float deltaTime;
81 uint32_t frameCount; 82 83 float fixedDeltaTime; 84 // leaving the fixed time stepping to the update functions, 85 // we need to know the fixed time at the start of the frame 86 float fixedTimeStart; 87 // and the number of fixed steps that we have to make this frame 88 // The fixedTime is fixedTimeStart + n * fixedStepCount 89 uint8_t fixedStepCount;
90 } GameTime; 91 92 typedef struct ButtonState { 93 char isSelected; 94 char isDisabled; 95 } ButtonState; 96 97 typedef struct GUIState { 98 int isBlocked; 99 } GUIState; 100 101 typedef enum LevelState 102 { 103 LEVEL_STATE_NONE, 104 LEVEL_STATE_BUILDING, 105 LEVEL_STATE_BUILDING_PLACEMENT, 106 LEVEL_STATE_BATTLE, 107 LEVEL_STATE_WON_WAVE, 108 LEVEL_STATE_LOST_WAVE, 109 LEVEL_STATE_WON_LEVEL, 110 LEVEL_STATE_RESET, 111 } LevelState; 112 113 typedef struct EnemyWave { 114 uint8_t enemyType; 115 uint8_t wave; 116 uint16_t count; 117 float interval; 118 float delay; 119 Vector2 spawnPosition; 120 121 uint16_t spawned; 122 float timeToSpawnNext; 123 } EnemyWave; 124 125 #define ENEMY_MAX_WAVE_COUNT 10 126 127 typedef struct Level 128 { 129 int seed; 130 LevelState state; 131 LevelState nextState; 132 Camera3D camera; 133 int placementMode; 134 int16_t placementX; 135 int16_t placementY; 136 Vector2 placementTransitionPosition; 137 PhysicsPoint placementTowerSpring; 138 139 int initialGold; 140 int playerGold; 141 142 EnemyWave waves[ENEMY_MAX_WAVE_COUNT]; 143 int currentWave; 144 float waveEndTimer; 145 } Level; 146 147 typedef struct DeltaSrc 148 { 149 char x, y; 150 } DeltaSrc; 151 152 typedef struct PathfindingMap 153 { 154 int width, height; 155 float scale; 156 float *distances; 157 long *towerIndex; 158 DeltaSrc *deltaSrc; 159 float maxDistance; 160 Matrix toMapSpace; 161 Matrix toWorldSpace; 162 } PathfindingMap; 163 164 // when we execute the pathfinding algorithm, we need to store the active nodes 165 // in a queue. Each node has a position, a distance from the start, and the 166 // position of the node that we came from. 167 typedef struct PathfindingNode 168 { 169 int16_t x, y, fromX, fromY; 170 float distance; 171 } PathfindingNode; 172 173 typedef struct EnemyId 174 { 175 uint16_t index; 176 uint16_t generation; 177 } EnemyId; 178 179 typedef struct EnemyClassConfig 180 { 181 float speed; 182 float health; 183 float radius; 184 float maxAcceleration; 185 float requiredContactTime; 186 float explosionDamage; 187 float explosionRange; 188 float explosionPushbackPower; 189 int goldValue; 190 } EnemyClassConfig; 191 192 typedef struct Enemy 193 { 194 int16_t currentX, currentY; 195 int16_t nextX, nextY; 196 Vector2 simPosition; 197 Vector2 simVelocity; 198 uint16_t generation; 199 float walkedDistance; 200 float startMovingTime; 201 float damage, futureDamage; 202 float contactTime; 203 uint8_t enemyType; 204 uint8_t movePathCount; 205 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 206 } Enemy; 207 208 // a unit that uses sprites to be drawn 209 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0 210 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1 211 typedef struct SpriteUnit 212 { 213 Rectangle srcRect; 214 Vector2 offset; 215 int frameCount; 216 float frameDuration; 217 Rectangle srcWeaponIdleRect; 218 Vector2 srcWeaponIdleOffset; 219 Rectangle srcWeaponCooldownRect; 220 Vector2 srcWeaponCooldownOffset; 221 } SpriteUnit; 222 223 #define PROJECTILE_MAX_COUNT 1200 224 #define PROJECTILE_TYPE_NONE 0 225 #define PROJECTILE_TYPE_ARROW 1 226 #define PROJECTILE_TYPE_CATAPULT 2 227 #define PROJECTILE_TYPE_BALLISTA 3 228 229 typedef struct Projectile 230 { 231 uint8_t projectileType; 232 float shootTime; 233 float arrivalTime; 234 float distance; 235 Vector3 position; 236 Vector3 target; 237 Vector3 directionNormal; 238 EnemyId targetEnemy; 239 HitEffectConfig hitEffectConfig; 240 } Projectile; 241 242 //# Function declarations 243 float TowerGetMaxHealth(Tower *tower); 244 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 245 int EnemyAddDamageRange(Vector2 position, float range, float damage); 246 int EnemyAddDamage(Enemy *enemy, float damage); 247 248 //# Enemy functions 249 void EnemyInit(); 250 void EnemyDraw(); 251 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 252 void EnemyUpdate(); 253 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 254 float EnemyGetMaxHealth(Enemy *enemy); 255 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 256 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 257 EnemyId EnemyGetId(Enemy *enemy); 258 Enemy *EnemyTryResolve(EnemyId enemyId); 259 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 260 int EnemyAddDamage(Enemy *enemy, float damage); 261 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 262 int EnemyCount(); 263 void EnemyDrawHealthbars(Camera3D camera); 264 265 //# Tower functions 266 void TowerInit(); 267 Tower *TowerGetAt(int16_t x, int16_t y); 268 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 269 Tower *GetTowerByType(uint8_t towerType); 270 int GetTowerCosts(uint8_t towerType); 271 float TowerGetMaxHealth(Tower *tower); 272 void TowerDraw(); 273 void TowerDrawSingle(Tower tower); 274 void TowerUpdate(); 275 void TowerDrawHealthBars(Camera3D camera); 276 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 277 278 //# Particles 279 void ParticleInit(); 280 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime); 281 void ParticleUpdate(); 282 void ParticleDraw(); 283 284 //# Projectiles 285 void ProjectileInit(); 286 void ProjectileDraw(); 287 void ProjectileUpdate(); 288 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig); 289 290 //# Pathfinding map 291 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 292 float PathFindingGetDistance(int mapX, int mapY); 293 Vector2 PathFindingGetGradient(Vector3 world); 294 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 295 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells); 296 void PathFindingMapDraw(); 297 298 //# UI 299 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth); 300 301 //# Level 302 void DrawLevelGround(Level *level); 303 void DrawEnemyPath(Level *level, Color arrowColor); 304 305 //# variables 306 extern Level *currentLevel; 307 extern Enemy enemies[ENEMY_MAX_COUNT]; 308 extern int enemyCount; 309 extern EnemyClassConfig enemyClassConfigs[]; 310 311 extern GUIState guiState; 312 extern GameTime gameTime; 313 extern Tower towers[TOWER_MAX_COUNT]; 314 extern int towerCount; 315 316 extern Texture2D palette, spriteSheet; 317 318 #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
The spring simulation runs now the same, regardless of the frame rate.

With these changes, the spring simulation is now frame rate independent. I didn't introducing here a fixed update step function call, like it is often done in games (e.g. Unity), it is still part of the draw loop. Not the cleanest way, but it gets the job done.

Personally, I find it easier this way, but I am sure there are people who would disagree 😅.

Anyway, this is how the spring simulation looks like at different frame rates:

The tower wobbles around when moved.
The tower wobbling at 10FPS
The tower wobbles around when moved.
The tower wobbling at 30FPS

With this issue out of the way, we can now refine the spring behavior: Currently, the spring is staying at the same height all the time. The current feedback is only ensuring that the tower will get upright again. We can add now the typical spring behavior to the system: When the spring is stretched (length > 1.0) the tip is pulled towards the base (not the tip's resting position). When the spring is compressed (length < 1.0), the tip is pushed away from the base. This should give the spring a certain bounciness. We will later use the stretch and compression to scale the tower model to make it look like it is squashing and stretching.

  • 💾
  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 = {
 10   .fixedDeltaTime = 1.0f / 60.0f,
 11 };
 12 
 13 Model floorTileAModel = {0};
 14 Model floorTileBModel = {0};
 15 Model treeModel[2] = {0};
 16 Model firTreeModel[2] = {0};
 17 Model rockModels[5] = {0};
 18 Model grassPatchModel[1] = {0};
 19 
 20 Model pathArrowModel = {0};
 21 Model greenArrowModel = {0};
 22 
 23 Texture2D palette, spriteSheet;
 24 
 25 Level levels[] = {
 26   [0] = {
 27     .state = LEVEL_STATE_BUILDING,
 28     .initialGold = 20,
 29     .waves[0] = {
 30       .enemyType = ENEMY_TYPE_MINION,
 31       .wave = 0,
 32       .count = 5,
 33       .interval = 2.5f,
 34       .delay = 1.0f,
 35       .spawnPosition = {2, 6},
 36     },
 37     .waves[1] = {
 38       .enemyType = ENEMY_TYPE_MINION,
 39       .wave = 0,
 40       .count = 5,
 41       .interval = 2.5f,
 42       .delay = 1.0f,
 43       .spawnPosition = {-2, 6},
 44     },
 45     .waves[2] = {
 46       .enemyType = ENEMY_TYPE_MINION,
 47       .wave = 1,
 48       .count = 20,
 49       .interval = 1.5f,
 50       .delay = 1.0f,
 51       .spawnPosition = {0, 6},
 52     },
 53     .waves[3] = {
 54       .enemyType = ENEMY_TYPE_MINION,
 55       .wave = 2,
 56       .count = 30,
 57       .interval = 1.2f,
 58       .delay = 1.0f,
 59       .spawnPosition = {0, 6},
 60     }
 61   },
 62 };
 63 
 64 Level *currentLevel = levels;
 65 
 66 //# Game
 67 
 68 static Model LoadGLBModel(char *filename)
 69 {
 70   Model model = LoadModel(TextFormat("data/%s.glb",filename));
 71   for (int i = 0; i < model.materialCount; i++)
 72   {
 73     model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 74   }
 75   return model;
 76 }
 77 
 78 void LoadAssets()
 79 {
 80   // load a sprite sheet that contains all units
 81   spriteSheet = LoadTexture("data/spritesheet.png");
 82   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
 83 
 84   // we'll use a palette texture to colorize the all buildings and environment art
 85   palette = LoadTexture("data/palette.png");
 86   // The texture uses gradients on very small space, so we'll enable bilinear filtering
 87   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
 88 
 89   floorTileAModel = LoadGLBModel("floor-tile-a");
 90   floorTileBModel = LoadGLBModel("floor-tile-b");
 91   treeModel[0] = LoadGLBModel("leaftree-large-1-a");
 92   treeModel[1] = LoadGLBModel("leaftree-large-1-b");
 93   firTreeModel[0] = LoadGLBModel("firtree-1-a");
 94   firTreeModel[1] = LoadGLBModel("firtree-1-b");
 95   rockModels[0] = LoadGLBModel("rock-1");
 96   rockModels[1] = LoadGLBModel("rock-2");
 97   rockModels[2] = LoadGLBModel("rock-3");
 98   rockModels[3] = LoadGLBModel("rock-4");
 99   rockModels[4] = LoadGLBModel("rock-5");
100   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
101 
102   pathArrowModel = LoadGLBModel("direction-arrow-x");
103   greenArrowModel = LoadGLBModel("green-arrow");
104 }
105 
106 void InitLevel(Level *level)
107 {
108   level->seed = (int)(GetTime() * 100.0f);
109 
110   TowerInit();
111   EnemyInit();
112   ProjectileInit();
113   ParticleInit();
114   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
115 
116   level->placementMode = 0;
117   level->state = LEVEL_STATE_BUILDING;
118   level->nextState = LEVEL_STATE_NONE;
119   level->playerGold = level->initialGold;
120   level->currentWave = 0;
121   level->placementX = -1;
122   level->placementY = 0;
123 
124   Camera *camera = &level->camera;
125   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
126   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
127   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
128   camera->fovy = 10.0f;
129   camera->projection = CAMERA_ORTHOGRAPHIC;
130 }
131 
132 void DrawLevelHud(Level *level)
133 {
134   const char *text = TextFormat("Gold: %d", level->playerGold);
135   Font font = GetFontDefault();
136   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
137   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
138 }
139 
140 void DrawLevelReportLostWave(Level *level)
141 {
142   BeginMode3D(level->camera);
143   DrawLevelGround(level);
144   TowerDraw();
145   EnemyDraw();
146   ProjectileDraw();
147   ParticleDraw();
148   guiState.isBlocked = 0;
149   EndMode3D();
150 
151   TowerDrawHealthBars(level->camera);
152 
153   const char *text = "Wave lost";
154   int textWidth = MeasureText(text, 20);
155   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
156 
157   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
158   {
159     level->nextState = LEVEL_STATE_RESET;
160   }
161 }
162 
163 int HasLevelNextWave(Level *level)
164 {
165   for (int i = 0; i < 10; i++)
166   {
167     EnemyWave *wave = &level->waves[i];
168     if (wave->wave == level->currentWave)
169     {
170       return 1;
171     }
172   }
173   return 0;
174 }
175 
176 void DrawLevelReportWonWave(Level *level)
177 {
178   BeginMode3D(level->camera);
179   DrawLevelGround(level);
180   TowerDraw();
181   EnemyDraw();
182   ProjectileDraw();
183   ParticleDraw();
184   guiState.isBlocked = 0;
185   EndMode3D();
186 
187   TowerDrawHealthBars(level->camera);
188 
189   const char *text = "Wave won";
190   int textWidth = MeasureText(text, 20);
191   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
192 
193 
194   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
195   {
196     level->nextState = LEVEL_STATE_RESET;
197   }
198 
199   if (HasLevelNextWave(level))
200   {
201     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
202     {
203       level->nextState = LEVEL_STATE_BUILDING;
204     }
205   }
206   else {
207     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
208     {
209       level->nextState = LEVEL_STATE_WON_LEVEL;
210     }
211   }
212 }
213 
214 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
215 {
216   static ButtonState buttonStates[8] = {0};
217   int cost = GetTowerCosts(towerType);
218   const char *text = TextFormat("%s: %d", name, cost);
219   buttonStates[towerType].isSelected = level->placementMode == towerType;
220   buttonStates[towerType].isDisabled = level->playerGold < cost;
221   if (Button(text, x, y, width, height, &buttonStates[towerType]))
222   {
223     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
224     level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
225   }
226 }
227 
228 float GetRandomFloat(float min, float max)
229 {
230   int random = GetRandomValue(0, 0xfffffff);
231   return ((float)random / (float)0xfffffff) * (max - min) + min;
232 }
233 
234 void DrawLevelGround(Level *level)
235 {
236   // draw checkerboard ground pattern
237   for (int x = -5; x <= 5; x += 1)
238   {
239     for (int y = -5; y <= 5; y += 1)
240     {
241       Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
242       DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
243     }
244   }
245 
246   int oldSeed = GetRandomValue(0, 0xfffffff);
247   SetRandomSeed(level->seed);
248   // increase probability for trees via duplicated entries
249   Model borderModels[64];
250   int maxRockCount = GetRandomValue(2, 6);
251   int maxTreeCount = GetRandomValue(10, 20);
252   int maxFirTreeCount = GetRandomValue(5, 10);
253   int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
254   int grassPatchCount = GetRandomValue(5, 30);
255 
256   int modelCount = 0;
257   for (int i = 0; i < maxRockCount && modelCount < 63; i++)
258   {
259     borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
260   }
261   for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
262   {
263     borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
264   }
265   for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
266   {
267     borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
268   }
269   for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
270   {
271     borderModels[modelCount++] = grassPatchModel[0];
272   }
273 
274   // draw some objects around the border of the map
275   Vector3 up = {0, 1, 0};
276   // a pseudo random number generator to get the same result every time
277   const float wiggle = 0.75f;
278   const int layerCount = 3;
279   for (int layer = 0; layer < layerCount; layer++)
280   {
281     int layerPos = 6 + layer;
282     for (int x = -6 + layer; x <= 6 + layer; x += 1)
283     {
284       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
285         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 
286         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
287       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
288         (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 
289         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
290     }
291 
292     for (int z = -5 + layer; z <= 5 + layer; z += 1)
293     {
294       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
295         (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
296         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
297       DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)], 
298         (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 
299         up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
300     }
301   }
302 
303   SetRandomSeed(oldSeed);
304 }
305 
306 void DrawEnemyPath(Level *level, Color arrowColor)
307 {
308   const int castleX = 0, castleY = 0;
309   const int maxWaypointCount = 200;
310   const float timeStep = 1.0f;
311   Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
312 
313   // we start with a time offset to simulate the path, 
314   // this way the arrows are animated in a forward moving direction
315   // The time is wrapped around the time step to get a smooth animation
316   float timeOffset = fmodf(GetTime(), timeStep);
317 
318   for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
319   {
320     EnemyWave *wave = &level->waves[i];
321     if (wave->wave != level->currentWave)
322     {
323       continue;
324     }
325 
326     // use this dummy enemy to simulate the path
327     Enemy dummy = {
328       .enemyType = ENEMY_TYPE_MINION,
329       .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
330       .nextX = wave->spawnPosition.x,
331       .nextY = wave->spawnPosition.y,
332       .currentX = wave->spawnPosition.x,
333       .currentY = wave->spawnPosition.y,
334     };
335 
336     float deltaTime = timeOffset;
337     for (int j = 0; j < maxWaypointCount; j++)
338     {
339       int waypointPassedCount = 0;
340       Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
341       // after the initial variable starting offset, we use a fixed time step
342       deltaTime = timeStep;
343       dummy.simPosition = pos;
344 
345       // Update the dummy's position just like we do in the regular enemy update loop
346       for (int k = 0; k < waypointPassedCount; k++)
347       {
348         dummy.currentX = dummy.nextX;
349         dummy.currentY = dummy.nextY;
350         if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
351           Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
352         {
353           break;
354         }
355       }
356       if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
357       {
358         break;
359       }
360       
361       // get the angle we need to rotate the arrow model. The velocity is just fine for this.
362       float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
363       DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
364     }
365   }
366 }
367 
368 void DrawEnemyPaths(Level *level)
369 {
370   // disable depth testing for the path arrows
371   // flush the 3D batch to draw the arrows on top of everything
372   rlDrawRenderBatchActive();
373   rlDisableDepthTest();
374   DrawEnemyPath(level, (Color){64, 64, 64, 160});
375 
376   rlDrawRenderBatchActive();
377   rlEnableDepthTest();
378   DrawEnemyPath(level, WHITE);
379 }
380 
381 void DrawLevelBuildingPlacementState(Level *level)
382 {
383   BeginMode3D(level->camera);
384   DrawLevelGround(level);
385 
386   int blockedCellCount = 0;
387   Vector2 blockedCells[1];
388   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
389   float planeDistance = ray.position.y / -ray.direction.y;
390   float planeX = ray.direction.x * planeDistance + ray.position.x;
391   float planeY = ray.direction.z * planeDistance + ray.position.z;
392   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
393   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
394   if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
395   {
396     level->placementX = mapX;
397     level->placementY = mapY;
398   }
399   else
400   {
401     mapX = level->placementX;
402     mapY = level->placementY;
403   }
404   blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
405   PathFindingMapUpdate(blockedCellCount, blockedCells);
406 
407   TowerDraw();
408   EnemyDraw();
409   ProjectileDraw();
410   ParticleDraw();
411   DrawEnemyPaths(level);
412 
413   float dt = gameTime.fixedDeltaTime;
414   // smooth transition for the placement position using exponential decay
415   const float lambda = 15.0f;
416   float factor = 1.0f - expf(-lambda * dt);
417 
418   for (int i = 0; i < gameTime.fixedStepCount; i++)
419   {
420     level->placementTransitionPosition = 
421       Vector2Lerp(
422         level->placementTransitionPosition, 
423         (Vector2){mapX, mapY}, factor);
424 
425     // draw the spring position for debugging the spring simulation
426     // first step: stiff spring, no simulation
427     Vector3 worldPlacementPosition = (Vector3){
428       level->placementTransitionPosition.x, 0.0f, level->placementTransitionPosition.y};
429     Vector3 springTargetPosition = (Vector3){
430       worldPlacementPosition.x, 1.0f, worldPlacementPosition.z};
431     Vector3 springPointDelta = Vector3Subtract(springTargetPosition, level->placementTowerSpring.position);
432     Vector3 velocityChange = Vector3Scale(springPointDelta, dt * 200.0f);
433 // decay velocity of the upright forcing spring 434 // This force acts like a 2nd spring that pulls the tip upright into the air above the 435 // base position
436 level->placementTowerSpring.velocity = Vector3Scale(level->placementTowerSpring.velocity, 1.0f - expf(-120.0f * dt));
437 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, velocityChange); 438 439 // calculate length of the spring to calculate the force to apply to the placementTowerSpring position 440 // we use a simple spring model with a rest length of 1.0f 441 Vector3 springDelta = Vector3Subtract(worldPlacementPosition, level->placementTowerSpring.position); 442 float springLength = Vector3Length(springDelta); 443 float springForce = (springLength - 1.0f) * 1000.0f; 444 Vector3 springForceVector = Vector3Normalize(springDelta); 445 springForceVector = Vector3Scale(springForceVector, springForce); 446 level->placementTowerSpring.velocity = Vector3Add(level->placementTowerSpring.velocity, 447 Vector3Scale(springForceVector, dt)); 448
449 level->placementTowerSpring.position = Vector3Add(level->placementTowerSpring.position, 450 Vector3Scale(level->placementTowerSpring.velocity, dt)); 451 } 452 453 DrawCube(level->placementTowerSpring.position, 0.1f, 0.1f, 0.1f, RED); 454 DrawLine3D(level->placementTowerSpring.position, (Vector3){ 455 level->placementTransitionPosition.x, 0.0f, level->placementTransitionPosition.y}, YELLOW); 456 457 rlPushMatrix(); 458 rlTranslatef(level->placementTransitionPosition.x, 0, level->placementTransitionPosition.y); 459 DrawCubeWires((Vector3){0.0f, 0.0f, 0.0f}, 1.0f, 0.0f, 1.0f, RED); 460 461 462 // deactivated for now to debug the spring simulation 463 // Tower dummy = { 464 // .towerType = level->placementMode, 465 // }; 466 // TowerDrawSingle(dummy); 467 468 float bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f; 469 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f; 470 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f; 471 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f; 472 473 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE); 474 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE); 475 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE); 476 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE); 477 rlPopMatrix(); 478 479 guiState.isBlocked = 0; 480 481 EndMode3D(); 482 483 TowerDrawHealthBars(level->camera); 484 485 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0)) 486 { 487 level->nextState = LEVEL_STATE_BUILDING; 488 level->placementMode = TOWER_TYPE_NONE; 489 TraceLog(LOG_INFO, "Cancel building"); 490 } 491 492 if (Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0)) 493 { 494 level->nextState = LEVEL_STATE_BUILDING; 495 if (TowerTryAdd(level->placementMode, mapX, mapY)) 496 { 497 level->playerGold -= GetTowerCosts(level->placementMode); 498 level->placementMode = TOWER_TYPE_NONE; 499 } 500 } 501 } 502 503 void DrawLevelBuildingState(Level *level) 504 { 505 BeginMode3D(level->camera); 506 DrawLevelGround(level); 507 508 PathFindingMapUpdate(0, 0); 509 TowerDraw(); 510 EnemyDraw(); 511 ProjectileDraw(); 512 ParticleDraw(); 513 DrawEnemyPaths(level); 514 515 guiState.isBlocked = 0; 516 517 EndMode3D(); 518 519 TowerDrawHealthBars(level->camera); 520 521 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 522 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer"); 523 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista"); 524 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult"); 525 526 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 527 { 528 level->nextState = LEVEL_STATE_RESET; 529 } 530 531 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 532 { 533 level->nextState = LEVEL_STATE_BATTLE; 534 } 535 536 const char *text = "Building phase"; 537 int textWidth = MeasureText(text, 20); 538 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 539 } 540 541 void InitBattleStateConditions(Level *level) 542 { 543 level->state = LEVEL_STATE_BATTLE; 544 level->nextState = LEVEL_STATE_NONE; 545 level->waveEndTimer = 0.0f; 546 for (int i = 0; i < 10; i++) 547 { 548 EnemyWave *wave = &level->waves[i]; 549 wave->spawned = 0; 550 wave->timeToSpawnNext = wave->delay; 551 } 552 } 553 554 void DrawLevelBattleState(Level *level) 555 { 556 BeginMode3D(level->camera); 557 DrawLevelGround(level); 558 TowerDraw(); 559 EnemyDraw(); 560 ProjectileDraw(); 561 ParticleDraw(); 562 guiState.isBlocked = 0; 563 EndMode3D(); 564 565 EnemyDrawHealthbars(level->camera); 566 TowerDrawHealthBars(level->camera); 567 568 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 569 { 570 level->nextState = LEVEL_STATE_RESET; 571 } 572 573 int maxCount = 0; 574 int remainingCount = 0; 575 for (int i = 0; i < 10; i++) 576 { 577 EnemyWave *wave = &level->waves[i]; 578 if (wave->wave != level->currentWave) 579 { 580 continue; 581 } 582 maxCount += wave->count; 583 remainingCount += wave->count - wave->spawned; 584 } 585 int aliveCount = EnemyCount(); 586 remainingCount += aliveCount; 587 588 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 589 int textWidth = MeasureText(text, 20); 590 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 591 } 592 593 void DrawLevel(Level *level) 594 { 595 switch (level->state) 596 { 597 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 598 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break; 599 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 600 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 601 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 602 default: break; 603 } 604 605 DrawLevelHud(level); 606 } 607 608 void UpdateLevel(Level *level) 609 { 610 if (level->state == LEVEL_STATE_BATTLE) 611 { 612 int activeWaves = 0; 613 for (int i = 0; i < 10; i++) 614 { 615 EnemyWave *wave = &level->waves[i]; 616 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 617 { 618 continue; 619 } 620 activeWaves++; 621 wave->timeToSpawnNext -= gameTime.deltaTime; 622 if (wave->timeToSpawnNext <= 0.0f) 623 { 624 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 625 if (enemy) 626 { 627 wave->timeToSpawnNext = wave->interval; 628 wave->spawned++; 629 } 630 } 631 } 632 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 633 level->waveEndTimer += gameTime.deltaTime; 634 if (level->waveEndTimer >= 2.0f) 635 { 636 level->nextState = LEVEL_STATE_LOST_WAVE; 637 } 638 } 639 else if (activeWaves == 0 && EnemyCount() == 0) 640 { 641 level->waveEndTimer += gameTime.deltaTime; 642 if (level->waveEndTimer >= 2.0f) 643 { 644 level->nextState = LEVEL_STATE_WON_WAVE; 645 } 646 } 647 } 648 649 PathFindingMapUpdate(0, 0); 650 EnemyUpdate(); 651 TowerUpdate(); 652 ProjectileUpdate(); 653 ParticleUpdate(); 654 655 if (level->nextState == LEVEL_STATE_RESET) 656 { 657 InitLevel(level); 658 } 659 660 if (level->nextState == LEVEL_STATE_BATTLE) 661 { 662 InitBattleStateConditions(level); 663 } 664 665 if (level->nextState == LEVEL_STATE_WON_WAVE) 666 { 667 level->currentWave++; 668 level->state = LEVEL_STATE_WON_WAVE; 669 } 670 671 if (level->nextState == LEVEL_STATE_LOST_WAVE) 672 { 673 level->state = LEVEL_STATE_LOST_WAVE; 674 } 675 676 if (level->nextState == LEVEL_STATE_BUILDING) 677 { 678 level->state = LEVEL_STATE_BUILDING; 679 } 680 681 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT) 682 { 683 level->state = LEVEL_STATE_BUILDING_PLACEMENT; 684 level->placementTransitionPosition = (Vector2){ 685 level->placementX, level->placementY}; 686 // initialize the spring to the current position 687 level->placementTowerSpring = (PhysicsPoint){ 688 .position = (Vector3){level->placementX, 1.0f, level->placementY}, 689 .velocity = (Vector3){0.0f, 0.0f, 0.0f}, 690 }; 691 } 692 693 if (level->nextState == LEVEL_STATE_WON_LEVEL) 694 { 695 // make something of this later 696 InitLevel(level); 697 } 698 699 level->nextState = LEVEL_STATE_NONE; 700 } 701 702 float nextSpawnTime = 0.0f; 703 704 void ResetGame() 705 { 706 InitLevel(currentLevel); 707 } 708 709 void InitGame() 710 { 711 TowerInit(); 712 EnemyInit(); 713 ProjectileInit(); 714 ParticleInit(); 715 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 716 717 currentLevel = levels; 718 InitLevel(currentLevel); 719 } 720 721 //# Immediate GUI functions 722 723 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 724 { 725 const float healthBarHeight = 6.0f; 726 const float healthBarOffset = 15.0f; 727 const float inset = 2.0f; 728 const float innerWidth = healthBarWidth - inset * 2; 729 const float innerHeight = healthBarHeight - inset * 2; 730 731 Vector2 screenPos = GetWorldToScreen(position, camera); 732 float centerX = screenPos.x - healthBarWidth * 0.5f; 733 float topY = screenPos.y - healthBarOffset; 734 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 735 float healthWidth = innerWidth * healthRatio; 736 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 737 } 738 739 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 740 { 741 Rectangle bounds = {x, y, width, height}; 742 int isPressed = 0; 743 int isSelected = state && state->isSelected; 744 int isDisabled = state && state->isDisabled; 745 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 746 { 747 Color color = isSelected ? DARKGRAY : GRAY; 748 DrawRectangle(x, y, width, height, color); 749 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 750 { 751 isPressed = 1; 752 } 753 guiState.isBlocked = 1; 754 } 755 else 756 { 757 Color color = isSelected ? WHITE : LIGHTGRAY; 758 DrawRectangle(x, y, width, height, color); 759 } 760 Font font = GetFontDefault(); 761 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 762 Color textColor = isDisabled ? GRAY : BLACK; 763 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 764 return isPressed; 765 } 766 767 //# Main game loop 768 769 void GameUpdate() 770 { 771 UpdateLevel(currentLevel); 772 } 773 774 int main(void) 775 { 776 int screenWidth, screenHeight; 777 GetPreferredSize(&screenWidth, &screenHeight); 778 InitWindow(screenWidth, screenHeight, "Tower defense"); 779 int fps = 30; 780 SetTargetFPS(fps); 781 782 LoadAssets(); 783 InitGame(); 784 785 while (!WindowShouldClose()) 786 { 787 if (IsPaused()) { 788 // canvas is not visible in browser - do nothing 789 continue; 790 } 791 792 if (IsKeyPressed(KEY_F)) 793 { 794 fps += 5; 795 if (fps > 60) fps = 10; 796 SetTargetFPS(fps); 797 TraceLog(LOG_INFO, "FPS set to %d", fps); 798 } 799 800 float dt = GetFrameTime(); 801 // cap maximum delta time to 0.1 seconds to prevent large time steps 802 if (dt > 0.1f) dt = 0.1f; 803 gameTime.time += dt; 804 gameTime.deltaTime = dt; 805 806 float fixedTimeDiff = gameTime.time - gameTime.fixedTimeStart; 807 gameTime.fixedStepCount = (uint8_t)(fixedTimeDiff / gameTime.fixedDeltaTime); 808 809 BeginDrawing(); 810 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 811 812 GameUpdate(); 813 DrawLevel(currentLevel); 814 815 DrawText(TextFormat("FPS: %.1f : %d", 1.0f / (float) GetFrameTime(), fps), GetScreenWidth() - 180, 60, 20, WHITE); 816 EndDrawing(); 817 818 gameTime.fixedTimeStart += gameTime.fixedStepCount * gameTime.fixedDeltaTime; 819 } 820 821 CloseWindow(); 822 823 return 0; 824 }
  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 typedef struct PhysicsPoint
 12 {
 13   Vector3 position;
 14   Vector3 velocity;
 15 } PhysicsPoint;
 16 
 17 #define ENEMY_MAX_PATH_COUNT 8
 18 #define ENEMY_MAX_COUNT 400
 19 #define ENEMY_TYPE_NONE 0
 20 #define ENEMY_TYPE_MINION 1
 21 
 22 #define PARTICLE_MAX_COUNT 400
 23 #define PARTICLE_TYPE_NONE 0
 24 #define PARTICLE_TYPE_EXPLOSION 1
 25 
 26 typedef struct Particle
 27 {
 28   uint8_t particleType;
 29   float spawnTime;
 30   float lifetime;
 31   Vector3 position;
 32   Vector3 velocity;
 33   Vector3 scale;
 34 } Particle;
 35 
 36 #define TOWER_MAX_COUNT 400
 37 enum TowerType
 38 {
 39   TOWER_TYPE_NONE,
 40   TOWER_TYPE_BASE,
 41   TOWER_TYPE_ARCHER,
 42   TOWER_TYPE_BALLISTA,
 43   TOWER_TYPE_CATAPULT,
 44   TOWER_TYPE_WALL,
 45   TOWER_TYPE_COUNT
 46 };
 47 
 48 typedef struct HitEffectConfig
 49 {
 50   float damage;
 51   float areaDamageRadius;
 52   float pushbackPowerDistance;
 53 } HitEffectConfig;
 54 
 55 typedef struct TowerTypeConfig
 56 {
 57   float cooldown;
 58   float range;
 59   float projectileSpeed;
 60   
 61   uint8_t cost;
 62   uint8_t projectileType;
 63   uint16_t maxHealth;
 64 
 65   HitEffectConfig hitEffect;
 66 } TowerTypeConfig;
 67 
 68 typedef struct Tower
 69 {
 70   int16_t x, y;
 71   uint8_t towerType;
 72   Vector2 lastTargetPosition;
 73   float cooldown;
 74   float damage;
 75 } Tower;
 76 
 77 typedef struct GameTime
 78 {
 79   float time;
 80   float deltaTime;
 81   uint32_t frameCount;
 82 
 83   float fixedDeltaTime;
 84   // leaving the fixed time stepping to the update functions,
 85   // we need to know the fixed time at the start of the frame
 86   float fixedTimeStart;
 87   // and the number of fixed steps that we have to make this frame
 88   // The fixedTime is fixedTimeStart + n * fixedStepCount
 89   uint8_t fixedStepCount;
 90 } GameTime;
 91 
 92 typedef struct ButtonState {
 93   char isSelected;
 94   char isDisabled;
 95 } ButtonState;
 96 
 97 typedef struct GUIState {
 98   int isBlocked;
 99 } GUIState;
100 
101 typedef enum LevelState
102 {
103   LEVEL_STATE_NONE,
104   LEVEL_STATE_BUILDING,
105   LEVEL_STATE_BUILDING_PLACEMENT,
106   LEVEL_STATE_BATTLE,
107   LEVEL_STATE_WON_WAVE,
108   LEVEL_STATE_LOST_WAVE,
109   LEVEL_STATE_WON_LEVEL,
110   LEVEL_STATE_RESET,
111 } LevelState;
112 
113 typedef struct EnemyWave {
114   uint8_t enemyType;
115   uint8_t wave;
116   uint16_t count;
117   float interval;
118   float delay;
119   Vector2 spawnPosition;
120 
121   uint16_t spawned;
122   float timeToSpawnNext;
123 } EnemyWave;
124 
125 #define ENEMY_MAX_WAVE_COUNT 10
126 
127 typedef struct Level
128 {
129   int seed;
130   LevelState state;
131   LevelState nextState;
132   Camera3D camera;
133   int placementMode;
134   int16_t placementX;
135   int16_t placementY;
136   Vector2 placementTransitionPosition;
137   PhysicsPoint placementTowerSpring;
138 
139   int initialGold;
140   int playerGold;
141 
142   EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
143   int currentWave;
144   float waveEndTimer;
145 } Level;
146 
147 typedef struct DeltaSrc
148 {
149   char x, y;
150 } DeltaSrc;
151 
152 typedef struct PathfindingMap
153 {
154   int width, height;
155   float scale;
156   float *distances;
157   long *towerIndex; 
158   DeltaSrc *deltaSrc;
159   float maxDistance;
160   Matrix toMapSpace;
161   Matrix toWorldSpace;
162 } PathfindingMap;
163 
164 // when we execute the pathfinding algorithm, we need to store the active nodes
165 // in a queue. Each node has a position, a distance from the start, and the
166 // position of the node that we came from.
167 typedef struct PathfindingNode
168 {
169   int16_t x, y, fromX, fromY;
170   float distance;
171 } PathfindingNode;
172 
173 typedef struct EnemyId
174 {
175   uint16_t index;
176   uint16_t generation;
177 } EnemyId;
178 
179 typedef struct EnemyClassConfig
180 {
181   float speed;
182   float health;
183   float radius;
184   float maxAcceleration;
185   float requiredContactTime;
186   float explosionDamage;
187   float explosionRange;
188   float explosionPushbackPower;
189   int goldValue;
190 } EnemyClassConfig;
191 
192 typedef struct Enemy
193 {
194   int16_t currentX, currentY;
195   int16_t nextX, nextY;
196   Vector2 simPosition;
197   Vector2 simVelocity;
198   uint16_t generation;
199   float walkedDistance;
200   float startMovingTime;
201   float damage, futureDamage;
202   float contactTime;
203   uint8_t enemyType;
204   uint8_t movePathCount;
205   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
206 } Enemy;
207 
208 // a unit that uses sprites to be drawn
209 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
210 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
211 typedef struct SpriteUnit
212 {
213   Rectangle srcRect;
214   Vector2 offset;
215   int frameCount;
216   float frameDuration;
217   Rectangle srcWeaponIdleRect;
218   Vector2 srcWeaponIdleOffset;
219   Rectangle srcWeaponCooldownRect;
220   Vector2 srcWeaponCooldownOffset;
221 } SpriteUnit;
222 
223 #define PROJECTILE_MAX_COUNT 1200
224 #define PROJECTILE_TYPE_NONE 0
225 #define PROJECTILE_TYPE_ARROW 1
226 #define PROJECTILE_TYPE_CATAPULT 2
227 #define PROJECTILE_TYPE_BALLISTA 3
228 
229 typedef struct Projectile
230 {
231   uint8_t projectileType;
232   float shootTime;
233   float arrivalTime;
234   float distance;
235   Vector3 position;
236   Vector3 target;
237   Vector3 directionNormal;
238   EnemyId targetEnemy;
239   HitEffectConfig hitEffectConfig;
240 } Projectile;
241 
242 //# Function declarations
243 float TowerGetMaxHealth(Tower *tower);
244 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
245 int EnemyAddDamageRange(Vector2 position, float range, float damage);
246 int EnemyAddDamage(Enemy *enemy, float damage);
247 
248 //# Enemy functions
249 void EnemyInit();
250 void EnemyDraw();
251 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
252 void EnemyUpdate();
253 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
254 float EnemyGetMaxHealth(Enemy *enemy);
255 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
256 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
257 EnemyId EnemyGetId(Enemy *enemy);
258 Enemy *EnemyTryResolve(EnemyId enemyId);
259 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
260 int EnemyAddDamage(Enemy *enemy, float damage);
261 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
262 int EnemyCount();
263 void EnemyDrawHealthbars(Camera3D camera);
264 
265 //# Tower functions
266 void TowerInit();
267 Tower *TowerGetAt(int16_t x, int16_t y);
268 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
269 Tower *GetTowerByType(uint8_t towerType);
270 int GetTowerCosts(uint8_t towerType);
271 float TowerGetMaxHealth(Tower *tower);
272 void TowerDraw();
273 void TowerDrawSingle(Tower tower);
274 void TowerUpdate();
275 void TowerDrawHealthBars(Camera3D camera);
276 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
277 
278 //# Particles
279 void ParticleInit();
280 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
281 void ParticleUpdate();
282 void ParticleDraw();
283 
284 //# Projectiles
285 void ProjectileInit();
286 void ProjectileDraw();
287 void ProjectileUpdate();
288 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
289 
290 //# Pathfinding map
291 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
292 float PathFindingGetDistance(int mapX, int mapY);
293 Vector2 PathFindingGetGradient(Vector3 world);
294 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
295 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
296 void PathFindingMapDraw();
297 
298 //# UI
299 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
300 
301 //# Level
302 void DrawLevelGround(Level *level);
303 void DrawEnemyPath(Level *level, Color arrowColor);
304 
305 //# variables
306 extern Level *currentLevel;
307 extern Enemy enemies[ENEMY_MAX_COUNT];
308 extern int enemyCount;
309 extern EnemyClassConfig enemyClassConfigs[];
310 
311 extern GUIState guiState;
312 extern GameTime gameTime;
313 extern Tower towers[TOWER_MAX_COUNT];
314 extern int towerCount;
315 
316 extern Texture2D palette, spriteSheet;
317 
318 #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
The spring now has a bouncy behavior.

The spring now has a bouncy behavior and it looks pretty responsive:

The spring tip is now bouncing around.
The tip is now bouncing playfully around when the tower is moved.

Conclusion

Using a few lines of code and math formulas inspired by physics, we have a wobbling abstraction of a spring that we will use in the next part to orient and scale the tower model. This is also going to involve some more math, but it should be a little more simple since it is going to be a quite direct application of formulas that are taught in high school geometry.

I hope you liked what you saw in this post, so you will join me next Friday for the next part, since I am sure you will like it to toy around with the final version of this gimmick!

🍪