Simple tower defense tutorial, part 9: Decorations

Let's quickly look at what we had last time:

What the game looked like at the end of the last part

Having graphics for the towers and enemies makes the ground look quite bad. It is time to change that!

Small note: The ZIP files of the following tutorial steps contain a makefile to build the code on desktop. It expects raylib to be installed in a sibling directory to the tutorial code. The makefile is based on the raylib game template project. Hopefully that helps you to compile the project locally. I am still working on improving this, but this is quite a bit of work and I haven't managed to finish it yet. But at least this version should work for you - and isn't crashing (yes, the previous tutorial steps had a NULL pointer error that didn't cause problems in the browser, which is why I didn't notice it).

If you have make installed, you can also provide the installation directory of raylib this way:

make RAYLIB_SRC_PATH=/path/to/raylib

Making the game look beautiful isn't really necessary to do that now, but it's a fairly easy step and one I enjoy a lot. There are lots of opinions when to work out the graphic style. Using placeholders until the gameplay is solid is the typical approach and one that makes a lot of sense in a professional environment while artists focus on finding the right style. Just know: There are work places where it is not accepted to work on graphics isn't allowed until the gameplay is solid.

For me, it's quite difficult to continue working with primitive placeholder art as it tends to distract me, so I usually spend more time on the graphics than I should (probably the reason why I tend to not finish games). But again, this isn't work for me. And one thing I learned is, that it's better to have fun with your hobby than making it a chore. That's the reason I am doing the graphics at this point - not because it needs to be done at this point but because it is something I enjoy!

Another point is: What I intend to do here isn't very complicated to do. Once you understand the concept, you should be able to apply this in different situations as well.

So the plan is:

Checkerboard ground tiles

This is easy: We use two different grass tiles and place them in a checkerboard pattern. We introduce a function to load assets more conveniently: The models I created use all the same texture, so the code should assign the texture to the model automatically when loading the model. This is done in the LoadGLBModel function in line 50. The LoadAssets function in line 62 loads all the models and textures. We will later add more models to this function. Since the game is kept small, we don't need to worry about unloading unused assets - which is a rather complex topic.

The DrawLevelGround function in line 192 is responsible for drawing the level ground. Here's the code:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 //# Variables
  7 GUIState guiState = {0};
  8 GameTime gameTime = {0};
  9 
10 Model floorTileAModel = {0}; 11 Model floorTileBModel = {0}; 12 13 Texture2D palette, spriteSheet; 14
15 Level levels[] = { 16 [0] = { 17 .state = LEVEL_STATE_BUILDING, 18 .initialGold = 20, 19 .waves[0] = { 20 .enemyType = ENEMY_TYPE_MINION, 21 .wave = 0, 22 .count = 10, 23 .interval = 2.5f, 24 .delay = 1.0f, 25 .spawnPosition = {0, 6}, 26 }, 27 .waves[1] = { 28 .enemyType = ENEMY_TYPE_MINION, 29 .wave = 1, 30 .count = 20, 31 .interval = 1.5f, 32 .delay = 1.0f, 33 .spawnPosition = {0, 6}, 34 }, 35 .waves[2] = { 36 .enemyType = ENEMY_TYPE_MINION, 37 .wave = 2, 38 .count = 30, 39 .interval = 1.2f, 40 .delay = 1.0f, 41 .spawnPosition = {0, 6}, 42 } 43 }, 44 }; 45 46 Level *currentLevel = levels; 47
48 //# Game 49 50 static Model LoadGLBModel(char *filename) 51 { 52 Model model = LoadModel(TextFormat("data/%s.glb",filename)); 53 if (model.materialCount > 1) 54 { 55 model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 56 } 57 return model; 58 } 59 60 void LoadAssets() 61 { 62 // load a sprite sheet that contains all units 63 spriteSheet = LoadTexture("data/spritesheet.png"); 64 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR); 65 66 // we'll use a palette texture to colorize the all buildings and environment art 67 palette = LoadTexture("data/palette.png"); 68 // The texture uses gradients on very small space, so we'll enable bilinear filtering 69 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 70 71 floorTileAModel = LoadGLBModel("floor-tile-a"); 72 floorTileBModel = LoadGLBModel("floor-tile-b"); 73 }
74 75 void InitLevel(Level *level) 76 { 77 TowerInit(); 78 EnemyInit(); 79 ProjectileInit(); 80 ParticleInit(); 81 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 82 83 level->placementMode = 0; 84 level->state = LEVEL_STATE_BUILDING; 85 level->nextState = LEVEL_STATE_NONE; 86 level->playerGold = level->initialGold; 87 level->currentWave = 0; 88 89 Camera *camera = &level->camera; 90 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 91 camera->target = (Vector3){0.0f, 0.0f, 0.0f}; 92 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 93 camera->fovy = 10.0f; 94 camera->projection = CAMERA_ORTHOGRAPHIC; 95 } 96 97 void DrawLevelHud(Level *level) 98 { 99 const char *text = TextFormat("Gold: %d", level->playerGold); 100 Font font = GetFontDefault(); 101 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 102 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 103 } 104 105 void DrawLevelReportLostWave(Level *level) 106 { 107 BeginMode3D(level->camera);
108 DrawLevelGround(level);
109 TowerDraw(); 110 EnemyDraw(); 111 ProjectileDraw(); 112 ParticleDraw(); 113 guiState.isBlocked = 0; 114 EndMode3D(); 115 116 TowerDrawHealthBars(level->camera); 117 118 const char *text = "Wave lost"; 119 int textWidth = MeasureText(text, 20); 120 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 121 122 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 123 { 124 level->nextState = LEVEL_STATE_RESET; 125 } 126 } 127 128 int HasLevelNextWave(Level *level) 129 { 130 for (int i = 0; i < 10; i++) 131 { 132 EnemyWave *wave = &level->waves[i]; 133 if (wave->wave == level->currentWave) 134 { 135 return 1; 136 } 137 } 138 return 0; 139 } 140 141 void DrawLevelReportWonWave(Level *level) 142 { 143 BeginMode3D(level->camera);
144 DrawLevelGround(level);
145 TowerDraw(); 146 EnemyDraw(); 147 ProjectileDraw(); 148 ParticleDraw(); 149 guiState.isBlocked = 0; 150 EndMode3D(); 151 152 TowerDrawHealthBars(level->camera); 153 154 const char *text = "Wave won"; 155 int textWidth = MeasureText(text, 20); 156 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 157 158 159 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 160 { 161 level->nextState = LEVEL_STATE_RESET; 162 } 163 164 if (HasLevelNextWave(level)) 165 { 166 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 167 { 168 level->nextState = LEVEL_STATE_BUILDING; 169 } 170 } 171 else { 172 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 173 { 174 level->nextState = LEVEL_STATE_WON_LEVEL; 175 } 176 } 177 } 178 179 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 180 { 181 static ButtonState buttonStates[8] = {0}; 182 int cost = GetTowerCosts(towerType); 183 const char *text = TextFormat("%s: %d", name, cost); 184 buttonStates[towerType].isSelected = level->placementMode == towerType; 185 buttonStates[towerType].isDisabled = level->playerGold < cost; 186 if (Button(text, x, y, width, height, &buttonStates[towerType])) 187 {
188 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 189 } 190 } 191 192 void DrawLevelGround(Level *level) 193 { 194 // draw checkerboard ground pattern 195 for (int x = -5; x <= 5; x += 1) 196 { 197 for (int y = -5; y <= 5; y += 1) 198 { 199 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 200 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 201 }
202 } 203 } 204 205 void DrawLevelBuildingState(Level *level) 206 { 207 BeginMode3D(level->camera);
208 DrawLevelGround(level);
209 TowerDraw(); 210 EnemyDraw(); 211 ProjectileDraw(); 212 ParticleDraw(); 213 214 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 215 float planeDistance = ray.position.y / -ray.direction.y; 216 float planeX = ray.direction.x * planeDistance + ray.position.x; 217 float planeY = ray.direction.z * planeDistance + ray.position.z; 218 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 219 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
220 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5)
221 { 222 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 223 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 224 { 225 if (TowerTryAdd(level->placementMode, mapX, mapY)) 226 { 227 level->playerGold -= GetTowerCosts(level->placementMode); 228 level->placementMode = TOWER_TYPE_NONE; 229 } 230 } 231 } 232 233 guiState.isBlocked = 0; 234 235 EndMode3D(); 236 237 TowerDrawHealthBars(level->camera); 238 239 static ButtonState buildWallButtonState = {0}; 240 static ButtonState buildGunButtonState = {0}; 241 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 242 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN; 243
244 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 245 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_GUN, "Archer");
246 247 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 248 { 249 level->nextState = LEVEL_STATE_RESET; 250 } 251 252 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 253 { 254 level->nextState = LEVEL_STATE_BATTLE; 255 } 256 257 const char *text = "Building phase"; 258 int textWidth = MeasureText(text, 20); 259 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 260 } 261 262 void InitBattleStateConditions(Level *level) 263 { 264 level->state = LEVEL_STATE_BATTLE; 265 level->nextState = LEVEL_STATE_NONE; 266 level->waveEndTimer = 0.0f; 267 for (int i = 0; i < 10; i++) 268 { 269 EnemyWave *wave = &level->waves[i]; 270 wave->spawned = 0; 271 wave->timeToSpawnNext = wave->delay; 272 } 273 } 274 275 void DrawLevelBattleState(Level *level) 276 { 277 BeginMode3D(level->camera);
278 DrawLevelGround(level);
279 TowerDraw(); 280 EnemyDraw(); 281 ProjectileDraw(); 282 ParticleDraw(); 283 guiState.isBlocked = 0; 284 EndMode3D(); 285 286 EnemyDrawHealthbars(level->camera); 287 TowerDrawHealthBars(level->camera); 288 289 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 290 { 291 level->nextState = LEVEL_STATE_RESET; 292 } 293 294 int maxCount = 0; 295 int remainingCount = 0; 296 for (int i = 0; i < 10; i++) 297 { 298 EnemyWave *wave = &level->waves[i]; 299 if (wave->wave != level->currentWave) 300 { 301 continue; 302 } 303 maxCount += wave->count; 304 remainingCount += wave->count - wave->spawned; 305 } 306 int aliveCount = EnemyCount(); 307 remainingCount += aliveCount; 308 309 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 310 int textWidth = MeasureText(text, 20); 311 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 312 } 313 314 void DrawLevel(Level *level) 315 { 316 switch (level->state) 317 { 318 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 319 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 320 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 321 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 322 default: break; 323 } 324 325 DrawLevelHud(level); 326 } 327 328 void UpdateLevel(Level *level) 329 { 330 if (level->state == LEVEL_STATE_BATTLE) 331 { 332 int activeWaves = 0; 333 for (int i = 0; i < 10; i++) 334 { 335 EnemyWave *wave = &level->waves[i]; 336 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 337 { 338 continue; 339 } 340 activeWaves++; 341 wave->timeToSpawnNext -= gameTime.deltaTime; 342 if (wave->timeToSpawnNext <= 0.0f) 343 { 344 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 345 if (enemy) 346 { 347 wave->timeToSpawnNext = wave->interval; 348 wave->spawned++; 349 } 350 } 351 } 352 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 353 level->waveEndTimer += gameTime.deltaTime; 354 if (level->waveEndTimer >= 2.0f) 355 { 356 level->nextState = LEVEL_STATE_LOST_WAVE; 357 } 358 } 359 else if (activeWaves == 0 && EnemyCount() == 0) 360 { 361 level->waveEndTimer += gameTime.deltaTime; 362 if (level->waveEndTimer >= 2.0f) 363 { 364 level->nextState = LEVEL_STATE_WON_WAVE; 365 } 366 } 367 } 368 369 PathFindingMapUpdate(); 370 EnemyUpdate(); 371 TowerUpdate(); 372 ProjectileUpdate(); 373 ParticleUpdate(); 374 375 if (level->nextState == LEVEL_STATE_RESET) 376 { 377 InitLevel(level); 378 } 379 380 if (level->nextState == LEVEL_STATE_BATTLE) 381 { 382 InitBattleStateConditions(level); 383 } 384 385 if (level->nextState == LEVEL_STATE_WON_WAVE) 386 { 387 level->currentWave++; 388 level->state = LEVEL_STATE_WON_WAVE; 389 } 390 391 if (level->nextState == LEVEL_STATE_LOST_WAVE) 392 { 393 level->state = LEVEL_STATE_LOST_WAVE; 394 } 395 396 if (level->nextState == LEVEL_STATE_BUILDING) 397 { 398 level->state = LEVEL_STATE_BUILDING; 399 } 400 401 if (level->nextState == LEVEL_STATE_WON_LEVEL) 402 { 403 // make something of this later 404 InitLevel(level); 405 } 406 407 level->nextState = LEVEL_STATE_NONE; 408 } 409 410 float nextSpawnTime = 0.0f; 411 412 void ResetGame() 413 { 414 InitLevel(currentLevel); 415 } 416 417 void InitGame() 418 { 419 TowerInit(); 420 EnemyInit(); 421 ProjectileInit(); 422 ParticleInit(); 423 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 424 425 currentLevel = levels; 426 InitLevel(currentLevel); 427 } 428 429 //# Immediate GUI functions 430 431 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 432 { 433 const float healthBarHeight = 6.0f; 434 const float healthBarOffset = 15.0f; 435 const float inset = 2.0f; 436 const float innerWidth = healthBarWidth - inset * 2; 437 const float innerHeight = healthBarHeight - inset * 2; 438 439 Vector2 screenPos = GetWorldToScreen(position, camera); 440 float centerX = screenPos.x - healthBarWidth * 0.5f; 441 float topY = screenPos.y - healthBarOffset; 442 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 443 float healthWidth = innerWidth * healthRatio; 444 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 445 } 446 447 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 448 { 449 Rectangle bounds = {x, y, width, height}; 450 int isPressed = 0; 451 int isSelected = state && state->isSelected; 452 int isDisabled = state && state->isDisabled; 453 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 454 { 455 Color color = isSelected ? DARKGRAY : GRAY; 456 DrawRectangle(x, y, width, height, color); 457 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 458 { 459 isPressed = 1; 460 } 461 guiState.isBlocked = 1; 462 } 463 else 464 { 465 Color color = isSelected ? WHITE : LIGHTGRAY; 466 DrawRectangle(x, y, width, height, color); 467 } 468 Font font = GetFontDefault(); 469 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 470 Color textColor = isDisabled ? GRAY : BLACK; 471 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 472 return isPressed; 473 } 474 475 //# Main game loop 476 477 void GameUpdate() 478 { 479 float dt = GetFrameTime(); 480 // cap maximum delta time to 0.1 seconds to prevent large time steps 481 if (dt > 0.1f) dt = 0.1f; 482 gameTime.time += dt; 483 gameTime.deltaTime = dt; 484 485 UpdateLevel(currentLevel); 486 } 487 488 int main(void) 489 { 490 int screenWidth, screenHeight; 491 GetPreferredSize(&screenWidth, &screenHeight); 492 InitWindow(screenWidth, screenHeight, "Tower defense"); 493 SetTargetFPS(30);
494 495 LoadAssets();
496 InitGame(); 497 498 while (!WindowShouldClose()) 499 { 500 if (IsPaused()) { 501 // canvas is not visible in browser - do nothing 502 continue; 503 } 504 505 BeginDrawing();
506 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
507 508 GameUpdate(); 509 DrawLevel(currentLevel); 510 511 EndDrawing(); 512 } 513 514 CloseWindow(); 515 516 return 0; 517 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
 30 #define TOWER_TYPE_NONE 0
 31 #define TOWER_TYPE_BASE 1
 32 #define TOWER_TYPE_GUN 2
 33 #define TOWER_TYPE_WALL 3
 34 #define TOWER_TYPE_COUNT 4
 35 
 36 typedef struct Tower
 37 {
 38   int16_t x, y;
 39   uint8_t towerType;
 40   Vector2 lastTargetPosition;
 41   float cooldown;
 42   float damage;
 43 } Tower;
 44 
 45 typedef struct GameTime
 46 {
 47   float time;
 48   float deltaTime;
 49 } GameTime;
 50 
 51 typedef struct ButtonState {
 52   char isSelected;
 53   char isDisabled;
 54 } ButtonState;
 55 
 56 typedef struct GUIState {
 57   int isBlocked;
 58 } GUIState;
 59 
 60 typedef enum LevelState
 61 {
 62   LEVEL_STATE_NONE,
 63   LEVEL_STATE_BUILDING,
 64   LEVEL_STATE_BATTLE,
 65   LEVEL_STATE_WON_WAVE,
 66   LEVEL_STATE_LOST_WAVE,
 67   LEVEL_STATE_WON_LEVEL,
 68   LEVEL_STATE_RESET,
 69 } LevelState;
 70 
 71 typedef struct EnemyWave {
 72   uint8_t enemyType;
 73   uint8_t wave;
 74   uint16_t count;
 75   float interval;
 76   float delay;
 77   Vector2 spawnPosition;
 78 
 79   uint16_t spawned;
 80   float timeToSpawnNext;
 81 } EnemyWave;
 82 
 83 typedef struct Level
 84 {
 85   LevelState state;
 86   LevelState nextState;
 87   Camera3D camera;
 88   int placementMode;
 89 
 90   int initialGold;
 91   int playerGold;
 92 
 93   EnemyWave waves[10];
 94   int currentWave;
 95   float waveEndTimer;
 96 } Level;
 97 
 98 typedef struct DeltaSrc
 99 {
100   char x, y;
101 } DeltaSrc;
102 
103 typedef struct PathfindingMap
104 {
105   int width, height;
106   float scale;
107   float *distances;
108   long *towerIndex; 
109   DeltaSrc *deltaSrc;
110   float maxDistance;
111   Matrix toMapSpace;
112   Matrix toWorldSpace;
113 } PathfindingMap;
114 
115 // when we execute the pathfinding algorithm, we need to store the active nodes
116 // in a queue. Each node has a position, a distance from the start, and the
117 // position of the node that we came from.
118 typedef struct PathfindingNode
119 {
120   int16_t x, y, fromX, fromY;
121   float distance;
122 } PathfindingNode;
123 
124 typedef struct EnemyId
125 {
126   uint16_t index;
127   uint16_t generation;
128 } EnemyId;
129 
130 typedef struct EnemyClassConfig
131 {
132   float speed;
133   float health;
134   float radius;
135   float maxAcceleration;
136   float requiredContactTime;
137   float explosionDamage;
138   float explosionRange;
139   float explosionPushbackPower;
140   int goldValue;
141 } EnemyClassConfig;
142 
143 typedef struct Enemy
144 {
145   int16_t currentX, currentY;
146   int16_t nextX, nextY;
147   Vector2 simPosition;
148   Vector2 simVelocity;
149   uint16_t generation;
150   float walkedDistance;
151   float startMovingTime;
152   float damage, futureDamage;
153   float contactTime;
154   uint8_t enemyType;
155   uint8_t movePathCount;
156   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
157 } Enemy;
158 
159 // a unit that uses sprites to be drawn
160 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
161 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
162 typedef struct SpriteUnit
163 {
164   Rectangle srcRect;
165   Vector2 offset;
166   int frameCount;
167   float frameDuration;
168   Rectangle srcWeaponIdleRect;
169   Vector2 srcWeaponIdleOffset;
170   Rectangle srcWeaponCooldownRect;
171   Vector2 srcWeaponCooldownOffset;
172 } SpriteUnit;
173 
174 #define PROJECTILE_MAX_COUNT 1200
175 #define PROJECTILE_TYPE_NONE 0
176 #define PROJECTILE_TYPE_ARROW 1
177 
178 typedef struct Projectile
179 {
180   uint8_t projectileType;
181   float shootTime;
182   float arrivalTime;
183   float distance;
184   float damage;
185   Vector3 position;
186   Vector3 target;
187   Vector3 directionNormal;
188   EnemyId targetEnemy;
189 } Projectile;
190 
191 //# Function declarations
192 float TowerGetMaxHealth(Tower *tower);
193 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
194 int EnemyAddDamage(Enemy *enemy, float damage);
195 
196 //# Enemy functions
197 void EnemyInit();
198 void EnemyDraw();
199 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
200 void EnemyUpdate();
201 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
202 float EnemyGetMaxHealth(Enemy *enemy);
203 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
204 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
205 EnemyId EnemyGetId(Enemy *enemy);
206 Enemy *EnemyTryResolve(EnemyId enemyId);
207 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
208 int EnemyAddDamage(Enemy *enemy, float damage);
209 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
210 int EnemyCount();
211 void EnemyDrawHealthbars(Camera3D camera);
212 
213 //# Tower functions
214 void TowerInit();
215 Tower *TowerGetAt(int16_t x, int16_t y);
216 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
217 Tower *GetTowerByType(uint8_t towerType);
218 int GetTowerCosts(uint8_t towerType);
219 float TowerGetMaxHealth(Tower *tower);
220 void TowerDraw();
221 void TowerUpdate();
222 void TowerDrawHealthBars(Camera3D camera);
223 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
224 
225 //# Particles
226 void ParticleInit();
227 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
228 void ParticleUpdate();
229 void ParticleDraw();
230 
231 //# Projectiles
232 void ProjectileInit();
233 void ProjectileDraw();
234 void ProjectileUpdate();
235 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage);
236 
237 //# Pathfinding map
238 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
239 float PathFindingGetDistance(int mapX, int mapY);
240 Vector2 PathFindingGetGradient(Vector3 world);
241 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
242 void PathFindingMapUpdate();
243 void PathFindingMapDraw();
244 
245 //# UI
246 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
247 
248 //# Level 249 void DrawLevelGround(Level *level); 250
251 //# variables 252 extern Level *currentLevel; 253 extern Enemy enemies[ENEMY_MAX_COUNT]; 254 extern int enemyCount; 255 extern EnemyClassConfig enemyClassConfigs[]; 256 257 extern GUIState guiState; 258 extern GameTime gameTime; 259 extern Tower towers[TOWER_MAX_COUNT];
260 extern int towerCount; 261 262 extern Texture2D palette, spriteSheet;
263 264 #endif
  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}, 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 EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522   }
523 }
  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()
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 < towerCount; i++)
131   {
132     Tower *tower = &towers[i];
133     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134     {
135       continue;
136     }
137     int16_t mapX, mapY;
138     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139     // this would not work correctly and needs to be refined to allow towers covering multiple cells
140     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141     // one cell. For now.
142     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143     {
144       continue;
145     }
146     int index = mapY * width + mapX;
147     pathfindingMap.towerIndex[index] = i;
148   }
149 
150   // we start at the castle and add the castle to the queue
151   pathfindingMap.maxDistance = 0.0f;
152   pathfindingNodeQueueCount = 0;
153   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154   PathfindingNode *node = 0;
155   while ((node = PathFindingNodePop()))
156   {
157     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158     {
159       continue;
160     }
161     int index = node->y * width + node->x;
162     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163     {
164       continue;
165     }
166 
167     int deltaX = node->x - node->fromX;
168     int deltaY = node->y - node->fromY;
169     // even if the cell is blocked by a tower, we still may want to store the direction
170     // (though this might not be needed, IDK right now)
171     pathfindingMap.deltaSrc[index].x = (char) deltaX;
172     pathfindingMap.deltaSrc[index].y = (char) deltaY;
173 
174     // we skip nodes that are blocked by towers
175     if (pathfindingMap.towerIndex[index] >= 0)
176     {
177       node->distance += 8.0f;
178     }
179     pathfindingMap.distances[index] = node->distance;
180     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185   }
186 }
187 
188 void PathFindingMapDraw()
189 {
190   float cellSize = pathfindingMap.scale * 0.9f;
191   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192   for (int x = 0; x < pathfindingMap.width; x++)
193   {
194     for (int y = 0; y < pathfindingMap.height; y++)
195     {
196       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200       // animate the distance "wave" to show how the pathfinding algorithm expands
201       // from the castle
202       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203       {
204         color = BLACK;
205       }
206       DrawCube(position, cellSize, 0.1f, cellSize, color);
207     }
208   }
209 }
210 
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213   int16_t mapX, mapY;
214   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215   {
216     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217     return (Vector2){(float)-delta.x, (float)-delta.y};
218   }
219   // fallback to a simple gradient calculation
220   float n = PathFindingGetDistance(mapX, mapY - 1);
221   float s = PathFindingGetDistance(mapX, mapY + 1);
222   float w = PathFindingGetDistance(mapX - 1, mapY);
223   float e = PathFindingGetDistance(mapX + 1, mapY);
224   return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 void ProjectileInit()
  8 {
  9   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 10   {
 11     projectiles[i] = (Projectile){0};
 12   }
 13 }
 14 
 15 void ProjectileDraw()
 16 {
 17   for (int i = 0; i < projectileCount; i++)
 18   {
 19     Projectile projectile = projectiles[i];
 20     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 21     {
 22       continue;
 23     }
 24     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 25     if (transition >= 1.0f)
 26     {
 27       continue;
 28     }
 29     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 30     {
 31       float t = transition + transitionOffset * 0.3f;
 32       if (t > 1.0f)
 33       {
 34         break;
 35       }
 36       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 37       Color color = RED;
 38       if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 39       {
 40         // make tip red but quickly fade to brown
 41         color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 42         // fake a ballista flight path using parabola equation
 43         float parabolaT = t - 0.5f;
 44         parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 45         position.y += 0.15f * parabolaT * projectile.distance;
 46       }
 47 
 48       float size = 0.06f * (transitionOffset + 0.25f);
 49       DrawCube(position, size, size, size, color);
 50     }
 51   }
 52 }
 53 
 54 void ProjectileUpdate()
 55 {
 56   for (int i = 0; i < projectileCount; i++)
 57   {
 58     Projectile *projectile = &projectiles[i];
 59     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 60     {
 61       continue;
 62     }
 63     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 64     if (transition >= 1.0f)
 65     {
 66       projectile->projectileType = PROJECTILE_TYPE_NONE;
 67       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 68       if (enemy)
 69       {
 70         EnemyAddDamage(enemy, projectile->damage);
 71       }
 72       continue;
 73     }
 74   }
 75 }
 76 
 77 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage)
 78 {
 79   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       projectile->projectileType = projectileType;
 85       projectile->shootTime = gameTime.time;
 86       float distance = Vector3Distance(position, target);
 87       projectile->arrivalTime = gameTime.time + distance / speed;
 88       projectile->damage = damage;
 89       projectile->position = position;
 90       projectile->target = target;
 91       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
 92       projectile->distance = distance;
 93       projectile->targetEnemy = EnemyGetId(enemy);
 94       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 95       return projectile;
 96     }
 97   }
 98   return 0;
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 Tower towers[TOWER_MAX_COUNT];
  5 int towerCount = 0;
  6 
  7 Model towerModels[TOWER_TYPE_COUNT];
8
9 // definition of our archer unit 10 SpriteUnit archerUnit = { 11 .srcRect = {0, 0, 16, 16}, 12 .offset = {7, 1}, 13 .frameCount = 1, 14 .frameDuration = 0.0f, 15 .srcWeaponIdleRect = {16, 0, 6, 16}, 16 .srcWeaponIdleOffset = {8, 0}, 17 .srcWeaponCooldownRect = {22, 0, 11, 16}, 18 .srcWeaponCooldownOffset = {10, 0}, 19 }; 20 21 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase) 22 { 23 float xScale = flip ? -1.0f : 1.0f; 24 Camera3D camera = currentLevel->camera; 25 float size = 0.5f; 26 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale }; 27 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size }; 28 // we want the sprite to face the camera, so we need to calculate the up vector 29 Vector3 forward = Vector3Subtract(camera.target, camera.position); 30 Vector3 up = {0, 1, 0}; 31 Vector3 right = Vector3CrossProduct(forward, up); 32 up = Vector3Normalize(Vector3CrossProduct(right, forward)); 33 34 Rectangle srcRect = unit.srcRect; 35 if (unit.frameCount > 1) 36 { 37 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width; 38 } 39 if (flip) 40 { 41 srcRect.x += srcRect.width; 42 srcRect.width = -srcRect.width; 43 } 44 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE); 45 46 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0) 47 { 48 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size }; 49 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size }; 50 srcRect = unit.srcWeaponCooldownRect; 51 if (flip) 52 { 53 // position.x = flip * scale.x * 0.5f; 54 srcRect.x += srcRect.width; 55 srcRect.width = -srcRect.width; 56 offset.x = scale.x - offset.x; 57 } 58 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE); 59 } 60 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0) 61 { 62 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size }; 63 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size }; 64 srcRect = unit.srcWeaponIdleRect; 65 if (flip) 66 { 67 // position.x = flip * scale.x * 0.5f; 68 srcRect.x += srcRect.width; 69 srcRect.width = -srcRect.width; 70 offset.x = scale.x - offset.x; 71 }
72 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE); 73 } 74 } 75 76 void TowerInit() 77 { 78 for (int i = 0; i < TOWER_MAX_COUNT; i++) 79 { 80 towers[i] = (Tower){0}; 81 }
82 towerCount = 0; 83 84 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb"); 85 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb"); 86 87 for (int i = 0; i < TOWER_TYPE_COUNT; i++) 88 { 89 if (towerModels[i].materials) 90 { 91 // assign the palette texture to the material of the model (0 is not used afaik) 92 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 93 } 94 } 95 } 96 97 static void TowerGunUpdate(Tower *tower) 98 { 99 if (tower->cooldown <= 0) 100 { 101 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 102 if (enemy) 103 { 104 tower->cooldown = 0.5f; 105 // shoot the enemy; determine future position of the enemy 106 float bulletSpeed = 4.0f; 107 float bulletDamage = 3.0f; 108 Vector2 velocity = enemy->simVelocity; 109 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 110 Vector2 towerPosition = {tower->x, tower->y}; 111 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 112 for (int i = 0; i < 8; i++) { 113 velocity = enemy->simVelocity; 114 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 115 float distance = Vector2Distance(towerPosition, futurePosition); 116 float eta2 = distance / bulletSpeed; 117 if (fabs(eta - eta2) < 0.01f) { 118 break; 119 } 120 eta = (eta2 + eta) * 0.5f; 121 } 122 ProjectileTryAdd(PROJECTILE_TYPE_ARROW, enemy, 123 (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 124 (Vector3){futurePosition.x, 0.25f, futurePosition.y}, 125 bulletSpeed, bulletDamage); 126 enemy->futureDamage += bulletDamage; 127 tower->lastTargetPosition = futurePosition; 128 } 129 } 130 else 131 { 132 tower->cooldown -= gameTime.deltaTime; 133 } 134 } 135 136 Tower *TowerGetAt(int16_t x, int16_t y) 137 { 138 for (int i = 0; i < towerCount; i++) 139 { 140 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE) 141 { 142 return &towers[i]; 143 } 144 } 145 return 0; 146 } 147 148 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 149 { 150 if (towerCount >= TOWER_MAX_COUNT) 151 { 152 return 0; 153 } 154 155 Tower *tower = TowerGetAt(x, y); 156 if (tower) 157 { 158 return 0; 159 } 160 161 tower = &towers[towerCount++]; 162 tower->x = x; 163 tower->y = y; 164 tower->towerType = towerType; 165 tower->cooldown = 0.0f; 166 tower->damage = 0.0f; 167 return tower; 168 } 169 170 Tower *GetTowerByType(uint8_t towerType) 171 { 172 for (int i = 0; i < towerCount; i++) 173 { 174 if (towers[i].towerType == towerType) 175 { 176 return &towers[i]; 177 } 178 } 179 return 0; 180 } 181 182 int GetTowerCosts(uint8_t towerType) 183 { 184 switch (towerType) 185 { 186 case TOWER_TYPE_BASE: 187 return 0; 188 case TOWER_TYPE_GUN: 189 return 6; 190 case TOWER_TYPE_WALL: 191 return 2; 192 } 193 return 0; 194 } 195 196 float TowerGetMaxHealth(Tower *tower) 197 { 198 switch (tower->towerType) 199 { 200 case TOWER_TYPE_BASE: 201 return 10.0f; 202 case TOWER_TYPE_GUN: 203 return 3.0f; 204 case TOWER_TYPE_WALL: 205 return 5.0f; 206 } 207 return 0.0f; 208 } 209 210 void TowerDraw() 211 { 212 for (int i = 0; i < towerCount; i++) 213 { 214 Tower tower = towers[i]; 215 if (tower.towerType == TOWER_TYPE_NONE) 216 { 217 continue; 218 } 219 220 switch (tower.towerType) 221 { 222 case TOWER_TYPE_GUN: 223 { 224 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera); 225 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera); 226 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 227 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 228 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE); 229 } 230 break; 231 default: 232 if (towerModels[tower.towerType].materials) 233 { 234 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 235 } else { 236 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 237 } 238 break; 239 } 240 } 241 } 242 243 void TowerUpdate() 244 { 245 for (int i = 0; i < towerCount; i++) 246 { 247 Tower *tower = &towers[i]; 248 switch (tower->towerType) 249 { 250 case TOWER_TYPE_GUN: 251 TowerGunUpdate(tower); 252 break; 253 } 254 } 255 } 256 257 void TowerDrawHealthBars(Camera3D camera) 258 { 259 for (int i = 0; i < towerCount; i++) 260 { 261 Tower *tower = &towers[i]; 262 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 263 { 264 continue; 265 } 266 267 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 268 float maxHealth = TowerGetMaxHealth(tower); 269 float health = maxHealth - tower->damage; 270 float healthRatio = health / maxHealth; 271 272 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f); 273 } 274 }
  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 game with the checkerboard pattern on the ground and some decorative objects. The towers can now only be placed on the checkerboard tiles.

The function is quite simple and the level size is hardcoded to be 11x11 (from -5 to 5 on the x and z axis). The function loops over all tiles and draws the tile depending on the sum of x and y. If the sum is even, it draws the first tile, otherwise the second tile:

  1 for (int x = -5; x <= 5; x += 1)
  2 {
  3   for (int y = -5; y <= 5; y += 1)
  4   {
  5     Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
  6     DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
  7   }
  8 }

The function to load the GLB models and assigning the palette texture simply assigns the texture to all materials in the model and returns the model:

  1 static Model LoadGLBModel(char *filename)
  2 {
  3   Model model = LoadModel(TextFormat("data/%s.glb",filename));
  4   if (model.materialCount > 1)
  5   {
  6     model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
  7   }
  8   return model;
  9 }

The LoadAssets function is now taking care of loading the texture and floor tiles. We could later also load the tower and enemy models here - or let the tower init function load the models through the LoadGLBModel function. It isn't a bad idea that the modules take care of their own assets, but for bigger projects where asset management becomes more complex, it's better to have dedicated asset management functions to avoid loading the same asset multiple times or unloading assets that are still in use. Thankfully, our game is unlikely to grow that big.

For now, let's focus on setting up the environment decorations, so let's ignore that step here:

  1 void LoadAssets()
  2 {
  3   // load a sprite sheet that contains all units
  4   spriteSheet = LoadTexture("data/spritesheet.png");
  5   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
  6 
  7   // we'll use a palette texture to colorize the all buildings and environment art
  8   palette = LoadTexture("data/palette.png");
  9   // The texture uses gradients on very small space, so we'll enable bilinear filtering
 10   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
 11 
 12   floorTileAModel = LoadGLBModel("floor-tile-a");
 13   floorTileBModel = LoadGLBModel("floor-tile-b");
 14 }

By clearing the background to a dark green color, it looks like the game map is placed on a grass field. Quite more appealing than the blue background we had!

But let's add some more decorations outside the map to make it look more natural, just for fun! We have the assets, so let's use them.

Decoration objects

The idea is to place now some decorative objects randomly around the map.

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 //# Variables
  7 GUIState guiState = {0};
  8 GameTime gameTime = {0};
  9 
 10 Model floorTileAModel = {0};
 11 Model floorTileBModel = {0};
12 Model grassPatchModel[1] = {0};
13 14 Texture2D palette, spriteSheet; 15 16 Level levels[] = { 17 [0] = { 18 .state = LEVEL_STATE_BUILDING, 19 .initialGold = 20, 20 .waves[0] = { 21 .enemyType = ENEMY_TYPE_MINION, 22 .wave = 0, 23 .count = 10, 24 .interval = 2.5f, 25 .delay = 1.0f, 26 .spawnPosition = {0, 6}, 27 }, 28 .waves[1] = { 29 .enemyType = ENEMY_TYPE_MINION, 30 .wave = 1, 31 .count = 20, 32 .interval = 1.5f, 33 .delay = 1.0f, 34 .spawnPosition = {0, 6}, 35 }, 36 .waves[2] = { 37 .enemyType = ENEMY_TYPE_MINION, 38 .wave = 2, 39 .count = 30, 40 .interval = 1.2f, 41 .delay = 1.0f, 42 .spawnPosition = {0, 6}, 43 } 44 }, 45 }; 46 47 Level *currentLevel = levels; 48 49 //# Game 50 51 static Model LoadGLBModel(char *filename) 52 { 53 Model model = LoadModel(TextFormat("data/%s.glb",filename)); 54 if (model.materialCount > 1) 55 { 56 model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 57 } 58 return model; 59 } 60 61 void LoadAssets() 62 { 63 // load a sprite sheet that contains all units 64 spriteSheet = LoadTexture("data/spritesheet.png"); 65 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR); 66 67 // we'll use a palette texture to colorize the all buildings and environment art 68 palette = LoadTexture("data/palette.png"); 69 // The texture uses gradients on very small space, so we'll enable bilinear filtering 70 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 71 72 floorTileAModel = LoadGLBModel("floor-tile-a");
73 floorTileBModel = LoadGLBModel("floor-tile-b"); 74 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
75 } 76 77 void InitLevel(Level *level) 78 { 79 TowerInit(); 80 EnemyInit(); 81 ProjectileInit(); 82 ParticleInit(); 83 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 84 85 level->placementMode = 0; 86 level->state = LEVEL_STATE_BUILDING; 87 level->nextState = LEVEL_STATE_NONE; 88 level->playerGold = level->initialGold; 89 level->currentWave = 0; 90 91 Camera *camera = &level->camera; 92 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 93 camera->target = (Vector3){0.0f, 0.0f, 0.0f}; 94 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 95 camera->fovy = 10.0f; 96 camera->projection = CAMERA_ORTHOGRAPHIC; 97 } 98 99 void DrawLevelHud(Level *level) 100 { 101 const char *text = TextFormat("Gold: %d", level->playerGold); 102 Font font = GetFontDefault(); 103 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 104 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 105 } 106 107 void DrawLevelReportLostWave(Level *level) 108 { 109 BeginMode3D(level->camera); 110 DrawLevelGround(level); 111 TowerDraw(); 112 EnemyDraw(); 113 ProjectileDraw(); 114 ParticleDraw(); 115 guiState.isBlocked = 0; 116 EndMode3D(); 117 118 TowerDrawHealthBars(level->camera); 119 120 const char *text = "Wave lost"; 121 int textWidth = MeasureText(text, 20); 122 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 123 124 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 125 { 126 level->nextState = LEVEL_STATE_RESET; 127 } 128 } 129 130 int HasLevelNextWave(Level *level) 131 { 132 for (int i = 0; i < 10; i++) 133 { 134 EnemyWave *wave = &level->waves[i]; 135 if (wave->wave == level->currentWave) 136 { 137 return 1; 138 } 139 } 140 return 0; 141 } 142 143 void DrawLevelReportWonWave(Level *level) 144 { 145 BeginMode3D(level->camera); 146 DrawLevelGround(level); 147 TowerDraw(); 148 EnemyDraw(); 149 ProjectileDraw(); 150 ParticleDraw(); 151 guiState.isBlocked = 0; 152 EndMode3D(); 153 154 TowerDrawHealthBars(level->camera); 155 156 const char *text = "Wave won"; 157 int textWidth = MeasureText(text, 20); 158 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 159 160 161 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 162 { 163 level->nextState = LEVEL_STATE_RESET; 164 } 165 166 if (HasLevelNextWave(level)) 167 { 168 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 169 { 170 level->nextState = LEVEL_STATE_BUILDING; 171 } 172 } 173 else { 174 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 175 { 176 level->nextState = LEVEL_STATE_WON_LEVEL; 177 } 178 } 179 } 180 181 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 182 { 183 static ButtonState buttonStates[8] = {0}; 184 int cost = GetTowerCosts(towerType); 185 const char *text = TextFormat("%s: %d", name, cost); 186 buttonStates[towerType].isSelected = level->placementMode == towerType; 187 buttonStates[towerType].isDisabled = level->playerGold < cost; 188 if (Button(text, x, y, width, height, &buttonStates[towerType])) 189 { 190 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 191 } 192 } 193 194 void DrawLevelGround(Level *level) 195 { 196 // draw checkerboard ground pattern 197 for (int x = -5; x <= 5; x += 1) 198 { 199 for (int y = -5; y <= 5; y += 1) 200 { 201 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
202 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 203 } 204 } 205 206 // draw grass patches around the edges 207 const int layerCount = 2; 208 for (int layer = 0; layer < layerCount; layer++) 209 { 210 int layerPos = 6 + layer; 211 for (int x = -6 + layer; x <= 6 + layer; x += 1) 212 { 213 DrawModel(grassPatchModel[0], 214 (Vector3){x, 0.0f, -layerPos}, 215 1.0f, WHITE); 216 DrawModel(grassPatchModel[0], 217 (Vector3){x, 0.0f, layerPos}, 218 1.0f, WHITE); 219 } 220 221 for (int z = -5 + layer; z <= 5 + layer; z += 1) 222 { 223 DrawModel(grassPatchModel[0], 224 (Vector3){-layerPos, 0.0f, z}, 225 1.0f, WHITE); 226 DrawModel(grassPatchModel[0], 227 (Vector3){layerPos, 0.0f, z}, 228 1.0f, WHITE);
229 } 230 } 231 } 232 233 void DrawLevelBuildingState(Level *level) 234 { 235 BeginMode3D(level->camera); 236 DrawLevelGround(level); 237 TowerDraw(); 238 EnemyDraw(); 239 ProjectileDraw(); 240 ParticleDraw(); 241 242 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 243 float planeDistance = ray.position.y / -ray.direction.y; 244 float planeX = ray.direction.x * planeDistance + ray.position.x; 245 float planeY = ray.direction.z * planeDistance + ray.position.z; 246 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 247 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 248 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5) 249 { 250 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 251 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 252 { 253 if (TowerTryAdd(level->placementMode, mapX, mapY)) 254 { 255 level->playerGold -= GetTowerCosts(level->placementMode); 256 level->placementMode = TOWER_TYPE_NONE; 257 } 258 } 259 } 260 261 guiState.isBlocked = 0; 262 263 EndMode3D(); 264 265 TowerDrawHealthBars(level->camera); 266 267 static ButtonState buildWallButtonState = {0}; 268 static ButtonState buildGunButtonState = {0}; 269 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 270 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN; 271 272 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 273 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_GUN, "Archer"); 274 275 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 276 { 277 level->nextState = LEVEL_STATE_RESET; 278 } 279 280 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 281 { 282 level->nextState = LEVEL_STATE_BATTLE; 283 } 284 285 const char *text = "Building phase"; 286 int textWidth = MeasureText(text, 20); 287 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 288 } 289 290 void InitBattleStateConditions(Level *level) 291 { 292 level->state = LEVEL_STATE_BATTLE; 293 level->nextState = LEVEL_STATE_NONE; 294 level->waveEndTimer = 0.0f; 295 for (int i = 0; i < 10; i++) 296 { 297 EnemyWave *wave = &level->waves[i]; 298 wave->spawned = 0; 299 wave->timeToSpawnNext = wave->delay; 300 } 301 } 302 303 void DrawLevelBattleState(Level *level) 304 { 305 BeginMode3D(level->camera); 306 DrawLevelGround(level); 307 TowerDraw(); 308 EnemyDraw(); 309 ProjectileDraw(); 310 ParticleDraw(); 311 guiState.isBlocked = 0; 312 EndMode3D(); 313 314 EnemyDrawHealthbars(level->camera); 315 TowerDrawHealthBars(level->camera); 316 317 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 318 { 319 level->nextState = LEVEL_STATE_RESET; 320 } 321 322 int maxCount = 0; 323 int remainingCount = 0; 324 for (int i = 0; i < 10; i++) 325 { 326 EnemyWave *wave = &level->waves[i]; 327 if (wave->wave != level->currentWave) 328 { 329 continue; 330 } 331 maxCount += wave->count; 332 remainingCount += wave->count - wave->spawned; 333 } 334 int aliveCount = EnemyCount(); 335 remainingCount += aliveCount; 336 337 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 338 int textWidth = MeasureText(text, 20); 339 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 340 } 341 342 void DrawLevel(Level *level) 343 { 344 switch (level->state) 345 { 346 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 347 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 348 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 349 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 350 default: break; 351 } 352 353 DrawLevelHud(level); 354 } 355 356 void UpdateLevel(Level *level) 357 { 358 if (level->state == LEVEL_STATE_BATTLE) 359 { 360 int activeWaves = 0; 361 for (int i = 0; i < 10; i++) 362 { 363 EnemyWave *wave = &level->waves[i]; 364 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 365 { 366 continue; 367 } 368 activeWaves++; 369 wave->timeToSpawnNext -= gameTime.deltaTime; 370 if (wave->timeToSpawnNext <= 0.0f) 371 { 372 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 373 if (enemy) 374 { 375 wave->timeToSpawnNext = wave->interval; 376 wave->spawned++; 377 } 378 } 379 } 380 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 381 level->waveEndTimer += gameTime.deltaTime; 382 if (level->waveEndTimer >= 2.0f) 383 { 384 level->nextState = LEVEL_STATE_LOST_WAVE; 385 } 386 } 387 else if (activeWaves == 0 && EnemyCount() == 0) 388 { 389 level->waveEndTimer += gameTime.deltaTime; 390 if (level->waveEndTimer >= 2.0f) 391 { 392 level->nextState = LEVEL_STATE_WON_WAVE; 393 } 394 } 395 } 396 397 PathFindingMapUpdate(); 398 EnemyUpdate(); 399 TowerUpdate(); 400 ProjectileUpdate(); 401 ParticleUpdate(); 402 403 if (level->nextState == LEVEL_STATE_RESET) 404 { 405 InitLevel(level); 406 } 407 408 if (level->nextState == LEVEL_STATE_BATTLE) 409 { 410 InitBattleStateConditions(level); 411 } 412 413 if (level->nextState == LEVEL_STATE_WON_WAVE) 414 { 415 level->currentWave++; 416 level->state = LEVEL_STATE_WON_WAVE; 417 } 418 419 if (level->nextState == LEVEL_STATE_LOST_WAVE) 420 { 421 level->state = LEVEL_STATE_LOST_WAVE; 422 } 423 424 if (level->nextState == LEVEL_STATE_BUILDING) 425 { 426 level->state = LEVEL_STATE_BUILDING; 427 } 428 429 if (level->nextState == LEVEL_STATE_WON_LEVEL) 430 { 431 // make something of this later 432 InitLevel(level); 433 } 434 435 level->nextState = LEVEL_STATE_NONE; 436 } 437 438 float nextSpawnTime = 0.0f; 439 440 void ResetGame() 441 { 442 InitLevel(currentLevel); 443 } 444 445 void InitGame() 446 { 447 TowerInit(); 448 EnemyInit(); 449 ProjectileInit(); 450 ParticleInit(); 451 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 452 453 currentLevel = levels; 454 InitLevel(currentLevel); 455 } 456 457 //# Immediate GUI functions 458 459 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 460 { 461 const float healthBarHeight = 6.0f; 462 const float healthBarOffset = 15.0f; 463 const float inset = 2.0f; 464 const float innerWidth = healthBarWidth - inset * 2; 465 const float innerHeight = healthBarHeight - inset * 2; 466 467 Vector2 screenPos = GetWorldToScreen(position, camera); 468 float centerX = screenPos.x - healthBarWidth * 0.5f; 469 float topY = screenPos.y - healthBarOffset; 470 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 471 float healthWidth = innerWidth * healthRatio; 472 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 473 } 474 475 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 476 { 477 Rectangle bounds = {x, y, width, height}; 478 int isPressed = 0; 479 int isSelected = state && state->isSelected; 480 int isDisabled = state && state->isDisabled; 481 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 482 { 483 Color color = isSelected ? DARKGRAY : GRAY; 484 DrawRectangle(x, y, width, height, color); 485 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 486 { 487 isPressed = 1; 488 } 489 guiState.isBlocked = 1; 490 } 491 else 492 { 493 Color color = isSelected ? WHITE : LIGHTGRAY; 494 DrawRectangle(x, y, width, height, color); 495 } 496 Font font = GetFontDefault(); 497 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 498 Color textColor = isDisabled ? GRAY : BLACK; 499 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 500 return isPressed; 501 } 502 503 //# Main game loop 504 505 void GameUpdate() 506 { 507 float dt = GetFrameTime(); 508 // cap maximum delta time to 0.1 seconds to prevent large time steps 509 if (dt > 0.1f) dt = 0.1f; 510 gameTime.time += dt; 511 gameTime.deltaTime = dt; 512 513 UpdateLevel(currentLevel); 514 } 515 516 int main(void) 517 { 518 int screenWidth, screenHeight; 519 GetPreferredSize(&screenWidth, &screenHeight); 520 InitWindow(screenWidth, screenHeight, "Tower defense"); 521 SetTargetFPS(30); 522 523 LoadAssets(); 524 InitGame(); 525 526 while (!WindowShouldClose()) 527 { 528 if (IsPaused()) { 529 // canvas is not visible in browser - do nothing 530 continue; 531 } 532 533 BeginDrawing(); 534 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 535 536 GameUpdate(); 537 DrawLevel(currentLevel); 538 539 EndDrawing(); 540 } 541 542 CloseWindow(); 543 544 return 0; 545 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
 30 #define TOWER_TYPE_NONE 0
 31 #define TOWER_TYPE_BASE 1
 32 #define TOWER_TYPE_GUN 2
 33 #define TOWER_TYPE_WALL 3
 34 #define TOWER_TYPE_COUNT 4
 35 
 36 typedef struct Tower
 37 {
 38   int16_t x, y;
 39   uint8_t towerType;
 40   Vector2 lastTargetPosition;
 41   float cooldown;
 42   float damage;
 43 } Tower;
 44 
 45 typedef struct GameTime
 46 {
 47   float time;
 48   float deltaTime;
 49 } GameTime;
 50 
 51 typedef struct ButtonState {
 52   char isSelected;
 53   char isDisabled;
 54 } ButtonState;
 55 
 56 typedef struct GUIState {
 57   int isBlocked;
 58 } GUIState;
 59 
 60 typedef enum LevelState
 61 {
 62   LEVEL_STATE_NONE,
 63   LEVEL_STATE_BUILDING,
 64   LEVEL_STATE_BATTLE,
 65   LEVEL_STATE_WON_WAVE,
 66   LEVEL_STATE_LOST_WAVE,
 67   LEVEL_STATE_WON_LEVEL,
 68   LEVEL_STATE_RESET,
 69 } LevelState;
 70 
 71 typedef struct EnemyWave {
 72   uint8_t enemyType;
 73   uint8_t wave;
 74   uint16_t count;
 75   float interval;
 76   float delay;
 77   Vector2 spawnPosition;
 78 
 79   uint16_t spawned;
 80   float timeToSpawnNext;
 81 } EnemyWave;
 82 
 83 typedef struct Level
 84 {
 85   LevelState state;
 86   LevelState nextState;
 87   Camera3D camera;
 88   int placementMode;
 89 
 90   int initialGold;
 91   int playerGold;
 92 
 93   EnemyWave waves[10];
 94   int currentWave;
 95   float waveEndTimer;
 96 } Level;
 97 
 98 typedef struct DeltaSrc
 99 {
100   char x, y;
101 } DeltaSrc;
102 
103 typedef struct PathfindingMap
104 {
105   int width, height;
106   float scale;
107   float *distances;
108   long *towerIndex; 
109   DeltaSrc *deltaSrc;
110   float maxDistance;
111   Matrix toMapSpace;
112   Matrix toWorldSpace;
113 } PathfindingMap;
114 
115 // when we execute the pathfinding algorithm, we need to store the active nodes
116 // in a queue. Each node has a position, a distance from the start, and the
117 // position of the node that we came from.
118 typedef struct PathfindingNode
119 {
120   int16_t x, y, fromX, fromY;
121   float distance;
122 } PathfindingNode;
123 
124 typedef struct EnemyId
125 {
126   uint16_t index;
127   uint16_t generation;
128 } EnemyId;
129 
130 typedef struct EnemyClassConfig
131 {
132   float speed;
133   float health;
134   float radius;
135   float maxAcceleration;
136   float requiredContactTime;
137   float explosionDamage;
138   float explosionRange;
139   float explosionPushbackPower;
140   int goldValue;
141 } EnemyClassConfig;
142 
143 typedef struct Enemy
144 {
145   int16_t currentX, currentY;
146   int16_t nextX, nextY;
147   Vector2 simPosition;
148   Vector2 simVelocity;
149   uint16_t generation;
150   float walkedDistance;
151   float startMovingTime;
152   float damage, futureDamage;
153   float contactTime;
154   uint8_t enemyType;
155   uint8_t movePathCount;
156   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
157 } Enemy;
158 
159 // a unit that uses sprites to be drawn
160 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
161 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
162 typedef struct SpriteUnit
163 {
164   Rectangle srcRect;
165   Vector2 offset;
166   int frameCount;
167   float frameDuration;
168   Rectangle srcWeaponIdleRect;
169   Vector2 srcWeaponIdleOffset;
170   Rectangle srcWeaponCooldownRect;
171   Vector2 srcWeaponCooldownOffset;
172 } SpriteUnit;
173 
174 #define PROJECTILE_MAX_COUNT 1200
175 #define PROJECTILE_TYPE_NONE 0
176 #define PROJECTILE_TYPE_ARROW 1
177 
178 typedef struct Projectile
179 {
180   uint8_t projectileType;
181   float shootTime;
182   float arrivalTime;
183   float distance;
184   float damage;
185   Vector3 position;
186   Vector3 target;
187   Vector3 directionNormal;
188   EnemyId targetEnemy;
189 } Projectile;
190 
191 //# Function declarations
192 float TowerGetMaxHealth(Tower *tower);
193 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
194 int EnemyAddDamage(Enemy *enemy, float damage);
195 
196 //# Enemy functions
197 void EnemyInit();
198 void EnemyDraw();
199 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
200 void EnemyUpdate();
201 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
202 float EnemyGetMaxHealth(Enemy *enemy);
203 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
204 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
205 EnemyId EnemyGetId(Enemy *enemy);
206 Enemy *EnemyTryResolve(EnemyId enemyId);
207 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
208 int EnemyAddDamage(Enemy *enemy, float damage);
209 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
210 int EnemyCount();
211 void EnemyDrawHealthbars(Camera3D camera);
212 
213 //# Tower functions
214 void TowerInit();
215 Tower *TowerGetAt(int16_t x, int16_t y);
216 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
217 Tower *GetTowerByType(uint8_t towerType);
218 int GetTowerCosts(uint8_t towerType);
219 float TowerGetMaxHealth(Tower *tower);
220 void TowerDraw();
221 void TowerUpdate();
222 void TowerDrawHealthBars(Camera3D camera);
223 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
224 
225 //# Particles
226 void ParticleInit();
227 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
228 void ParticleUpdate();
229 void ParticleDraw();
230 
231 //# Projectiles
232 void ProjectileInit();
233 void ProjectileDraw();
234 void ProjectileUpdate();
235 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage);
236 
237 //# Pathfinding map
238 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
239 float PathFindingGetDistance(int mapX, int mapY);
240 Vector2 PathFindingGetGradient(Vector3 world);
241 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
242 void PathFindingMapUpdate();
243 void PathFindingMapDraw();
244 
245 //# UI
246 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
247 
248 //# Level
249 void DrawLevelGround(Level *level);
250 
251 //# variables
252 extern Level *currentLevel;
253 extern Enemy enemies[ENEMY_MAX_COUNT];
254 extern int enemyCount;
255 extern EnemyClassConfig enemyClassConfigs[];
256 
257 extern GUIState guiState;
258 extern GameTime gameTime;
259 extern Tower towers[TOWER_MAX_COUNT];
260 extern int towerCount;
261 
262 extern Texture2D palette, spriteSheet;
263 
264 #endif
  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}, 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 EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522   }
523 }
  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()
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 < towerCount; i++)
131   {
132     Tower *tower = &towers[i];
133     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134     {
135       continue;
136     }
137     int16_t mapX, mapY;
138     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139     // this would not work correctly and needs to be refined to allow towers covering multiple cells
140     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141     // one cell. For now.
142     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143     {
144       continue;
145     }
146     int index = mapY * width + mapX;
147     pathfindingMap.towerIndex[index] = i;
148   }
149 
150   // we start at the castle and add the castle to the queue
151   pathfindingMap.maxDistance = 0.0f;
152   pathfindingNodeQueueCount = 0;
153   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154   PathfindingNode *node = 0;
155   while ((node = PathFindingNodePop()))
156   {
157     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158     {
159       continue;
160     }
161     int index = node->y * width + node->x;
162     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163     {
164       continue;
165     }
166 
167     int deltaX = node->x - node->fromX;
168     int deltaY = node->y - node->fromY;
169     // even if the cell is blocked by a tower, we still may want to store the direction
170     // (though this might not be needed, IDK right now)
171     pathfindingMap.deltaSrc[index].x = (char) deltaX;
172     pathfindingMap.deltaSrc[index].y = (char) deltaY;
173 
174     // we skip nodes that are blocked by towers
175     if (pathfindingMap.towerIndex[index] >= 0)
176     {
177       node->distance += 8.0f;
178     }
179     pathfindingMap.distances[index] = node->distance;
180     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185   }
186 }
187 
188 void PathFindingMapDraw()
189 {
190   float cellSize = pathfindingMap.scale * 0.9f;
191   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192   for (int x = 0; x < pathfindingMap.width; x++)
193   {
194     for (int y = 0; y < pathfindingMap.height; y++)
195     {
196       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200       // animate the distance "wave" to show how the pathfinding algorithm expands
201       // from the castle
202       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203       {
204         color = BLACK;
205       }
206       DrawCube(position, cellSize, 0.1f, cellSize, color);
207     }
208   }
209 }
210 
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213   int16_t mapX, mapY;
214   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215   {
216     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217     return (Vector2){(float)-delta.x, (float)-delta.y};
218   }
219   // fallback to a simple gradient calculation
220   float n = PathFindingGetDistance(mapX, mapY - 1);
221   float s = PathFindingGetDistance(mapX, mapY + 1);
222   float w = PathFindingGetDistance(mapX - 1, mapY);
223   float e = PathFindingGetDistance(mapX + 1, mapY);
224   return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 void ProjectileInit()
  8 {
  9   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 10   {
 11     projectiles[i] = (Projectile){0};
 12   }
 13 }
 14 
 15 void ProjectileDraw()
 16 {
 17   for (int i = 0; i < projectileCount; i++)
 18   {
 19     Projectile projectile = projectiles[i];
 20     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 21     {
 22       continue;
 23     }
 24     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 25     if (transition >= 1.0f)
 26     {
 27       continue;
 28     }
 29     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 30     {
 31       float t = transition + transitionOffset * 0.3f;
 32       if (t > 1.0f)
 33       {
 34         break;
 35       }
 36       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 37       Color color = RED;
 38       if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 39       {
 40         // make tip red but quickly fade to brown
 41         color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 42         // fake a ballista flight path using parabola equation
 43         float parabolaT = t - 0.5f;
 44         parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 45         position.y += 0.15f * parabolaT * projectile.distance;
 46       }
 47 
 48       float size = 0.06f * (transitionOffset + 0.25f);
 49       DrawCube(position, size, size, size, color);
 50     }
 51   }
 52 }
 53 
 54 void ProjectileUpdate()
 55 {
 56   for (int i = 0; i < projectileCount; i++)
 57   {
 58     Projectile *projectile = &projectiles[i];
 59     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 60     {
 61       continue;
 62     }
 63     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 64     if (transition >= 1.0f)
 65     {
 66       projectile->projectileType = PROJECTILE_TYPE_NONE;
 67       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 68       if (enemy)
 69       {
 70         EnemyAddDamage(enemy, projectile->damage);
 71       }
 72       continue;
 73     }
 74   }
 75 }
 76 
 77 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage)
 78 {
 79   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       projectile->projectileType = projectileType;
 85       projectile->shootTime = gameTime.time;
 86       float distance = Vector3Distance(position, target);
 87       projectile->arrivalTime = gameTime.time + distance / speed;
 88       projectile->damage = damage;
 89       projectile->position = position;
 90       projectile->target = target;
 91       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
 92       projectile->distance = distance;
 93       projectile->targetEnemy = EnemyGetId(enemy);
 94       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 95       return projectile;
 96     }
 97   }
 98   return 0;
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 Tower towers[TOWER_MAX_COUNT];
  5 int towerCount = 0;
  6 
  7 Model towerModels[TOWER_TYPE_COUNT];
  8 
  9 // definition of our archer unit
 10 SpriteUnit archerUnit = {
 11     .srcRect = {0, 0, 16, 16},
 12     .offset = {7, 1},
 13     .frameCount = 1,
 14     .frameDuration = 0.0f,
 15     .srcWeaponIdleRect = {16, 0, 6, 16},
 16     .srcWeaponIdleOffset = {8, 0},
 17     .srcWeaponCooldownRect = {22, 0, 11, 16},
 18     .srcWeaponCooldownOffset = {10, 0},
 19 };
 20 
 21 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 22 {
 23   float xScale = flip ? -1.0f : 1.0f;
 24   Camera3D camera = currentLevel->camera;
 25   float size = 0.5f;
 26   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 27   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 28   // we want the sprite to face the camera, so we need to calculate the up vector
 29   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 30   Vector3 up = {0, 1, 0};
 31   Vector3 right = Vector3CrossProduct(forward, up);
 32   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 33 
 34   Rectangle srcRect = unit.srcRect;
 35   if (unit.frameCount > 1)
 36   {
 37     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 38   }
 39   if (flip)
 40   {
 41     srcRect.x += srcRect.width;
 42     srcRect.width = -srcRect.width;
 43   }
 44   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 45 
 46   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 47   {
 48     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 49     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 50     srcRect = unit.srcWeaponCooldownRect;
 51     if (flip)
 52     {
 53       // position.x = flip * scale.x * 0.5f;
 54       srcRect.x += srcRect.width;
 55       srcRect.width = -srcRect.width;
 56       offset.x = scale.x - offset.x;
 57     }
 58     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 59   }
 60   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
 61   {
 62     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
 63     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
 64     srcRect = unit.srcWeaponIdleRect;
 65     if (flip)
 66     {
 67       // position.x = flip * scale.x * 0.5f;
 68       srcRect.x += srcRect.width;
 69       srcRect.width = -srcRect.width;
 70       offset.x = scale.x - offset.x;
 71     }
 72     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 73   }
 74 }
 75 
 76 void TowerInit()
 77 {
 78   for (int i = 0; i < TOWER_MAX_COUNT; i++)
 79   {
 80     towers[i] = (Tower){0};
 81   }
 82   towerCount = 0;
 83 
 84   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
 85   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
 86 
 87   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
 88   {
 89     if (towerModels[i].materials)
 90     {
 91       // assign the palette texture to the material of the model (0 is not used afaik)
 92       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 93     }
 94   }
 95 }
 96 
 97 static void TowerGunUpdate(Tower *tower)
 98 {
 99   if (tower->cooldown <= 0)
100   {
101     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
102     if (enemy)
103     {
104       tower->cooldown = 0.5f;
105       // shoot the enemy; determine future position of the enemy
106       float bulletSpeed = 4.0f;
107       float bulletDamage = 3.0f;
108       Vector2 velocity = enemy->simVelocity;
109       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
110       Vector2 towerPosition = {tower->x, tower->y};
111       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
112       for (int i = 0; i < 8; i++) {
113         velocity = enemy->simVelocity;
114         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
115         float distance = Vector2Distance(towerPosition, futurePosition);
116         float eta2 = distance / bulletSpeed;
117         if (fabs(eta - eta2) < 0.01f) {
118           break;
119         }
120         eta = (eta2 + eta) * 0.5f;
121       }
122       ProjectileTryAdd(PROJECTILE_TYPE_ARROW, enemy, 
123         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
124         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
125         bulletSpeed, bulletDamage);
126       enemy->futureDamage += bulletDamage;
127       tower->lastTargetPosition = futurePosition;
128     }
129   }
130   else
131   {
132     tower->cooldown -= gameTime.deltaTime;
133   }
134 }
135 
136 Tower *TowerGetAt(int16_t x, int16_t y)
137 {
138   for (int i = 0; i < towerCount; i++)
139   {
140     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
141     {
142       return &towers[i];
143     }
144   }
145   return 0;
146 }
147 
148 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
149 {
150   if (towerCount >= TOWER_MAX_COUNT)
151   {
152     return 0;
153   }
154 
155   Tower *tower = TowerGetAt(x, y);
156   if (tower)
157   {
158     return 0;
159   }
160 
161   tower = &towers[towerCount++];
162   tower->x = x;
163   tower->y = y;
164   tower->towerType = towerType;
165   tower->cooldown = 0.0f;
166   tower->damage = 0.0f;
167   return tower;
168 }
169 
170 Tower *GetTowerByType(uint8_t towerType)
171 {
172   for (int i = 0; i < towerCount; i++)
173   {
174     if (towers[i].towerType == towerType)
175     {
176       return &towers[i];
177     }
178   }
179   return 0;
180 }
181 
182 int GetTowerCosts(uint8_t towerType)
183 {
184   switch (towerType)
185   {
186   case TOWER_TYPE_BASE:
187     return 0;
188   case TOWER_TYPE_GUN:
189     return 6;
190   case TOWER_TYPE_WALL:
191     return 2;
192   }
193   return 0;
194 }
195 
196 float TowerGetMaxHealth(Tower *tower)
197 {
198   switch (tower->towerType)
199   {
200   case TOWER_TYPE_BASE:
201     return 10.0f;
202   case TOWER_TYPE_GUN:
203     return 3.0f;
204   case TOWER_TYPE_WALL:
205     return 5.0f;
206   }
207   return 0.0f;
208 }
209 
210 void TowerDraw()
211 {
212   for (int i = 0; i < towerCount; i++)
213   {
214     Tower tower = towers[i];
215     if (tower.towerType == TOWER_TYPE_NONE)
216     {
217       continue;
218     }
219 
220     switch (tower.towerType)
221     {
222     case TOWER_TYPE_GUN:
223       {
224         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
225         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
226         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
227         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
228           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
229       }
230       break;
231     default:
232       if (towerModels[tower.towerType].materials)
233       {
234         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
235       } else {
236         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
237       }
238       break;
239     }
240   }
241 }
242 
243 void TowerUpdate()
244 {
245   for (int i = 0; i < towerCount; i++)
246   {
247     Tower *tower = &towers[i];
248     switch (tower->towerType)
249     {
250     case TOWER_TYPE_GUN:
251       TowerGunUpdate(tower);
252       break;
253     }
254   }
255 }
256 
257 void TowerDrawHealthBars(Camera3D camera)
258 {
259   for (int i = 0; i < towerCount; i++)
260   {
261     Tower *tower = &towers[i];
262     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
263     {
264       continue;
265     }
266     
267     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
268     float maxHealth = TowerGetMaxHealth(tower);
269     float health = maxHealth - tower->damage;
270     float healthRatio = health / maxHealth;
271     
272     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
273   }
274 }
  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
Everything is the same, but now we have some grass around the map.

The first change we do is to load an additional model and draw it around the map. The placement of the objects uses two nested loops constructions:

  1   const int layerCount = 2;
  2   for (int layer = 0; layer < layerCount; layer++)
  3   {
  4     int layerPos = 6 + layer;
  5     for (int x = -6 + layer; x <= 6 + layer; x += 1)
  6     {
  7       DrawModel(grassPatchModel[0], 
  8         (Vector3){x, 0.0f, -layerPos}, 
  9         1.0f, WHITE);
 10       DrawModel(grassPatchModel[0], 
 11         (Vector3){x, 0.0f, layerPos}, 
 12         1.0f, WHITE);
 13     }
 14 
 15     for (int z = -5 + layer; z <= 5 + layer; z += 1)
 16     {
 17       DrawModel(grassPatchModel[0], 
 18         (Vector3){-layerPos, 0.0f, z}, 
 19         1.0f, WHITE);
 20       DrawModel(grassPatchModel[0], 
 21         (Vector3){layerPos, 0.0f, z}, 
 22         1.0f, WHITE);
 23     }
 24   }

The layerCount variable determines how many layers of objects are placed around the map. The layerPos variable is the distance from the center of the map - either in x or z direction (since our map is a square, one variable can be used for both axes).

The purpose of the two inner loops is to place objects along the edges of the x and z axis. The first loop places the models along the x axis on the left and right side of the map. The second loop places the models along the z axis on the top and bottom side of the map.

However, the objects are placed in a very strict grid. It would be better to place them in some random fashion to make it look more natural. Let's do that next and use a random value to offset the position of the objects a little bit.

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 //# Variables
  7 GUIState guiState = {0};
  8 GameTime gameTime = {0};
  9 
 10 Model floorTileAModel = {0};
 11 Model floorTileBModel = {0};
 12 Model grassPatchModel[1] = {0};
 13 
 14 Texture2D palette, spriteSheet;
 15 
 16 Level levels[] = {
 17   [0] = {
 18     .state = LEVEL_STATE_BUILDING,
 19     .initialGold = 20,
 20     .waves[0] = {
 21       .enemyType = ENEMY_TYPE_MINION,
 22       .wave = 0,
 23       .count = 10,
 24       .interval = 2.5f,
 25       .delay = 1.0f,
 26       .spawnPosition = {0, 6},
 27     },
 28     .waves[1] = {
 29       .enemyType = ENEMY_TYPE_MINION,
 30       .wave = 1,
 31       .count = 20,
 32       .interval = 1.5f,
 33       .delay = 1.0f,
 34       .spawnPosition = {0, 6},
 35     },
 36     .waves[2] = {
 37       .enemyType = ENEMY_TYPE_MINION,
 38       .wave = 2,
 39       .count = 30,
 40       .interval = 1.2f,
 41       .delay = 1.0f,
 42       .spawnPosition = {0, 6},
 43     }
 44   },
 45 };
 46 
 47 Level *currentLevel = levels;
 48 
 49 //# Game
 50 
 51 static Model LoadGLBModel(char *filename)
 52 {
 53   Model model = LoadModel(TextFormat("data/%s.glb",filename));
 54   if (model.materialCount > 1)
 55   {
 56     model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 57   }
 58   return model;
 59 }
 60 
 61 void LoadAssets()
 62 {
 63   // load a sprite sheet that contains all units
 64   spriteSheet = LoadTexture("data/spritesheet.png");
 65   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
 66 
 67   // we'll use a palette texture to colorize the all buildings and environment art
 68   palette = LoadTexture("data/palette.png");
 69   // The texture uses gradients on very small space, so we'll enable bilinear filtering
 70   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
 71 
 72   floorTileAModel = LoadGLBModel("floor-tile-a");
 73   floorTileBModel = LoadGLBModel("floor-tile-b");
 74   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
 75 }
 76 
 77 void InitLevel(Level *level)
 78 {
 79   TowerInit();
 80   EnemyInit();
 81   ProjectileInit();
 82   ParticleInit();
 83   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
 84 
 85   level->placementMode = 0;
 86   level->state = LEVEL_STATE_BUILDING;
 87   level->nextState = LEVEL_STATE_NONE;
 88   level->playerGold = level->initialGold;
 89   level->currentWave = 0;
 90 
 91   Camera *camera = &level->camera;
 92   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
 93   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
 94   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
 95   camera->fovy = 10.0f;
 96   camera->projection = CAMERA_ORTHOGRAPHIC;
 97 }
 98 
 99 void DrawLevelHud(Level *level)
100 {
101   const char *text = TextFormat("Gold: %d", level->playerGold);
102   Font font = GetFontDefault();
103   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
104   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
105 }
106 
107 void DrawLevelReportLostWave(Level *level)
108 {
109   BeginMode3D(level->camera);
110   DrawLevelGround(level);
111   TowerDraw();
112   EnemyDraw();
113   ProjectileDraw();
114   ParticleDraw();
115   guiState.isBlocked = 0;
116   EndMode3D();
117 
118   TowerDrawHealthBars(level->camera);
119 
120   const char *text = "Wave lost";
121   int textWidth = MeasureText(text, 20);
122   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
123 
124   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
125   {
126     level->nextState = LEVEL_STATE_RESET;
127   }
128 }
129 
130 int HasLevelNextWave(Level *level)
131 {
132   for (int i = 0; i < 10; i++)
133   {
134     EnemyWave *wave = &level->waves[i];
135     if (wave->wave == level->currentWave)
136     {
137       return 1;
138     }
139   }
140   return 0;
141 }
142 
143 void DrawLevelReportWonWave(Level *level)
144 {
145   BeginMode3D(level->camera);
146   DrawLevelGround(level);
147   TowerDraw();
148   EnemyDraw();
149   ProjectileDraw();
150   ParticleDraw();
151   guiState.isBlocked = 0;
152   EndMode3D();
153 
154   TowerDrawHealthBars(level->camera);
155 
156   const char *text = "Wave won";
157   int textWidth = MeasureText(text, 20);
158   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
159 
160 
161   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
162   {
163     level->nextState = LEVEL_STATE_RESET;
164   }
165 
166   if (HasLevelNextWave(level))
167   {
168     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
169     {
170       level->nextState = LEVEL_STATE_BUILDING;
171     }
172   }
173   else {
174     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
175     {
176       level->nextState = LEVEL_STATE_WON_LEVEL;
177     }
178   }
179 }
180 
181 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
182 {
183   static ButtonState buttonStates[8] = {0};
184   int cost = GetTowerCosts(towerType);
185   const char *text = TextFormat("%s: %d", name, cost);
186   buttonStates[towerType].isSelected = level->placementMode == towerType;
187   buttonStates[towerType].isDisabled = level->playerGold < cost;
188   if (Button(text, x, y, width, height, &buttonStates[towerType]))
189   {
190     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
191   }
192 }
193 
194 void DrawLevelGround(Level *level)
195 {
196   // draw checkerboard ground pattern
197   for (int x = -5; x <= 5; x += 1)
198   {
199     for (int y = -5; y <= 5; y += 1)
200     {
201       Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
202       DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
203     }
204   }
205 
206   // draw grass patches around the edges
207   const int layerCount = 2;
208   for (int layer = 0; layer < layerCount; layer++)
209   {
210     int layerPos = 6 + layer;
211     for (int x = -6 + layer; x <= 6 + layer; x += 1)
212     {
213       DrawModel(grassPatchModel[0], 
214 (Vector3){x + (float)GetRandomValue(-25,25) / 100.0f, 0.0f, -layerPos + (float)GetRandomValue(-25,25) / 100.0f},
215 1.0f, WHITE); 216 DrawModel(grassPatchModel[0],
217 (Vector3){x + (float)GetRandomValue(-25,25) / 100.0f, 0.0f, layerPos + (float)GetRandomValue(-25,25) / 100.0f},
218 1.0f, WHITE); 219 } 220 221 for (int z = -5 + layer; z <= 5 + layer; z += 1) 222 { 223 DrawModel(grassPatchModel[0],
224 (Vector3){-layerPos + (float)GetRandomValue(-25,25) / 100.0f, 0.0f, z + (float)GetRandomValue(-25,25) / 100.0f},
225 1.0f, WHITE); 226 DrawModel(grassPatchModel[0],
227 (Vector3){layerPos + (float)GetRandomValue(-25,25) / 100.0f, 0.0f, z + (float)GetRandomValue(-25,25) / 100.0f},
228 1.0f, WHITE); 229 } 230 } 231 } 232 233 void DrawLevelBuildingState(Level *level) 234 { 235 BeginMode3D(level->camera); 236 DrawLevelGround(level); 237 TowerDraw(); 238 EnemyDraw(); 239 ProjectileDraw(); 240 ParticleDraw(); 241 242 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 243 float planeDistance = ray.position.y / -ray.direction.y; 244 float planeX = ray.direction.x * planeDistance + ray.position.x; 245 float planeY = ray.direction.z * planeDistance + ray.position.z; 246 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 247 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 248 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5) 249 { 250 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 251 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 252 { 253 if (TowerTryAdd(level->placementMode, mapX, mapY)) 254 { 255 level->playerGold -= GetTowerCosts(level->placementMode); 256 level->placementMode = TOWER_TYPE_NONE; 257 } 258 } 259 } 260 261 guiState.isBlocked = 0; 262 263 EndMode3D(); 264 265 TowerDrawHealthBars(level->camera); 266 267 static ButtonState buildWallButtonState = {0}; 268 static ButtonState buildGunButtonState = {0}; 269 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 270 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN; 271 272 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 273 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_GUN, "Archer"); 274 275 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 276 { 277 level->nextState = LEVEL_STATE_RESET; 278 } 279 280 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 281 { 282 level->nextState = LEVEL_STATE_BATTLE; 283 } 284 285 const char *text = "Building phase"; 286 int textWidth = MeasureText(text, 20); 287 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 288 } 289 290 void InitBattleStateConditions(Level *level) 291 { 292 level->state = LEVEL_STATE_BATTLE; 293 level->nextState = LEVEL_STATE_NONE; 294 level->waveEndTimer = 0.0f; 295 for (int i = 0; i < 10; i++) 296 { 297 EnemyWave *wave = &level->waves[i]; 298 wave->spawned = 0; 299 wave->timeToSpawnNext = wave->delay; 300 } 301 } 302 303 void DrawLevelBattleState(Level *level) 304 { 305 BeginMode3D(level->camera); 306 DrawLevelGround(level); 307 TowerDraw(); 308 EnemyDraw(); 309 ProjectileDraw(); 310 ParticleDraw(); 311 guiState.isBlocked = 0; 312 EndMode3D(); 313 314 EnemyDrawHealthbars(level->camera); 315 TowerDrawHealthBars(level->camera); 316 317 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 318 { 319 level->nextState = LEVEL_STATE_RESET; 320 } 321 322 int maxCount = 0; 323 int remainingCount = 0; 324 for (int i = 0; i < 10; i++) 325 { 326 EnemyWave *wave = &level->waves[i]; 327 if (wave->wave != level->currentWave) 328 { 329 continue; 330 } 331 maxCount += wave->count; 332 remainingCount += wave->count - wave->spawned; 333 } 334 int aliveCount = EnemyCount(); 335 remainingCount += aliveCount; 336 337 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 338 int textWidth = MeasureText(text, 20); 339 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 340 } 341 342 void DrawLevel(Level *level) 343 { 344 switch (level->state) 345 { 346 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 347 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 348 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 349 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 350 default: break; 351 } 352 353 DrawLevelHud(level); 354 } 355 356 void UpdateLevel(Level *level) 357 { 358 if (level->state == LEVEL_STATE_BATTLE) 359 { 360 int activeWaves = 0; 361 for (int i = 0; i < 10; i++) 362 { 363 EnemyWave *wave = &level->waves[i]; 364 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 365 { 366 continue; 367 } 368 activeWaves++; 369 wave->timeToSpawnNext -= gameTime.deltaTime; 370 if (wave->timeToSpawnNext <= 0.0f) 371 { 372 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 373 if (enemy) 374 { 375 wave->timeToSpawnNext = wave->interval; 376 wave->spawned++; 377 } 378 } 379 } 380 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 381 level->waveEndTimer += gameTime.deltaTime; 382 if (level->waveEndTimer >= 2.0f) 383 { 384 level->nextState = LEVEL_STATE_LOST_WAVE; 385 } 386 } 387 else if (activeWaves == 0 && EnemyCount() == 0) 388 { 389 level->waveEndTimer += gameTime.deltaTime; 390 if (level->waveEndTimer >= 2.0f) 391 { 392 level->nextState = LEVEL_STATE_WON_WAVE; 393 } 394 } 395 } 396 397 PathFindingMapUpdate(); 398 EnemyUpdate(); 399 TowerUpdate(); 400 ProjectileUpdate(); 401 ParticleUpdate(); 402 403 if (level->nextState == LEVEL_STATE_RESET) 404 { 405 InitLevel(level); 406 } 407 408 if (level->nextState == LEVEL_STATE_BATTLE) 409 { 410 InitBattleStateConditions(level); 411 } 412 413 if (level->nextState == LEVEL_STATE_WON_WAVE) 414 { 415 level->currentWave++; 416 level->state = LEVEL_STATE_WON_WAVE; 417 } 418 419 if (level->nextState == LEVEL_STATE_LOST_WAVE) 420 { 421 level->state = LEVEL_STATE_LOST_WAVE; 422 } 423 424 if (level->nextState == LEVEL_STATE_BUILDING) 425 { 426 level->state = LEVEL_STATE_BUILDING; 427 } 428 429 if (level->nextState == LEVEL_STATE_WON_LEVEL) 430 { 431 // make something of this later 432 InitLevel(level); 433 } 434 435 level->nextState = LEVEL_STATE_NONE; 436 } 437 438 float nextSpawnTime = 0.0f; 439 440 void ResetGame() 441 { 442 InitLevel(currentLevel); 443 } 444 445 void InitGame() 446 { 447 TowerInit(); 448 EnemyInit(); 449 ProjectileInit(); 450 ParticleInit(); 451 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 452 453 currentLevel = levels; 454 InitLevel(currentLevel); 455 } 456 457 //# Immediate GUI functions 458 459 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 460 { 461 const float healthBarHeight = 6.0f; 462 const float healthBarOffset = 15.0f; 463 const float inset = 2.0f; 464 const float innerWidth = healthBarWidth - inset * 2; 465 const float innerHeight = healthBarHeight - inset * 2; 466 467 Vector2 screenPos = GetWorldToScreen(position, camera); 468 float centerX = screenPos.x - healthBarWidth * 0.5f; 469 float topY = screenPos.y - healthBarOffset; 470 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 471 float healthWidth = innerWidth * healthRatio; 472 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 473 } 474 475 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 476 { 477 Rectangle bounds = {x, y, width, height}; 478 int isPressed = 0; 479 int isSelected = state && state->isSelected; 480 int isDisabled = state && state->isDisabled; 481 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 482 { 483 Color color = isSelected ? DARKGRAY : GRAY; 484 DrawRectangle(x, y, width, height, color); 485 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 486 { 487 isPressed = 1; 488 } 489 guiState.isBlocked = 1; 490 } 491 else 492 { 493 Color color = isSelected ? WHITE : LIGHTGRAY; 494 DrawRectangle(x, y, width, height, color); 495 } 496 Font font = GetFontDefault(); 497 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 498 Color textColor = isDisabled ? GRAY : BLACK; 499 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 500 return isPressed; 501 } 502 503 //# Main game loop 504 505 void GameUpdate() 506 { 507 float dt = GetFrameTime(); 508 // cap maximum delta time to 0.1 seconds to prevent large time steps 509 if (dt > 0.1f) dt = 0.1f; 510 gameTime.time += dt; 511 gameTime.deltaTime = dt; 512 513 UpdateLevel(currentLevel); 514 } 515 516 int main(void) 517 { 518 int screenWidth, screenHeight; 519 GetPreferredSize(&screenWidth, &screenHeight); 520 InitWindow(screenWidth, screenHeight, "Tower defense"); 521 SetTargetFPS(30); 522 523 LoadAssets(); 524 InitGame(); 525 526 while (!WindowShouldClose()) 527 { 528 if (IsPaused()) { 529 // canvas is not visible in browser - do nothing 530 continue; 531 } 532 533 BeginDrawing(); 534 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 535 536 GameUpdate(); 537 DrawLevel(currentLevel); 538 539 EndDrawing(); 540 } 541 542 CloseWindow(); 543 544 return 0; 545 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
 30 #define TOWER_TYPE_NONE 0
 31 #define TOWER_TYPE_BASE 1
 32 #define TOWER_TYPE_GUN 2
 33 #define TOWER_TYPE_WALL 3
 34 #define TOWER_TYPE_COUNT 4
 35 
 36 typedef struct Tower
 37 {
 38   int16_t x, y;
 39   uint8_t towerType;
 40   Vector2 lastTargetPosition;
 41   float cooldown;
 42   float damage;
 43 } Tower;
 44 
 45 typedef struct GameTime
 46 {
 47   float time;
 48   float deltaTime;
 49 } GameTime;
 50 
 51 typedef struct ButtonState {
 52   char isSelected;
 53   char isDisabled;
 54 } ButtonState;
 55 
 56 typedef struct GUIState {
 57   int isBlocked;
 58 } GUIState;
 59 
 60 typedef enum LevelState
 61 {
 62   LEVEL_STATE_NONE,
 63   LEVEL_STATE_BUILDING,
 64   LEVEL_STATE_BATTLE,
 65   LEVEL_STATE_WON_WAVE,
 66   LEVEL_STATE_LOST_WAVE,
 67   LEVEL_STATE_WON_LEVEL,
 68   LEVEL_STATE_RESET,
 69 } LevelState;
 70 
 71 typedef struct EnemyWave {
 72   uint8_t enemyType;
 73   uint8_t wave;
 74   uint16_t count;
 75   float interval;
 76   float delay;
 77   Vector2 spawnPosition;
 78 
 79   uint16_t spawned;
 80   float timeToSpawnNext;
 81 } EnemyWave;
 82 
 83 typedef struct Level
 84 {
 85   LevelState state;
 86   LevelState nextState;
 87   Camera3D camera;
 88   int placementMode;
 89 
 90   int initialGold;
 91   int playerGold;
 92 
 93   EnemyWave waves[10];
 94   int currentWave;
 95   float waveEndTimer;
 96 } Level;
 97 
 98 typedef struct DeltaSrc
 99 {
100   char x, y;
101 } DeltaSrc;
102 
103 typedef struct PathfindingMap
104 {
105   int width, height;
106   float scale;
107   float *distances;
108   long *towerIndex; 
109   DeltaSrc *deltaSrc;
110   float maxDistance;
111   Matrix toMapSpace;
112   Matrix toWorldSpace;
113 } PathfindingMap;
114 
115 // when we execute the pathfinding algorithm, we need to store the active nodes
116 // in a queue. Each node has a position, a distance from the start, and the
117 // position of the node that we came from.
118 typedef struct PathfindingNode
119 {
120   int16_t x, y, fromX, fromY;
121   float distance;
122 } PathfindingNode;
123 
124 typedef struct EnemyId
125 {
126   uint16_t index;
127   uint16_t generation;
128 } EnemyId;
129 
130 typedef struct EnemyClassConfig
131 {
132   float speed;
133   float health;
134   float radius;
135   float maxAcceleration;
136   float requiredContactTime;
137   float explosionDamage;
138   float explosionRange;
139   float explosionPushbackPower;
140   int goldValue;
141 } EnemyClassConfig;
142 
143 typedef struct Enemy
144 {
145   int16_t currentX, currentY;
146   int16_t nextX, nextY;
147   Vector2 simPosition;
148   Vector2 simVelocity;
149   uint16_t generation;
150   float walkedDistance;
151   float startMovingTime;
152   float damage, futureDamage;
153   float contactTime;
154   uint8_t enemyType;
155   uint8_t movePathCount;
156   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
157 } Enemy;
158 
159 // a unit that uses sprites to be drawn
160 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
161 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
162 typedef struct SpriteUnit
163 {
164   Rectangle srcRect;
165   Vector2 offset;
166   int frameCount;
167   float frameDuration;
168   Rectangle srcWeaponIdleRect;
169   Vector2 srcWeaponIdleOffset;
170   Rectangle srcWeaponCooldownRect;
171   Vector2 srcWeaponCooldownOffset;
172 } SpriteUnit;
173 
174 #define PROJECTILE_MAX_COUNT 1200
175 #define PROJECTILE_TYPE_NONE 0
176 #define PROJECTILE_TYPE_ARROW 1
177 
178 typedef struct Projectile
179 {
180   uint8_t projectileType;
181   float shootTime;
182   float arrivalTime;
183   float distance;
184   float damage;
185   Vector3 position;
186   Vector3 target;
187   Vector3 directionNormal;
188   EnemyId targetEnemy;
189 } Projectile;
190 
191 //# Function declarations
192 float TowerGetMaxHealth(Tower *tower);
193 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
194 int EnemyAddDamage(Enemy *enemy, float damage);
195 
196 //# Enemy functions
197 void EnemyInit();
198 void EnemyDraw();
199 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
200 void EnemyUpdate();
201 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
202 float EnemyGetMaxHealth(Enemy *enemy);
203 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
204 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
205 EnemyId EnemyGetId(Enemy *enemy);
206 Enemy *EnemyTryResolve(EnemyId enemyId);
207 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
208 int EnemyAddDamage(Enemy *enemy, float damage);
209 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
210 int EnemyCount();
211 void EnemyDrawHealthbars(Camera3D camera);
212 
213 //# Tower functions
214 void TowerInit();
215 Tower *TowerGetAt(int16_t x, int16_t y);
216 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
217 Tower *GetTowerByType(uint8_t towerType);
218 int GetTowerCosts(uint8_t towerType);
219 float TowerGetMaxHealth(Tower *tower);
220 void TowerDraw();
221 void TowerUpdate();
222 void TowerDrawHealthBars(Camera3D camera);
223 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
224 
225 //# Particles
226 void ParticleInit();
227 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
228 void ParticleUpdate();
229 void ParticleDraw();
230 
231 //# Projectiles
232 void ProjectileInit();
233 void ProjectileDraw();
234 void ProjectileUpdate();
235 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage);
236 
237 //# Pathfinding map
238 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
239 float PathFindingGetDistance(int mapX, int mapY);
240 Vector2 PathFindingGetGradient(Vector3 world);
241 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
242 void PathFindingMapUpdate();
243 void PathFindingMapDraw();
244 
245 //# UI
246 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
247 
248 //# Level
249 void DrawLevelGround(Level *level);
250 
251 //# variables
252 extern Level *currentLevel;
253 extern Enemy enemies[ENEMY_MAX_COUNT];
254 extern int enemyCount;
255 extern EnemyClassConfig enemyClassConfigs[];
256 
257 extern GUIState guiState;
258 extern GameTime gameTime;
259 extern Tower towers[TOWER_MAX_COUNT];
260 extern int towerCount;
261 
262 extern Texture2D palette, spriteSheet;
263 
264 #endif
  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}, 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 EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522   }
523 }
  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()
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 < towerCount; i++)
131   {
132     Tower *tower = &towers[i];
133     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134     {
135       continue;
136     }
137     int16_t mapX, mapY;
138     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139     // this would not work correctly and needs to be refined to allow towers covering multiple cells
140     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141     // one cell. For now.
142     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143     {
144       continue;
145     }
146     int index = mapY * width + mapX;
147     pathfindingMap.towerIndex[index] = i;
148   }
149 
150   // we start at the castle and add the castle to the queue
151   pathfindingMap.maxDistance = 0.0f;
152   pathfindingNodeQueueCount = 0;
153   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154   PathfindingNode *node = 0;
155   while ((node = PathFindingNodePop()))
156   {
157     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158     {
159       continue;
160     }
161     int index = node->y * width + node->x;
162     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163     {
164       continue;
165     }
166 
167     int deltaX = node->x - node->fromX;
168     int deltaY = node->y - node->fromY;
169     // even if the cell is blocked by a tower, we still may want to store the direction
170     // (though this might not be needed, IDK right now)
171     pathfindingMap.deltaSrc[index].x = (char) deltaX;
172     pathfindingMap.deltaSrc[index].y = (char) deltaY;
173 
174     // we skip nodes that are blocked by towers
175     if (pathfindingMap.towerIndex[index] >= 0)
176     {
177       node->distance += 8.0f;
178     }
179     pathfindingMap.distances[index] = node->distance;
180     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185   }
186 }
187 
188 void PathFindingMapDraw()
189 {
190   float cellSize = pathfindingMap.scale * 0.9f;
191   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192   for (int x = 0; x < pathfindingMap.width; x++)
193   {
194     for (int y = 0; y < pathfindingMap.height; y++)
195     {
196       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200       // animate the distance "wave" to show how the pathfinding algorithm expands
201       // from the castle
202       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203       {
204         color = BLACK;
205       }
206       DrawCube(position, cellSize, 0.1f, cellSize, color);
207     }
208   }
209 }
210 
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213   int16_t mapX, mapY;
214   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215   {
216     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217     return (Vector2){(float)-delta.x, (float)-delta.y};
218   }
219   // fallback to a simple gradient calculation
220   float n = PathFindingGetDistance(mapX, mapY - 1);
221   float s = PathFindingGetDistance(mapX, mapY + 1);
222   float w = PathFindingGetDistance(mapX - 1, mapY);
223   float e = PathFindingGetDistance(mapX + 1, mapY);
224   return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 void ProjectileInit()
  8 {
  9   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 10   {
 11     projectiles[i] = (Projectile){0};
 12   }
 13 }
 14 
 15 void ProjectileDraw()
 16 {
 17   for (int i = 0; i < projectileCount; i++)
 18   {
 19     Projectile projectile = projectiles[i];
 20     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 21     {
 22       continue;
 23     }
 24     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 25     if (transition >= 1.0f)
 26     {
 27       continue;
 28     }
 29     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 30     {
 31       float t = transition + transitionOffset * 0.3f;
 32       if (t > 1.0f)
 33       {
 34         break;
 35       }
 36       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 37       Color color = RED;
 38       if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 39       {
 40         // make tip red but quickly fade to brown
 41         color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 42         // fake a ballista flight path using parabola equation
 43         float parabolaT = t - 0.5f;
 44         parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 45         position.y += 0.15f * parabolaT * projectile.distance;
 46       }
 47 
 48       float size = 0.06f * (transitionOffset + 0.25f);
 49       DrawCube(position, size, size, size, color);
 50     }
 51   }
 52 }
 53 
 54 void ProjectileUpdate()
 55 {
 56   for (int i = 0; i < projectileCount; i++)
 57   {
 58     Projectile *projectile = &projectiles[i];
 59     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 60     {
 61       continue;
 62     }
 63     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 64     if (transition >= 1.0f)
 65     {
 66       projectile->projectileType = PROJECTILE_TYPE_NONE;
 67       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 68       if (enemy)
 69       {
 70         EnemyAddDamage(enemy, projectile->damage);
 71       }
 72       continue;
 73     }
 74   }
 75 }
 76 
 77 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage)
 78 {
 79   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       projectile->projectileType = projectileType;
 85       projectile->shootTime = gameTime.time;
 86       float distance = Vector3Distance(position, target);
 87       projectile->arrivalTime = gameTime.time + distance / speed;
 88       projectile->damage = damage;
 89       projectile->position = position;
 90       projectile->target = target;
 91       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
 92       projectile->distance = distance;
 93       projectile->targetEnemy = EnemyGetId(enemy);
 94       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 95       return projectile;
 96     }
 97   }
 98   return 0;
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 Tower towers[TOWER_MAX_COUNT];
  5 int towerCount = 0;
  6 
  7 Model towerModels[TOWER_TYPE_COUNT];
  8 
  9 // definition of our archer unit
 10 SpriteUnit archerUnit = {
 11     .srcRect = {0, 0, 16, 16},
 12     .offset = {7, 1},
 13     .frameCount = 1,
 14     .frameDuration = 0.0f,
 15     .srcWeaponIdleRect = {16, 0, 6, 16},
 16     .srcWeaponIdleOffset = {8, 0},
 17     .srcWeaponCooldownRect = {22, 0, 11, 16},
 18     .srcWeaponCooldownOffset = {10, 0},
 19 };
 20 
 21 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 22 {
 23   float xScale = flip ? -1.0f : 1.0f;
 24   Camera3D camera = currentLevel->camera;
 25   float size = 0.5f;
 26   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 27   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 28   // we want the sprite to face the camera, so we need to calculate the up vector
 29   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 30   Vector3 up = {0, 1, 0};
 31   Vector3 right = Vector3CrossProduct(forward, up);
 32   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 33 
 34   Rectangle srcRect = unit.srcRect;
 35   if (unit.frameCount > 1)
 36   {
 37     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 38   }
 39   if (flip)
 40   {
 41     srcRect.x += srcRect.width;
 42     srcRect.width = -srcRect.width;
 43   }
 44   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 45 
 46   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 47   {
 48     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 49     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 50     srcRect = unit.srcWeaponCooldownRect;
 51     if (flip)
 52     {
 53       // position.x = flip * scale.x * 0.5f;
 54       srcRect.x += srcRect.width;
 55       srcRect.width = -srcRect.width;
 56       offset.x = scale.x - offset.x;
 57     }
 58     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 59   }
 60   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
 61   {
 62     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
 63     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
 64     srcRect = unit.srcWeaponIdleRect;
 65     if (flip)
 66     {
 67       // position.x = flip * scale.x * 0.5f;
 68       srcRect.x += srcRect.width;
 69       srcRect.width = -srcRect.width;
 70       offset.x = scale.x - offset.x;
 71     }
 72     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 73   }
 74 }
 75 
 76 void TowerInit()
 77 {
 78   for (int i = 0; i < TOWER_MAX_COUNT; i++)
 79   {
 80     towers[i] = (Tower){0};
 81   }
 82   towerCount = 0;
 83 
 84   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
 85   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
 86 
 87   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
 88   {
 89     if (towerModels[i].materials)
 90     {
 91       // assign the palette texture to the material of the model (0 is not used afaik)
 92       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 93     }
 94   }
 95 }
 96 
 97 static void TowerGunUpdate(Tower *tower)
 98 {
 99   if (tower->cooldown <= 0)
100   {
101     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
102     if (enemy)
103     {
104       tower->cooldown = 0.5f;
105       // shoot the enemy; determine future position of the enemy
106       float bulletSpeed = 4.0f;
107       float bulletDamage = 3.0f;
108       Vector2 velocity = enemy->simVelocity;
109       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
110       Vector2 towerPosition = {tower->x, tower->y};
111       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
112       for (int i = 0; i < 8; i++) {
113         velocity = enemy->simVelocity;
114         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
115         float distance = Vector2Distance(towerPosition, futurePosition);
116         float eta2 = distance / bulletSpeed;
117         if (fabs(eta - eta2) < 0.01f) {
118           break;
119         }
120         eta = (eta2 + eta) * 0.5f;
121       }
122       ProjectileTryAdd(PROJECTILE_TYPE_ARROW, enemy, 
123         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
124         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
125         bulletSpeed, bulletDamage);
126       enemy->futureDamage += bulletDamage;
127       tower->lastTargetPosition = futurePosition;
128     }
129   }
130   else
131   {
132     tower->cooldown -= gameTime.deltaTime;
133   }
134 }
135 
136 Tower *TowerGetAt(int16_t x, int16_t y)
137 {
138   for (int i = 0; i < towerCount; i++)
139   {
140     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
141     {
142       return &towers[i];
143     }
144   }
145   return 0;
146 }
147 
148 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
149 {
150   if (towerCount >= TOWER_MAX_COUNT)
151   {
152     return 0;
153   }
154 
155   Tower *tower = TowerGetAt(x, y);
156   if (tower)
157   {
158     return 0;
159   }
160 
161   tower = &towers[towerCount++];
162   tower->x = x;
163   tower->y = y;
164   tower->towerType = towerType;
165   tower->cooldown = 0.0f;
166   tower->damage = 0.0f;
167   return tower;
168 }
169 
170 Tower *GetTowerByType(uint8_t towerType)
171 {
172   for (int i = 0; i < towerCount; i++)
173   {
174     if (towers[i].towerType == towerType)
175     {
176       return &towers[i];
177     }
178   }
179   return 0;
180 }
181 
182 int GetTowerCosts(uint8_t towerType)
183 {
184   switch (towerType)
185   {
186   case TOWER_TYPE_BASE:
187     return 0;
188   case TOWER_TYPE_GUN:
189     return 6;
190   case TOWER_TYPE_WALL:
191     return 2;
192   }
193   return 0;
194 }
195 
196 float TowerGetMaxHealth(Tower *tower)
197 {
198   switch (tower->towerType)
199   {
200   case TOWER_TYPE_BASE:
201     return 10.0f;
202   case TOWER_TYPE_GUN:
203     return 3.0f;
204   case TOWER_TYPE_WALL:
205     return 5.0f;
206   }
207   return 0.0f;
208 }
209 
210 void TowerDraw()
211 {
212   for (int i = 0; i < towerCount; i++)
213   {
214     Tower tower = towers[i];
215     if (tower.towerType == TOWER_TYPE_NONE)
216     {
217       continue;
218     }
219 
220     switch (tower.towerType)
221     {
222     case TOWER_TYPE_GUN:
223       {
224         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
225         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
226         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
227         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
228           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
229       }
230       break;
231     default:
232       if (towerModels[tower.towerType].materials)
233       {
234         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
235       } else {
236         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
237       }
238       break;
239     }
240   }
241 }
242 
243 void TowerUpdate()
244 {
245   for (int i = 0; i < towerCount; i++)
246   {
247     Tower *tower = &towers[i];
248     switch (tower->towerType)
249     {
250     case TOWER_TYPE_GUN:
251       TowerGunUpdate(tower);
252       break;
253     }
254   }
255 }
256 
257 void TowerDrawHealthBars(Camera3D camera)
258 {
259   for (int i = 0; i < towerCount; i++)
260   {
261     Tower *tower = &towers[i];
262     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
263     {
264       continue;
265     }
266     
267     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
268     float maxHealth = TowerGetMaxHealth(tower);
269     float health = maxHealth - tower->damage;
270     float healthRatio = health / maxHealth;
271     
272     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
273   }
274 }
  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
Ooops. The wiggling is out of control 😱! What's going on?!

Random object placement

The objects are now offsetted by a random value:

  1 (float)GetRandomValue(-25,25) / 100.0f

But the graphics are jumping all over the place! This is not what we want here.

The problem is, that we draw the objects every frame and the random value is ... well, random. Every time. That is why they jump each frame and it looks just like noise. We need to make sure that the random value is the same every frame:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 //# Variables
  7 GUIState guiState = {0};
  8 GameTime gameTime = {0};
  9 
 10 Model floorTileAModel = {0};
 11 Model floorTileBModel = {0};
 12 Model grassPatchModel[1] = {0};
 13 
 14 Texture2D palette, spriteSheet;
 15 
 16 Level levels[] = {
 17   [0] = {
 18     .state = LEVEL_STATE_BUILDING,
 19     .initialGold = 20,
 20     .waves[0] = {
 21       .enemyType = ENEMY_TYPE_MINION,
 22       .wave = 0,
 23       .count = 10,
 24       .interval = 2.5f,
 25       .delay = 1.0f,
 26       .spawnPosition = {0, 6},
 27     },
 28     .waves[1] = {
 29       .enemyType = ENEMY_TYPE_MINION,
 30       .wave = 1,
 31       .count = 20,
 32       .interval = 1.5f,
 33       .delay = 1.0f,
 34       .spawnPosition = {0, 6},
 35     },
 36     .waves[2] = {
 37       .enemyType = ENEMY_TYPE_MINION,
 38       .wave = 2,
 39       .count = 30,
 40       .interval = 1.2f,
 41       .delay = 1.0f,
 42       .spawnPosition = {0, 6},
 43     }
 44   },
 45 };
 46 
 47 Level *currentLevel = levels;
 48 
 49 //# Game
 50 
 51 static Model LoadGLBModel(char *filename)
 52 {
 53   Model model = LoadModel(TextFormat("data/%s.glb",filename));
 54   if (model.materialCount > 1)
 55   {
 56     model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 57   }
 58   return model;
 59 }
 60 
 61 void LoadAssets()
 62 {
 63   // load a sprite sheet that contains all units
 64   spriteSheet = LoadTexture("data/spritesheet.png");
 65   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
 66 
 67   // we'll use a palette texture to colorize the all buildings and environment art
 68   palette = LoadTexture("data/palette.png");
 69   // The texture uses gradients on very small space, so we'll enable bilinear filtering
 70   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
 71 
 72   floorTileAModel = LoadGLBModel("floor-tile-a");
 73   floorTileBModel = LoadGLBModel("floor-tile-b");
 74   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
 75 }
 76 
 77 void InitLevel(Level *level)
 78 {
 79   TowerInit();
 80   EnemyInit();
 81   ProjectileInit();
 82   ParticleInit();
 83   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
 84 
 85   level->placementMode = 0;
 86   level->state = LEVEL_STATE_BUILDING;
 87   level->nextState = LEVEL_STATE_NONE;
 88   level->playerGold = level->initialGold;
 89   level->currentWave = 0;
 90 
 91   Camera *camera = &level->camera;
 92   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
 93   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
 94   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
 95   camera->fovy = 10.0f;
 96   camera->projection = CAMERA_ORTHOGRAPHIC;
 97 }
 98 
 99 void DrawLevelHud(Level *level)
100 {
101   const char *text = TextFormat("Gold: %d", level->playerGold);
102   Font font = GetFontDefault();
103   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
104   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
105 }
106 
107 void DrawLevelReportLostWave(Level *level)
108 {
109   BeginMode3D(level->camera);
110   DrawLevelGround(level);
111   TowerDraw();
112   EnemyDraw();
113   ProjectileDraw();
114   ParticleDraw();
115   guiState.isBlocked = 0;
116   EndMode3D();
117 
118   TowerDrawHealthBars(level->camera);
119 
120   const char *text = "Wave lost";
121   int textWidth = MeasureText(text, 20);
122   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
123 
124   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
125   {
126     level->nextState = LEVEL_STATE_RESET;
127   }
128 }
129 
130 int HasLevelNextWave(Level *level)
131 {
132   for (int i = 0; i < 10; i++)
133   {
134     EnemyWave *wave = &level->waves[i];
135     if (wave->wave == level->currentWave)
136     {
137       return 1;
138     }
139   }
140   return 0;
141 }
142 
143 void DrawLevelReportWonWave(Level *level)
144 {
145   BeginMode3D(level->camera);
146   DrawLevelGround(level);
147   TowerDraw();
148   EnemyDraw();
149   ProjectileDraw();
150   ParticleDraw();
151   guiState.isBlocked = 0;
152   EndMode3D();
153 
154   TowerDrawHealthBars(level->camera);
155 
156   const char *text = "Wave won";
157   int textWidth = MeasureText(text, 20);
158   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
159 
160 
161   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
162   {
163     level->nextState = LEVEL_STATE_RESET;
164   }
165 
166   if (HasLevelNextWave(level))
167   {
168     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
169     {
170       level->nextState = LEVEL_STATE_BUILDING;
171     }
172   }
173   else {
174     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
175     {
176       level->nextState = LEVEL_STATE_WON_LEVEL;
177     }
178   }
179 }
180 
181 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
182 {
183   static ButtonState buttonStates[8] = {0};
184   int cost = GetTowerCosts(towerType);
185   const char *text = TextFormat("%s: %d", name, cost);
186   buttonStates[towerType].isSelected = level->placementMode == towerType;
187   buttonStates[towerType].isDisabled = level->playerGold < cost;
188   if (Button(text, x, y, width, height, &buttonStates[towerType]))
189   {
190     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
191   }
192 }
193 
194 void DrawLevelGround(Level *level)
195 {
196   // draw checkerboard ground pattern
197   for (int x = -5; x <= 5; x += 1)
198   {
199     for (int y = -5; y <= 5; y += 1)
200     {
201       Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
202       DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
203     }
204   }
205 
206   // draw grass patches around the edges
207   const int layerCount = 2;
208 SetRandomSeed(123);
209 for (int layer = 0; layer < layerCount; layer++) 210 { 211 int layerPos = 6 + layer; 212 for (int x = -6 + layer; x <= 6 + layer; x += 1) 213 { 214 DrawModel(grassPatchModel[0], 215 (Vector3){x + (float)GetRandomValue(-25,25) / 100.0f, 0.0f, -layerPos + (float)GetRandomValue(-25,25) / 100.0f}, 216 1.0f, WHITE); 217 DrawModel(grassPatchModel[0], 218 (Vector3){x + (float)GetRandomValue(-25,25) / 100.0f, 0.0f, layerPos + (float)GetRandomValue(-25,25) / 100.0f}, 219 1.0f, WHITE); 220 } 221 222 for (int z = -5 + layer; z <= 5 + layer; z += 1) 223 { 224 DrawModel(grassPatchModel[0], 225 (Vector3){-layerPos + (float)GetRandomValue(-25,25) / 100.0f, 0.0f, z + (float)GetRandomValue(-25,25) / 100.0f}, 226 1.0f, WHITE); 227 DrawModel(grassPatchModel[0], 228 (Vector3){layerPos + (float)GetRandomValue(-25,25) / 100.0f, 0.0f, z + (float)GetRandomValue(-25,25) / 100.0f}, 229 1.0f, WHITE); 230 } 231 } 232 } 233 234 void DrawLevelBuildingState(Level *level) 235 { 236 BeginMode3D(level->camera); 237 DrawLevelGround(level); 238 TowerDraw(); 239 EnemyDraw(); 240 ProjectileDraw(); 241 ParticleDraw(); 242 243 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 244 float planeDistance = ray.position.y / -ray.direction.y; 245 float planeX = ray.direction.x * planeDistance + ray.position.x; 246 float planeY = ray.direction.z * planeDistance + ray.position.z; 247 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 248 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 249 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5) 250 { 251 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 252 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 253 { 254 if (TowerTryAdd(level->placementMode, mapX, mapY)) 255 { 256 level->playerGold -= GetTowerCosts(level->placementMode); 257 level->placementMode = TOWER_TYPE_NONE; 258 } 259 } 260 } 261 262 guiState.isBlocked = 0; 263 264 EndMode3D(); 265 266 TowerDrawHealthBars(level->camera); 267 268 static ButtonState buildWallButtonState = {0}; 269 static ButtonState buildGunButtonState = {0}; 270 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 271 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN; 272 273 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 274 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_GUN, "Archer"); 275 276 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 277 { 278 level->nextState = LEVEL_STATE_RESET; 279 } 280 281 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 282 { 283 level->nextState = LEVEL_STATE_BATTLE; 284 } 285 286 const char *text = "Building phase"; 287 int textWidth = MeasureText(text, 20); 288 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 289 } 290 291 void InitBattleStateConditions(Level *level) 292 { 293 level->state = LEVEL_STATE_BATTLE; 294 level->nextState = LEVEL_STATE_NONE; 295 level->waveEndTimer = 0.0f; 296 for (int i = 0; i < 10; i++) 297 { 298 EnemyWave *wave = &level->waves[i]; 299 wave->spawned = 0; 300 wave->timeToSpawnNext = wave->delay; 301 } 302 } 303 304 void DrawLevelBattleState(Level *level) 305 { 306 BeginMode3D(level->camera); 307 DrawLevelGround(level); 308 TowerDraw(); 309 EnemyDraw(); 310 ProjectileDraw(); 311 ParticleDraw(); 312 guiState.isBlocked = 0; 313 EndMode3D(); 314 315 EnemyDrawHealthbars(level->camera); 316 TowerDrawHealthBars(level->camera); 317 318 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 319 { 320 level->nextState = LEVEL_STATE_RESET; 321 } 322 323 int maxCount = 0; 324 int remainingCount = 0; 325 for (int i = 0; i < 10; i++) 326 { 327 EnemyWave *wave = &level->waves[i]; 328 if (wave->wave != level->currentWave) 329 { 330 continue; 331 } 332 maxCount += wave->count; 333 remainingCount += wave->count - wave->spawned; 334 } 335 int aliveCount = EnemyCount(); 336 remainingCount += aliveCount; 337 338 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 339 int textWidth = MeasureText(text, 20); 340 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 341 } 342 343 void DrawLevel(Level *level) 344 { 345 switch (level->state) 346 { 347 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 348 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 349 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 350 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 351 default: break; 352 } 353 354 DrawLevelHud(level); 355 } 356 357 void UpdateLevel(Level *level) 358 { 359 if (level->state == LEVEL_STATE_BATTLE) 360 { 361 int activeWaves = 0; 362 for (int i = 0; i < 10; i++) 363 { 364 EnemyWave *wave = &level->waves[i]; 365 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 366 { 367 continue; 368 } 369 activeWaves++; 370 wave->timeToSpawnNext -= gameTime.deltaTime; 371 if (wave->timeToSpawnNext <= 0.0f) 372 { 373 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 374 if (enemy) 375 { 376 wave->timeToSpawnNext = wave->interval; 377 wave->spawned++; 378 } 379 } 380 } 381 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 382 level->waveEndTimer += gameTime.deltaTime; 383 if (level->waveEndTimer >= 2.0f) 384 { 385 level->nextState = LEVEL_STATE_LOST_WAVE; 386 } 387 } 388 else if (activeWaves == 0 && EnemyCount() == 0) 389 { 390 level->waveEndTimer += gameTime.deltaTime; 391 if (level->waveEndTimer >= 2.0f) 392 { 393 level->nextState = LEVEL_STATE_WON_WAVE; 394 } 395 } 396 } 397 398 PathFindingMapUpdate(); 399 EnemyUpdate(); 400 TowerUpdate(); 401 ProjectileUpdate(); 402 ParticleUpdate(); 403 404 if (level->nextState == LEVEL_STATE_RESET) 405 { 406 InitLevel(level); 407 } 408 409 if (level->nextState == LEVEL_STATE_BATTLE) 410 { 411 InitBattleStateConditions(level); 412 } 413 414 if (level->nextState == LEVEL_STATE_WON_WAVE) 415 { 416 level->currentWave++; 417 level->state = LEVEL_STATE_WON_WAVE; 418 } 419 420 if (level->nextState == LEVEL_STATE_LOST_WAVE) 421 { 422 level->state = LEVEL_STATE_LOST_WAVE; 423 } 424 425 if (level->nextState == LEVEL_STATE_BUILDING) 426 { 427 level->state = LEVEL_STATE_BUILDING; 428 } 429 430 if (level->nextState == LEVEL_STATE_WON_LEVEL) 431 { 432 // make something of this later 433 InitLevel(level); 434 } 435 436 level->nextState = LEVEL_STATE_NONE; 437 } 438 439 float nextSpawnTime = 0.0f; 440 441 void ResetGame() 442 { 443 InitLevel(currentLevel); 444 } 445 446 void InitGame() 447 { 448 TowerInit(); 449 EnemyInit(); 450 ProjectileInit(); 451 ParticleInit(); 452 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 453 454 currentLevel = levels; 455 InitLevel(currentLevel); 456 } 457 458 //# Immediate GUI functions 459 460 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 461 { 462 const float healthBarHeight = 6.0f; 463 const float healthBarOffset = 15.0f; 464 const float inset = 2.0f; 465 const float innerWidth = healthBarWidth - inset * 2; 466 const float innerHeight = healthBarHeight - inset * 2; 467 468 Vector2 screenPos = GetWorldToScreen(position, camera); 469 float centerX = screenPos.x - healthBarWidth * 0.5f; 470 float topY = screenPos.y - healthBarOffset; 471 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 472 float healthWidth = innerWidth * healthRatio; 473 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 474 } 475 476 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 477 { 478 Rectangle bounds = {x, y, width, height}; 479 int isPressed = 0; 480 int isSelected = state && state->isSelected; 481 int isDisabled = state && state->isDisabled; 482 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 483 { 484 Color color = isSelected ? DARKGRAY : GRAY; 485 DrawRectangle(x, y, width, height, color); 486 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 487 { 488 isPressed = 1; 489 } 490 guiState.isBlocked = 1; 491 } 492 else 493 { 494 Color color = isSelected ? WHITE : LIGHTGRAY; 495 DrawRectangle(x, y, width, height, color); 496 } 497 Font font = GetFontDefault(); 498 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 499 Color textColor = isDisabled ? GRAY : BLACK; 500 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 501 return isPressed; 502 } 503 504 //# Main game loop 505 506 void GameUpdate() 507 { 508 float dt = GetFrameTime(); 509 // cap maximum delta time to 0.1 seconds to prevent large time steps 510 if (dt > 0.1f) dt = 0.1f; 511 gameTime.time += dt; 512 gameTime.deltaTime = dt; 513 514 UpdateLevel(currentLevel); 515 } 516 517 int main(void) 518 { 519 int screenWidth, screenHeight; 520 GetPreferredSize(&screenWidth, &screenHeight); 521 InitWindow(screenWidth, screenHeight, "Tower defense"); 522 SetTargetFPS(30); 523 524 LoadAssets(); 525 InitGame(); 526 527 while (!WindowShouldClose()) 528 { 529 if (IsPaused()) { 530 // canvas is not visible in browser - do nothing 531 continue; 532 } 533 534 BeginDrawing(); 535 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 536 537 GameUpdate(); 538 DrawLevel(currentLevel); 539 540 EndDrawing(); 541 } 542 543 CloseWindow(); 544 545 return 0; 546 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
 30 #define TOWER_TYPE_NONE 0
 31 #define TOWER_TYPE_BASE 1
 32 #define TOWER_TYPE_GUN 2
 33 #define TOWER_TYPE_WALL 3
 34 #define TOWER_TYPE_COUNT 4
 35 
 36 typedef struct Tower
 37 {
 38   int16_t x, y;
 39   uint8_t towerType;
 40   Vector2 lastTargetPosition;
 41   float cooldown;
 42   float damage;
 43 } Tower;
 44 
 45 typedef struct GameTime
 46 {
 47   float time;
 48   float deltaTime;
 49 } GameTime;
 50 
 51 typedef struct ButtonState {
 52   char isSelected;
 53   char isDisabled;
 54 } ButtonState;
 55 
 56 typedef struct GUIState {
 57   int isBlocked;
 58 } GUIState;
 59 
 60 typedef enum LevelState
 61 {
 62   LEVEL_STATE_NONE,
 63   LEVEL_STATE_BUILDING,
 64   LEVEL_STATE_BATTLE,
 65   LEVEL_STATE_WON_WAVE,
 66   LEVEL_STATE_LOST_WAVE,
 67   LEVEL_STATE_WON_LEVEL,
 68   LEVEL_STATE_RESET,
 69 } LevelState;
 70 
 71 typedef struct EnemyWave {
 72   uint8_t enemyType;
 73   uint8_t wave;
 74   uint16_t count;
 75   float interval;
 76   float delay;
 77   Vector2 spawnPosition;
 78 
 79   uint16_t spawned;
 80   float timeToSpawnNext;
 81 } EnemyWave;
 82 
 83 typedef struct Level
 84 {
 85   LevelState state;
 86   LevelState nextState;
 87   Camera3D camera;
 88   int placementMode;
 89 
 90   int initialGold;
 91   int playerGold;
 92 
 93   EnemyWave waves[10];
 94   int currentWave;
 95   float waveEndTimer;
 96 } Level;
 97 
 98 typedef struct DeltaSrc
 99 {
100   char x, y;
101 } DeltaSrc;
102 
103 typedef struct PathfindingMap
104 {
105   int width, height;
106   float scale;
107   float *distances;
108   long *towerIndex; 
109   DeltaSrc *deltaSrc;
110   float maxDistance;
111   Matrix toMapSpace;
112   Matrix toWorldSpace;
113 } PathfindingMap;
114 
115 // when we execute the pathfinding algorithm, we need to store the active nodes
116 // in a queue. Each node has a position, a distance from the start, and the
117 // position of the node that we came from.
118 typedef struct PathfindingNode
119 {
120   int16_t x, y, fromX, fromY;
121   float distance;
122 } PathfindingNode;
123 
124 typedef struct EnemyId
125 {
126   uint16_t index;
127   uint16_t generation;
128 } EnemyId;
129 
130 typedef struct EnemyClassConfig
131 {
132   float speed;
133   float health;
134   float radius;
135   float maxAcceleration;
136   float requiredContactTime;
137   float explosionDamage;
138   float explosionRange;
139   float explosionPushbackPower;
140   int goldValue;
141 } EnemyClassConfig;
142 
143 typedef struct Enemy
144 {
145   int16_t currentX, currentY;
146   int16_t nextX, nextY;
147   Vector2 simPosition;
148   Vector2 simVelocity;
149   uint16_t generation;
150   float walkedDistance;
151   float startMovingTime;
152   float damage, futureDamage;
153   float contactTime;
154   uint8_t enemyType;
155   uint8_t movePathCount;
156   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
157 } Enemy;
158 
159 // a unit that uses sprites to be drawn
160 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
161 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
162 typedef struct SpriteUnit
163 {
164   Rectangle srcRect;
165   Vector2 offset;
166   int frameCount;
167   float frameDuration;
168   Rectangle srcWeaponIdleRect;
169   Vector2 srcWeaponIdleOffset;
170   Rectangle srcWeaponCooldownRect;
171   Vector2 srcWeaponCooldownOffset;
172 } SpriteUnit;
173 
174 #define PROJECTILE_MAX_COUNT 1200
175 #define PROJECTILE_TYPE_NONE 0
176 #define PROJECTILE_TYPE_ARROW 1
177 
178 typedef struct Projectile
179 {
180   uint8_t projectileType;
181   float shootTime;
182   float arrivalTime;
183   float distance;
184   float damage;
185   Vector3 position;
186   Vector3 target;
187   Vector3 directionNormal;
188   EnemyId targetEnemy;
189 } Projectile;
190 
191 //# Function declarations
192 float TowerGetMaxHealth(Tower *tower);
193 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
194 int EnemyAddDamage(Enemy *enemy, float damage);
195 
196 //# Enemy functions
197 void EnemyInit();
198 void EnemyDraw();
199 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
200 void EnemyUpdate();
201 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
202 float EnemyGetMaxHealth(Enemy *enemy);
203 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
204 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
205 EnemyId EnemyGetId(Enemy *enemy);
206 Enemy *EnemyTryResolve(EnemyId enemyId);
207 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
208 int EnemyAddDamage(Enemy *enemy, float damage);
209 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
210 int EnemyCount();
211 void EnemyDrawHealthbars(Camera3D camera);
212 
213 //# Tower functions
214 void TowerInit();
215 Tower *TowerGetAt(int16_t x, int16_t y);
216 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
217 Tower *GetTowerByType(uint8_t towerType);
218 int GetTowerCosts(uint8_t towerType);
219 float TowerGetMaxHealth(Tower *tower);
220 void TowerDraw();
221 void TowerUpdate();
222 void TowerDrawHealthBars(Camera3D camera);
223 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
224 
225 //# Particles
226 void ParticleInit();
227 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
228 void ParticleUpdate();
229 void ParticleDraw();
230 
231 //# Projectiles
232 void ProjectileInit();
233 void ProjectileDraw();
234 void ProjectileUpdate();
235 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage);
236 
237 //# Pathfinding map
238 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
239 float PathFindingGetDistance(int mapX, int mapY);
240 Vector2 PathFindingGetGradient(Vector3 world);
241 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
242 void PathFindingMapUpdate();
243 void PathFindingMapDraw();
244 
245 //# UI
246 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
247 
248 //# Level
249 void DrawLevelGround(Level *level);
250 
251 //# variables
252 extern Level *currentLevel;
253 extern Enemy enemies[ENEMY_MAX_COUNT];
254 extern int enemyCount;
255 extern EnemyClassConfig enemyClassConfigs[];
256 
257 extern GUIState guiState;
258 extern GameTime gameTime;
259 extern Tower towers[TOWER_MAX_COUNT];
260 extern int towerCount;
261 
262 extern Texture2D palette, spriteSheet;
263 
264 #endif
  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}, 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 EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522   }
523 }
  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()
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 < towerCount; i++)
131   {
132     Tower *tower = &towers[i];
133     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134     {
135       continue;
136     }
137     int16_t mapX, mapY;
138     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139     // this would not work correctly and needs to be refined to allow towers covering multiple cells
140     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141     // one cell. For now.
142     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143     {
144       continue;
145     }
146     int index = mapY * width + mapX;
147     pathfindingMap.towerIndex[index] = i;
148   }
149 
150   // we start at the castle and add the castle to the queue
151   pathfindingMap.maxDistance = 0.0f;
152   pathfindingNodeQueueCount = 0;
153   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154   PathfindingNode *node = 0;
155   while ((node = PathFindingNodePop()))
156   {
157     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158     {
159       continue;
160     }
161     int index = node->y * width + node->x;
162     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163     {
164       continue;
165     }
166 
167     int deltaX = node->x - node->fromX;
168     int deltaY = node->y - node->fromY;
169     // even if the cell is blocked by a tower, we still may want to store the direction
170     // (though this might not be needed, IDK right now)
171     pathfindingMap.deltaSrc[index].x = (char) deltaX;
172     pathfindingMap.deltaSrc[index].y = (char) deltaY;
173 
174     // we skip nodes that are blocked by towers
175     if (pathfindingMap.towerIndex[index] >= 0)
176     {
177       node->distance += 8.0f;
178     }
179     pathfindingMap.distances[index] = node->distance;
180     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185   }
186 }
187 
188 void PathFindingMapDraw()
189 {
190   float cellSize = pathfindingMap.scale * 0.9f;
191   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192   for (int x = 0; x < pathfindingMap.width; x++)
193   {
194     for (int y = 0; y < pathfindingMap.height; y++)
195     {
196       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200       // animate the distance "wave" to show how the pathfinding algorithm expands
201       // from the castle
202       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203       {
204         color = BLACK;
205       }
206       DrawCube(position, cellSize, 0.1f, cellSize, color);
207     }
208   }
209 }
210 
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213   int16_t mapX, mapY;
214   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215   {
216     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217     return (Vector2){(float)-delta.x, (float)-delta.y};
218   }
219   // fallback to a simple gradient calculation
220   float n = PathFindingGetDistance(mapX, mapY - 1);
221   float s = PathFindingGetDistance(mapX, mapY + 1);
222   float w = PathFindingGetDistance(mapX - 1, mapY);
223   float e = PathFindingGetDistance(mapX + 1, mapY);
224   return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 void ProjectileInit()
  8 {
  9   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 10   {
 11     projectiles[i] = (Projectile){0};
 12   }
 13 }
 14 
 15 void ProjectileDraw()
 16 {
 17   for (int i = 0; i < projectileCount; i++)
 18   {
 19     Projectile projectile = projectiles[i];
 20     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 21     {
 22       continue;
 23     }
 24     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 25     if (transition >= 1.0f)
 26     {
 27       continue;
 28     }
 29     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 30     {
 31       float t = transition + transitionOffset * 0.3f;
 32       if (t > 1.0f)
 33       {
 34         break;
 35       }
 36       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 37       Color color = RED;
 38       if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 39       {
 40         // make tip red but quickly fade to brown
 41         color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 42         // fake a ballista flight path using parabola equation
 43         float parabolaT = t - 0.5f;
 44         parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 45         position.y += 0.15f * parabolaT * projectile.distance;
 46       }
 47 
 48       float size = 0.06f * (transitionOffset + 0.25f);
 49       DrawCube(position, size, size, size, color);
 50     }
 51   }
 52 }
 53 
 54 void ProjectileUpdate()
 55 {
 56   for (int i = 0; i < projectileCount; i++)
 57   {
 58     Projectile *projectile = &projectiles[i];
 59     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 60     {
 61       continue;
 62     }
 63     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 64     if (transition >= 1.0f)
 65     {
 66       projectile->projectileType = PROJECTILE_TYPE_NONE;
 67       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 68       if (enemy)
 69       {
 70         EnemyAddDamage(enemy, projectile->damage);
 71       }
 72       continue;
 73     }
 74   }
 75 }
 76 
 77 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage)
 78 {
 79   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       projectile->projectileType = projectileType;
 85       projectile->shootTime = gameTime.time;
 86       float distance = Vector3Distance(position, target);
 87       projectile->arrivalTime = gameTime.time + distance / speed;
 88       projectile->damage = damage;
 89       projectile->position = position;
 90       projectile->target = target;
 91       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
 92       projectile->distance = distance;
 93       projectile->targetEnemy = EnemyGetId(enemy);
 94       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 95       return projectile;
 96     }
 97   }
 98   return 0;
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 Tower towers[TOWER_MAX_COUNT];
  5 int towerCount = 0;
  6 
  7 Model towerModels[TOWER_TYPE_COUNT];
  8 
  9 // definition of our archer unit
 10 SpriteUnit archerUnit = {
 11     .srcRect = {0, 0, 16, 16},
 12     .offset = {7, 1},
 13     .frameCount = 1,
 14     .frameDuration = 0.0f,
 15     .srcWeaponIdleRect = {16, 0, 6, 16},
 16     .srcWeaponIdleOffset = {8, 0},
 17     .srcWeaponCooldownRect = {22, 0, 11, 16},
 18     .srcWeaponCooldownOffset = {10, 0},
 19 };
 20 
 21 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 22 {
 23   float xScale = flip ? -1.0f : 1.0f;
 24   Camera3D camera = currentLevel->camera;
 25   float size = 0.5f;
 26   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 27   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 28   // we want the sprite to face the camera, so we need to calculate the up vector
 29   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 30   Vector3 up = {0, 1, 0};
 31   Vector3 right = Vector3CrossProduct(forward, up);
 32   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 33 
 34   Rectangle srcRect = unit.srcRect;
 35   if (unit.frameCount > 1)
 36   {
 37     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 38   }
 39   if (flip)
 40   {
 41     srcRect.x += srcRect.width;
 42     srcRect.width = -srcRect.width;
 43   }
 44   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 45 
 46   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 47   {
 48     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 49     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 50     srcRect = unit.srcWeaponCooldownRect;
 51     if (flip)
 52     {
 53       // position.x = flip * scale.x * 0.5f;
 54       srcRect.x += srcRect.width;
 55       srcRect.width = -srcRect.width;
 56       offset.x = scale.x - offset.x;
 57     }
 58     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 59   }
 60   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
 61   {
 62     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
 63     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
 64     srcRect = unit.srcWeaponIdleRect;
 65     if (flip)
 66     {
 67       // position.x = flip * scale.x * 0.5f;
 68       srcRect.x += srcRect.width;
 69       srcRect.width = -srcRect.width;
 70       offset.x = scale.x - offset.x;
 71     }
 72     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 73   }
 74 }
 75 
 76 void TowerInit()
 77 {
 78   for (int i = 0; i < TOWER_MAX_COUNT; i++)
 79   {
 80     towers[i] = (Tower){0};
 81   }
 82   towerCount = 0;
 83 
 84   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
 85   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
 86 
 87   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
 88   {
 89     if (towerModels[i].materials)
 90     {
 91       // assign the palette texture to the material of the model (0 is not used afaik)
 92       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 93     }
 94   }
 95 }
 96 
 97 static void TowerGunUpdate(Tower *tower)
 98 {
 99   if (tower->cooldown <= 0)
100   {
101     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
102     if (enemy)
103     {
104       tower->cooldown = 0.5f;
105       // shoot the enemy; determine future position of the enemy
106       float bulletSpeed = 4.0f;
107       float bulletDamage = 3.0f;
108       Vector2 velocity = enemy->simVelocity;
109       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
110       Vector2 towerPosition = {tower->x, tower->y};
111       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
112       for (int i = 0; i < 8; i++) {
113         velocity = enemy->simVelocity;
114         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
115         float distance = Vector2Distance(towerPosition, futurePosition);
116         float eta2 = distance / bulletSpeed;
117         if (fabs(eta - eta2) < 0.01f) {
118           break;
119         }
120         eta = (eta2 + eta) * 0.5f;
121       }
122       ProjectileTryAdd(PROJECTILE_TYPE_ARROW, enemy, 
123         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
124         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
125         bulletSpeed, bulletDamage);
126       enemy->futureDamage += bulletDamage;
127       tower->lastTargetPosition = futurePosition;
128     }
129   }
130   else
131   {
132     tower->cooldown -= gameTime.deltaTime;
133   }
134 }
135 
136 Tower *TowerGetAt(int16_t x, int16_t y)
137 {
138   for (int i = 0; i < towerCount; i++)
139   {
140     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
141     {
142       return &towers[i];
143     }
144   }
145   return 0;
146 }
147 
148 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
149 {
150   if (towerCount >= TOWER_MAX_COUNT)
151   {
152     return 0;
153   }
154 
155   Tower *tower = TowerGetAt(x, y);
156   if (tower)
157   {
158     return 0;
159   }
160 
161   tower = &towers[towerCount++];
162   tower->x = x;
163   tower->y = y;
164   tower->towerType = towerType;
165   tower->cooldown = 0.0f;
166   tower->damage = 0.0f;
167   return tower;
168 }
169 
170 Tower *GetTowerByType(uint8_t towerType)
171 {
172   for (int i = 0; i < towerCount; i++)
173   {
174     if (towers[i].towerType == towerType)
175     {
176       return &towers[i];
177     }
178   }
179   return 0;
180 }
181 
182 int GetTowerCosts(uint8_t towerType)
183 {
184   switch (towerType)
185   {
186   case TOWER_TYPE_BASE:
187     return 0;
188   case TOWER_TYPE_GUN:
189     return 6;
190   case TOWER_TYPE_WALL:
191     return 2;
192   }
193   return 0;
194 }
195 
196 float TowerGetMaxHealth(Tower *tower)
197 {
198   switch (tower->towerType)
199   {
200   case TOWER_TYPE_BASE:
201     return 10.0f;
202   case TOWER_TYPE_GUN:
203     return 3.0f;
204   case TOWER_TYPE_WALL:
205     return 5.0f;
206   }
207   return 0.0f;
208 }
209 
210 void TowerDraw()
211 {
212   for (int i = 0; i < towerCount; i++)
213   {
214     Tower tower = towers[i];
215     if (tower.towerType == TOWER_TYPE_NONE)
216     {
217       continue;
218     }
219 
220     switch (tower.towerType)
221     {
222     case TOWER_TYPE_GUN:
223       {
224         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
225         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
226         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
227         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
228           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
229       }
230       break;
231     default:
232       if (towerModels[tower.towerType].materials)
233       {
234         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
235       } else {
236         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
237       }
238       break;
239     }
240   }
241 }
242 
243 void TowerUpdate()
244 {
245   for (int i = 0; i < towerCount; i++)
246   {
247     Tower *tower = &towers[i];
248     switch (tower->towerType)
249     {
250     case TOWER_TYPE_GUN:
251       TowerGunUpdate(tower);
252       break;
253     }
254   }
255 }
256 
257 void TowerDrawHealthBars(Camera3D camera)
258 {
259   for (int i = 0; i < towerCount; i++)
260   {
261     Tower *tower = &towers[i];
262     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
263     {
264       continue;
265     }
266     
267     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
268     float maxHealth = TowerGetMaxHealth(tower);
269     float health = maxHealth - tower->damage;
270     float healthRatio = health / maxHealth;
271     
272     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
273   }
274 }
  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
Whew! The grass is now staying in place 😮‍💨

By calling SetRandomSeed(123) we make sure that the random values are the same every frame. So the 3rd call to GetRandomValue will always return the same value. Hardly random, but that's why it is called a pseudo-random number generator, or PRNG: It's a deterministic sequence of numbers based on a seed. The seed here is 123. Deterministic means that when repeating the same actions, the results will be the same. So even when the application is restarted or loaded on a different machine, the sequence of random numbers will be the same (unless the PRNG implementation is OS dependent, then it will produce different looks).

The code we injected has however side effects that could later cause highly annoying problems:

By setting the seed in the draw function, we no longer get random numbers when we need them for our game logic! This is a critical problem! If this happens in a big project, it can be very difficult to find the reason why the game behaves the same for every action that is supposed to be random. When you call SetRandomSeed, you have to take care that you restore the randomness behavior after you are done with the deterministic random number generation sequence. This is a very important point to keep in mind when working with random numbers in games.

So how do we fix this? There is no way in raylib to obtain the current seed.

But this is not a problem: We only need a random seed. Given the current random generation sequence is still random, we can obtain a seed for later use by calling GetRandomValue(0, 0xfffffff) before we set the seed:

  1   int oldSeed = GetRandomValue(0, 0xfffffff);
  2   SetRandomSeed(123);
  3   ...
  4   SetRandomSeed(oldSeed);

Now we have a random seed for the next time we need it.

I can not stress this enough that whenever a function has a global state, you have to be aware that any call you do will have side effects that need to be considered. Most raylib functions don't have such side effects, though some do have internal states like caches, it does not affect the behavior of the function calls.

The random number generator is one of the few functions that have a global state where this is intentional, but for beginners and even experienced developers, it is not always obvious that this has to be taken care of.

I also want to mention that there are other ways to randomize the seed of the PRNG. The way I did it here is the most simple and efficient way to do it.

Another popular approach is to produce a random seed is to use the current time as a seed, but this has two quite obvious drawbacks: The first is that the seed is not really random because it is based on a predictable value (time). For game functions, this tends to be not a huge problem, but for cryptographic functions, this can introduce security issues (though cryptographic library usually don't use default PRNGs). The bigger problem for games is that the time function call is way more expensive than requesting a random number from the PRNG.

There is even a hidden need to use the previous PRNG state to produce a future seed: It stays deterministic. This is not only a good thing for debugging and testing. In fact, for procedural generated content, this is even critical:

Imagine you have a function A that does some deterministic procedural random generation and you call another function B. This function B is now also doing some procedural random generation. If B does not produce a deterministic new seed for the PRNG after it has finished, A will no longer produce deterministic results and you broke the chain of deterministic procedural random generation.

Speaking of determinism: At the moment, when we restart the level, the decorations look the same every time. This is because the seed is using a fixed value. We can change this by storing a random seed in the level struct and using this seed to set the random seed. This way, each level will have a different look:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 //# Variables
  7 GUIState guiState = {0};
  8 GameTime gameTime = {0};
  9 
 10 Model floorTileAModel = {0};
 11 Model floorTileBModel = {0};
 12 Model grassPatchModel[1] = {0};
 13 
 14 Texture2D palette, spriteSheet;
 15 
 16 Level levels[] = {
 17   [0] = {
 18     .state = LEVEL_STATE_BUILDING,
 19     .initialGold = 20,
 20     .waves[0] = {
 21       .enemyType = ENEMY_TYPE_MINION,
 22       .wave = 0,
 23       .count = 10,
 24       .interval = 2.5f,
 25       .delay = 1.0f,
 26       .spawnPosition = {0, 6},
 27     },
 28     .waves[1] = {
 29       .enemyType = ENEMY_TYPE_MINION,
 30       .wave = 1,
 31       .count = 20,
 32       .interval = 1.5f,
 33       .delay = 1.0f,
 34       .spawnPosition = {0, 6},
 35     },
 36     .waves[2] = {
 37       .enemyType = ENEMY_TYPE_MINION,
 38       .wave = 2,
 39       .count = 30,
 40       .interval = 1.2f,
 41       .delay = 1.0f,
 42       .spawnPosition = {0, 6},
 43     }
 44   },
 45 };
 46 
 47 Level *currentLevel = levels;
 48 
 49 //# Game
 50 
 51 static Model LoadGLBModel(char *filename)
 52 {
 53   Model model = LoadModel(TextFormat("data/%s.glb",filename));
 54   if (model.materialCount > 1)
 55   {
 56     model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 57   }
 58   return model;
 59 }
 60 
 61 void LoadAssets()
 62 {
 63   // load a sprite sheet that contains all units
 64   spriteSheet = LoadTexture("data/spritesheet.png");
 65   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
 66 
 67   // we'll use a palette texture to colorize the all buildings and environment art
 68   palette = LoadTexture("data/palette.png");
 69   // The texture uses gradients on very small space, so we'll enable bilinear filtering
 70   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
 71 
 72   floorTileAModel = LoadGLBModel("floor-tile-a");
 73   floorTileBModel = LoadGLBModel("floor-tile-b");
 74   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
 75 }
 76 
 77 void InitLevel(Level *level)
 78 {
79 level->seed = (int)(GetTime() * 100.0f); 80
81 TowerInit(); 82 EnemyInit(); 83 ProjectileInit(); 84 ParticleInit(); 85 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 86 87 level->placementMode = 0; 88 level->state = LEVEL_STATE_BUILDING; 89 level->nextState = LEVEL_STATE_NONE; 90 level->playerGold = level->initialGold; 91 level->currentWave = 0; 92 93 Camera *camera = &level->camera; 94 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 95 camera->target = (Vector3){0.0f, 0.0f, 0.0f}; 96 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 97 camera->fovy = 10.0f; 98 camera->projection = CAMERA_ORTHOGRAPHIC; 99 } 100 101 void DrawLevelHud(Level *level) 102 { 103 const char *text = TextFormat("Gold: %d", level->playerGold); 104 Font font = GetFontDefault(); 105 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 106 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 107 } 108 109 void DrawLevelReportLostWave(Level *level) 110 { 111 BeginMode3D(level->camera); 112 DrawLevelGround(level); 113 TowerDraw(); 114 EnemyDraw(); 115 ProjectileDraw(); 116 ParticleDraw(); 117 guiState.isBlocked = 0; 118 EndMode3D(); 119 120 TowerDrawHealthBars(level->camera); 121 122 const char *text = "Wave lost"; 123 int textWidth = MeasureText(text, 20); 124 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 125 126 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 127 { 128 level->nextState = LEVEL_STATE_RESET; 129 } 130 } 131 132 int HasLevelNextWave(Level *level) 133 { 134 for (int i = 0; i < 10; i++) 135 { 136 EnemyWave *wave = &level->waves[i]; 137 if (wave->wave == level->currentWave) 138 { 139 return 1; 140 } 141 } 142 return 0; 143 } 144 145 void DrawLevelReportWonWave(Level *level) 146 { 147 BeginMode3D(level->camera); 148 DrawLevelGround(level); 149 TowerDraw(); 150 EnemyDraw(); 151 ProjectileDraw(); 152 ParticleDraw(); 153 guiState.isBlocked = 0; 154 EndMode3D(); 155 156 TowerDrawHealthBars(level->camera); 157 158 const char *text = "Wave won"; 159 int textWidth = MeasureText(text, 20); 160 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 161 162 163 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 164 { 165 level->nextState = LEVEL_STATE_RESET; 166 } 167 168 if (HasLevelNextWave(level)) 169 { 170 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 171 { 172 level->nextState = LEVEL_STATE_BUILDING; 173 } 174 } 175 else { 176 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 177 { 178 level->nextState = LEVEL_STATE_WON_LEVEL; 179 } 180 } 181 } 182 183 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 184 { 185 static ButtonState buttonStates[8] = {0}; 186 int cost = GetTowerCosts(towerType); 187 const char *text = TextFormat("%s: %d", name, cost); 188 buttonStates[towerType].isSelected = level->placementMode == towerType; 189 buttonStates[towerType].isDisabled = level->playerGold < cost; 190 if (Button(text, x, y, width, height, &buttonStates[towerType])) 191 { 192 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 193 } 194 } 195 196 void DrawLevelGround(Level *level) 197 { 198 // draw checkerboard ground pattern 199 for (int x = -5; x <= 5; x += 1) 200 { 201 for (int y = -5; y <= 5; y += 1) 202 { 203 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 204 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 205 } 206 } 207 208 // draw grass patches around the edges 209 const int layerCount = 2;
210 unsigned int oldSeed = GetRandomValue(0, 0xfffffff); 211 SetRandomSeed(level->seed);
212 for (int layer = 0; layer < layerCount; layer++) 213 { 214 int layerPos = 6 + layer; 215 for (int x = -6 + layer; x <= 6 + layer; x += 1) 216 { 217 DrawModel(grassPatchModel[0], 218 (Vector3){x + (float)GetRandomValue(-25,25) / 100.0f, 0.0f, -layerPos + (float)GetRandomValue(-25,25) / 100.0f}, 219 1.0f, WHITE); 220 DrawModel(grassPatchModel[0], 221 (Vector3){x + (float)GetRandomValue(-25,25) / 100.0f, 0.0f, layerPos + (float)GetRandomValue(-25,25) / 100.0f}, 222 1.0f, WHITE); 223 } 224 225 for (int z = -5 + layer; z <= 5 + layer; z += 1) 226 { 227 DrawModel(grassPatchModel[0], 228 (Vector3){-layerPos + (float)GetRandomValue(-25,25) / 100.0f, 0.0f, z + (float)GetRandomValue(-25,25) / 100.0f}, 229 1.0f, WHITE); 230 DrawModel(grassPatchModel[0], 231 (Vector3){layerPos + (float)GetRandomValue(-25,25) / 100.0f, 0.0f, z + (float)GetRandomValue(-25,25) / 100.0f}, 232 1.0f, WHITE); 233 }
234 } 235 SetRandomSeed(oldSeed);
236 } 237 238 void DrawLevelBuildingState(Level *level) 239 { 240 BeginMode3D(level->camera); 241 DrawLevelGround(level); 242 TowerDraw(); 243 EnemyDraw(); 244 ProjectileDraw(); 245 ParticleDraw(); 246 247 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 248 float planeDistance = ray.position.y / -ray.direction.y; 249 float planeX = ray.direction.x * planeDistance + ray.position.x; 250 float planeY = ray.direction.z * planeDistance + ray.position.z; 251 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 252 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 253 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5) 254 { 255 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 256 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 257 { 258 if (TowerTryAdd(level->placementMode, mapX, mapY)) 259 { 260 level->playerGold -= GetTowerCosts(level->placementMode); 261 level->placementMode = TOWER_TYPE_NONE; 262 } 263 } 264 } 265 266 guiState.isBlocked = 0; 267 268 EndMode3D(); 269 270 TowerDrawHealthBars(level->camera); 271 272 static ButtonState buildWallButtonState = {0}; 273 static ButtonState buildGunButtonState = {0}; 274 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 275 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN; 276 277 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 278 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_GUN, "Archer"); 279 280 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 281 { 282 level->nextState = LEVEL_STATE_RESET; 283 } 284 285 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 286 { 287 level->nextState = LEVEL_STATE_BATTLE; 288 } 289 290 const char *text = "Building phase"; 291 int textWidth = MeasureText(text, 20); 292 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 293 } 294 295 void InitBattleStateConditions(Level *level) 296 { 297 level->state = LEVEL_STATE_BATTLE; 298 level->nextState = LEVEL_STATE_NONE; 299 level->waveEndTimer = 0.0f; 300 for (int i = 0; i < 10; i++) 301 { 302 EnemyWave *wave = &level->waves[i]; 303 wave->spawned = 0; 304 wave->timeToSpawnNext = wave->delay; 305 } 306 } 307 308 void DrawLevelBattleState(Level *level) 309 { 310 BeginMode3D(level->camera); 311 DrawLevelGround(level); 312 TowerDraw(); 313 EnemyDraw(); 314 ProjectileDraw(); 315 ParticleDraw(); 316 guiState.isBlocked = 0; 317 EndMode3D(); 318 319 EnemyDrawHealthbars(level->camera); 320 TowerDrawHealthBars(level->camera); 321 322 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 323 { 324 level->nextState = LEVEL_STATE_RESET; 325 } 326 327 int maxCount = 0; 328 int remainingCount = 0; 329 for (int i = 0; i < 10; i++) 330 { 331 EnemyWave *wave = &level->waves[i]; 332 if (wave->wave != level->currentWave) 333 { 334 continue; 335 } 336 maxCount += wave->count; 337 remainingCount += wave->count - wave->spawned; 338 } 339 int aliveCount = EnemyCount(); 340 remainingCount += aliveCount; 341 342 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 343 int textWidth = MeasureText(text, 20); 344 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 345 } 346 347 void DrawLevel(Level *level) 348 { 349 switch (level->state) 350 { 351 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 352 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 353 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 354 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 355 default: break; 356 } 357 358 DrawLevelHud(level); 359 } 360 361 void UpdateLevel(Level *level) 362 { 363 if (level->state == LEVEL_STATE_BATTLE) 364 { 365 int activeWaves = 0; 366 for (int i = 0; i < 10; i++) 367 { 368 EnemyWave *wave = &level->waves[i]; 369 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 370 { 371 continue; 372 } 373 activeWaves++; 374 wave->timeToSpawnNext -= gameTime.deltaTime; 375 if (wave->timeToSpawnNext <= 0.0f) 376 { 377 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 378 if (enemy) 379 { 380 wave->timeToSpawnNext = wave->interval; 381 wave->spawned++; 382 } 383 } 384 } 385 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 386 level->waveEndTimer += gameTime.deltaTime; 387 if (level->waveEndTimer >= 2.0f) 388 { 389 level->nextState = LEVEL_STATE_LOST_WAVE; 390 } 391 } 392 else if (activeWaves == 0 && EnemyCount() == 0) 393 { 394 level->waveEndTimer += gameTime.deltaTime; 395 if (level->waveEndTimer >= 2.0f) 396 { 397 level->nextState = LEVEL_STATE_WON_WAVE; 398 } 399 } 400 } 401 402 PathFindingMapUpdate(); 403 EnemyUpdate(); 404 TowerUpdate(); 405 ProjectileUpdate(); 406 ParticleUpdate(); 407 408 if (level->nextState == LEVEL_STATE_RESET) 409 { 410 InitLevel(level); 411 } 412 413 if (level->nextState == LEVEL_STATE_BATTLE) 414 { 415 InitBattleStateConditions(level); 416 } 417 418 if (level->nextState == LEVEL_STATE_WON_WAVE) 419 { 420 level->currentWave++; 421 level->state = LEVEL_STATE_WON_WAVE; 422 } 423 424 if (level->nextState == LEVEL_STATE_LOST_WAVE) 425 { 426 level->state = LEVEL_STATE_LOST_WAVE; 427 } 428 429 if (level->nextState == LEVEL_STATE_BUILDING) 430 { 431 level->state = LEVEL_STATE_BUILDING; 432 } 433 434 if (level->nextState == LEVEL_STATE_WON_LEVEL) 435 { 436 // make something of this later 437 InitLevel(level); 438 } 439 440 level->nextState = LEVEL_STATE_NONE; 441 } 442 443 float nextSpawnTime = 0.0f; 444 445 void ResetGame() 446 { 447 InitLevel(currentLevel); 448 } 449 450 void InitGame() 451 { 452 TowerInit(); 453 EnemyInit(); 454 ProjectileInit(); 455 ParticleInit(); 456 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 457 458 currentLevel = levels; 459 InitLevel(currentLevel); 460 } 461 462 //# Immediate GUI functions 463 464 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 465 { 466 const float healthBarHeight = 6.0f; 467 const float healthBarOffset = 15.0f; 468 const float inset = 2.0f; 469 const float innerWidth = healthBarWidth - inset * 2; 470 const float innerHeight = healthBarHeight - inset * 2; 471 472 Vector2 screenPos = GetWorldToScreen(position, camera); 473 float centerX = screenPos.x - healthBarWidth * 0.5f; 474 float topY = screenPos.y - healthBarOffset; 475 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 476 float healthWidth = innerWidth * healthRatio; 477 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 478 } 479 480 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 481 { 482 Rectangle bounds = {x, y, width, height}; 483 int isPressed = 0; 484 int isSelected = state && state->isSelected; 485 int isDisabled = state && state->isDisabled; 486 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 487 { 488 Color color = isSelected ? DARKGRAY : GRAY; 489 DrawRectangle(x, y, width, height, color); 490 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 491 { 492 isPressed = 1; 493 } 494 guiState.isBlocked = 1; 495 } 496 else 497 { 498 Color color = isSelected ? WHITE : LIGHTGRAY; 499 DrawRectangle(x, y, width, height, color); 500 } 501 Font font = GetFontDefault(); 502 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 503 Color textColor = isDisabled ? GRAY : BLACK; 504 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 505 return isPressed; 506 } 507 508 //# Main game loop 509 510 void GameUpdate() 511 { 512 float dt = GetFrameTime(); 513 // cap maximum delta time to 0.1 seconds to prevent large time steps 514 if (dt > 0.1f) dt = 0.1f; 515 gameTime.time += dt; 516 gameTime.deltaTime = dt; 517 518 UpdateLevel(currentLevel); 519 } 520 521 int main(void) 522 { 523 int screenWidth, screenHeight; 524 GetPreferredSize(&screenWidth, &screenHeight); 525 InitWindow(screenWidth, screenHeight, "Tower defense"); 526 SetTargetFPS(30); 527 528 LoadAssets(); 529 InitGame(); 530 531 while (!WindowShouldClose()) 532 { 533 if (IsPaused()) { 534 // canvas is not visible in browser - do nothing 535 continue; 536 } 537 538 BeginDrawing(); 539 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 540 541 GameUpdate(); 542 DrawLevel(currentLevel); 543 544 EndDrawing(); 545 } 546 547 CloseWindow(); 548 549 return 0; 550 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
 30 #define TOWER_TYPE_NONE 0
 31 #define TOWER_TYPE_BASE 1
 32 #define TOWER_TYPE_GUN 2
 33 #define TOWER_TYPE_WALL 3
 34 #define TOWER_TYPE_COUNT 4
 35 
 36 typedef struct Tower
 37 {
 38   int16_t x, y;
 39   uint8_t towerType;
 40   Vector2 lastTargetPosition;
 41   float cooldown;
 42   float damage;
 43 } Tower;
 44 
 45 typedef struct GameTime
 46 {
 47   float time;
 48   float deltaTime;
 49 } GameTime;
 50 
 51 typedef struct ButtonState {
 52   char isSelected;
 53   char isDisabled;
 54 } ButtonState;
 55 
 56 typedef struct GUIState {
 57   int isBlocked;
 58 } GUIState;
 59 
 60 typedef enum LevelState
 61 {
 62   LEVEL_STATE_NONE,
 63   LEVEL_STATE_BUILDING,
 64   LEVEL_STATE_BATTLE,
 65   LEVEL_STATE_WON_WAVE,
 66   LEVEL_STATE_LOST_WAVE,
 67   LEVEL_STATE_WON_LEVEL,
 68   LEVEL_STATE_RESET,
 69 } LevelState;
 70 
 71 typedef struct EnemyWave {
 72   uint8_t enemyType;
 73   uint8_t wave;
 74   uint16_t count;
 75   float interval;
 76   float delay;
 77   Vector2 spawnPosition;
 78 
 79   uint16_t spawned;
 80   float timeToSpawnNext;
 81 } EnemyWave;
 82 
 83 typedef struct Level
 84 {
85 int seed;
86 LevelState state; 87 LevelState nextState; 88 Camera3D camera; 89 int placementMode; 90 91 int initialGold; 92 int playerGold; 93 94 EnemyWave waves[10]; 95 int currentWave; 96 float waveEndTimer; 97 } Level; 98 99 typedef struct DeltaSrc 100 { 101 char x, y; 102 } DeltaSrc; 103 104 typedef struct PathfindingMap 105 { 106 int width, height; 107 float scale; 108 float *distances; 109 long *towerIndex; 110 DeltaSrc *deltaSrc; 111 float maxDistance; 112 Matrix toMapSpace; 113 Matrix toWorldSpace; 114 } PathfindingMap; 115 116 // when we execute the pathfinding algorithm, we need to store the active nodes 117 // in a queue. Each node has a position, a distance from the start, and the 118 // position of the node that we came from. 119 typedef struct PathfindingNode 120 { 121 int16_t x, y, fromX, fromY; 122 float distance; 123 } PathfindingNode; 124 125 typedef struct EnemyId 126 { 127 uint16_t index; 128 uint16_t generation; 129 } EnemyId; 130 131 typedef struct EnemyClassConfig 132 { 133 float speed; 134 float health; 135 float radius; 136 float maxAcceleration; 137 float requiredContactTime; 138 float explosionDamage; 139 float explosionRange; 140 float explosionPushbackPower; 141 int goldValue; 142 } EnemyClassConfig; 143 144 typedef struct Enemy 145 { 146 int16_t currentX, currentY; 147 int16_t nextX, nextY; 148 Vector2 simPosition; 149 Vector2 simVelocity; 150 uint16_t generation; 151 float walkedDistance; 152 float startMovingTime; 153 float damage, futureDamage; 154 float contactTime; 155 uint8_t enemyType; 156 uint8_t movePathCount; 157 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 158 } Enemy; 159 160 // a unit that uses sprites to be drawn 161 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0 162 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1 163 typedef struct SpriteUnit 164 { 165 Rectangle srcRect; 166 Vector2 offset; 167 int frameCount; 168 float frameDuration; 169 Rectangle srcWeaponIdleRect; 170 Vector2 srcWeaponIdleOffset; 171 Rectangle srcWeaponCooldownRect; 172 Vector2 srcWeaponCooldownOffset; 173 } SpriteUnit; 174 175 #define PROJECTILE_MAX_COUNT 1200 176 #define PROJECTILE_TYPE_NONE 0 177 #define PROJECTILE_TYPE_ARROW 1 178 179 typedef struct Projectile 180 { 181 uint8_t projectileType; 182 float shootTime; 183 float arrivalTime; 184 float distance; 185 float damage; 186 Vector3 position; 187 Vector3 target; 188 Vector3 directionNormal; 189 EnemyId targetEnemy; 190 } Projectile; 191 192 //# Function declarations 193 float TowerGetMaxHealth(Tower *tower); 194 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 195 int EnemyAddDamage(Enemy *enemy, float damage); 196 197 //# Enemy functions 198 void EnemyInit(); 199 void EnemyDraw(); 200 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 201 void EnemyUpdate(); 202 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 203 float EnemyGetMaxHealth(Enemy *enemy); 204 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 205 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 206 EnemyId EnemyGetId(Enemy *enemy); 207 Enemy *EnemyTryResolve(EnemyId enemyId); 208 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 209 int EnemyAddDamage(Enemy *enemy, float damage); 210 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 211 int EnemyCount(); 212 void EnemyDrawHealthbars(Camera3D camera); 213 214 //# Tower functions 215 void TowerInit(); 216 Tower *TowerGetAt(int16_t x, int16_t y); 217 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 218 Tower *GetTowerByType(uint8_t towerType); 219 int GetTowerCosts(uint8_t towerType); 220 float TowerGetMaxHealth(Tower *tower); 221 void TowerDraw(); 222 void TowerUpdate(); 223 void TowerDrawHealthBars(Camera3D camera); 224 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 225 226 //# Particles 227 void ParticleInit(); 228 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime); 229 void ParticleUpdate(); 230 void ParticleDraw(); 231 232 //# Projectiles 233 void ProjectileInit(); 234 void ProjectileDraw(); 235 void ProjectileUpdate(); 236 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage); 237 238 //# Pathfinding map 239 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 240 float PathFindingGetDistance(int mapX, int mapY); 241 Vector2 PathFindingGetGradient(Vector3 world); 242 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 243 void PathFindingMapUpdate(); 244 void PathFindingMapDraw(); 245 246 //# UI 247 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth); 248 249 //# Level 250 void DrawLevelGround(Level *level); 251 252 //# variables 253 extern Level *currentLevel; 254 extern Enemy enemies[ENEMY_MAX_COUNT]; 255 extern int enemyCount; 256 extern EnemyClassConfig enemyClassConfigs[]; 257 258 extern GUIState guiState; 259 extern GameTime gameTime; 260 extern Tower towers[TOWER_MAX_COUNT]; 261 extern int towerCount; 262 263 extern Texture2D palette, spriteSheet; 264 265 #endif
  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}, 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 EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522   }
523 }
  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()
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 < towerCount; i++)
131   {
132     Tower *tower = &towers[i];
133     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134     {
135       continue;
136     }
137     int16_t mapX, mapY;
138     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139     // this would not work correctly and needs to be refined to allow towers covering multiple cells
140     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141     // one cell. For now.
142     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143     {
144       continue;
145     }
146     int index = mapY * width + mapX;
147     pathfindingMap.towerIndex[index] = i;
148   }
149 
150   // we start at the castle and add the castle to the queue
151   pathfindingMap.maxDistance = 0.0f;
152   pathfindingNodeQueueCount = 0;
153   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154   PathfindingNode *node = 0;
155   while ((node = PathFindingNodePop()))
156   {
157     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158     {
159       continue;
160     }
161     int index = node->y * width + node->x;
162     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163     {
164       continue;
165     }
166 
167     int deltaX = node->x - node->fromX;
168     int deltaY = node->y - node->fromY;
169     // even if the cell is blocked by a tower, we still may want to store the direction
170     // (though this might not be needed, IDK right now)
171     pathfindingMap.deltaSrc[index].x = (char) deltaX;
172     pathfindingMap.deltaSrc[index].y = (char) deltaY;
173 
174     // we skip nodes that are blocked by towers
175     if (pathfindingMap.towerIndex[index] >= 0)
176     {
177       node->distance += 8.0f;
178     }
179     pathfindingMap.distances[index] = node->distance;
180     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185   }
186 }
187 
188 void PathFindingMapDraw()
189 {
190   float cellSize = pathfindingMap.scale * 0.9f;
191   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192   for (int x = 0; x < pathfindingMap.width; x++)
193   {
194     for (int y = 0; y < pathfindingMap.height; y++)
195     {
196       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200       // animate the distance "wave" to show how the pathfinding algorithm expands
201       // from the castle
202       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203       {
204         color = BLACK;
205       }
206       DrawCube(position, cellSize, 0.1f, cellSize, color);
207     }
208   }
209 }
210 
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213   int16_t mapX, mapY;
214   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215   {
216     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217     return (Vector2){(float)-delta.x, (float)-delta.y};
218   }
219   // fallback to a simple gradient calculation
220   float n = PathFindingGetDistance(mapX, mapY - 1);
221   float s = PathFindingGetDistance(mapX, mapY + 1);
222   float w = PathFindingGetDistance(mapX - 1, mapY);
223   float e = PathFindingGetDistance(mapX + 1, mapY);
224   return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 void ProjectileInit()
  8 {
  9   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 10   {
 11     projectiles[i] = (Projectile){0};
 12   }
 13 }
 14 
 15 void ProjectileDraw()
 16 {
 17   for (int i = 0; i < projectileCount; i++)
 18   {
 19     Projectile projectile = projectiles[i];
 20     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 21     {
 22       continue;
 23     }
 24     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 25     if (transition >= 1.0f)
 26     {
 27       continue;
 28     }
 29     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 30     {
 31       float t = transition + transitionOffset * 0.3f;
 32       if (t > 1.0f)
 33       {
 34         break;
 35       }
 36       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 37       Color color = RED;
 38       if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 39       {
 40         // make tip red but quickly fade to brown
 41         color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 42         // fake a ballista flight path using parabola equation
 43         float parabolaT = t - 0.5f;
 44         parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 45         position.y += 0.15f * parabolaT * projectile.distance;
 46       }
 47 
 48       float size = 0.06f * (transitionOffset + 0.25f);
 49       DrawCube(position, size, size, size, color);
 50     }
 51   }
 52 }
 53 
 54 void ProjectileUpdate()
 55 {
 56   for (int i = 0; i < projectileCount; i++)
 57   {
 58     Projectile *projectile = &projectiles[i];
 59     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 60     {
 61       continue;
 62     }
 63     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 64     if (transition >= 1.0f)
 65     {
 66       projectile->projectileType = PROJECTILE_TYPE_NONE;
 67       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 68       if (enemy)
 69       {
 70         EnemyAddDamage(enemy, projectile->damage);
 71       }
 72       continue;
 73     }
 74   }
 75 }
 76 
 77 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage)
 78 {
 79   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       projectile->projectileType = projectileType;
 85       projectile->shootTime = gameTime.time;
 86       float distance = Vector3Distance(position, target);
 87       projectile->arrivalTime = gameTime.time + distance / speed;
 88       projectile->damage = damage;
 89       projectile->position = position;
 90       projectile->target = target;
 91       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
 92       projectile->distance = distance;
 93       projectile->targetEnemy = EnemyGetId(enemy);
 94       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 95       return projectile;
 96     }
 97   }
 98   return 0;
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 Tower towers[TOWER_MAX_COUNT];
  5 int towerCount = 0;
  6 
  7 Model towerModels[TOWER_TYPE_COUNT];
  8 
  9 // definition of our archer unit
 10 SpriteUnit archerUnit = {
 11     .srcRect = {0, 0, 16, 16},
 12     .offset = {7, 1},
 13     .frameCount = 1,
 14     .frameDuration = 0.0f,
 15     .srcWeaponIdleRect = {16, 0, 6, 16},
 16     .srcWeaponIdleOffset = {8, 0},
 17     .srcWeaponCooldownRect = {22, 0, 11, 16},
 18     .srcWeaponCooldownOffset = {10, 0},
 19 };
 20 
 21 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 22 {
 23   float xScale = flip ? -1.0f : 1.0f;
 24   Camera3D camera = currentLevel->camera;
 25   float size = 0.5f;
 26   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 27   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 28   // we want the sprite to face the camera, so we need to calculate the up vector
 29   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 30   Vector3 up = {0, 1, 0};
 31   Vector3 right = Vector3CrossProduct(forward, up);
 32   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 33 
 34   Rectangle srcRect = unit.srcRect;
 35   if (unit.frameCount > 1)
 36   {
 37     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 38   }
 39   if (flip)
 40   {
 41     srcRect.x += srcRect.width;
 42     srcRect.width = -srcRect.width;
 43   }
 44   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 45 
 46   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 47   {
 48     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 49     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 50     srcRect = unit.srcWeaponCooldownRect;
 51     if (flip)
 52     {
 53       // position.x = flip * scale.x * 0.5f;
 54       srcRect.x += srcRect.width;
 55       srcRect.width = -srcRect.width;
 56       offset.x = scale.x - offset.x;
 57     }
 58     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 59   }
 60   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
 61   {
 62     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
 63     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
 64     srcRect = unit.srcWeaponIdleRect;
 65     if (flip)
 66     {
 67       // position.x = flip * scale.x * 0.5f;
 68       srcRect.x += srcRect.width;
 69       srcRect.width = -srcRect.width;
 70       offset.x = scale.x - offset.x;
 71     }
 72     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 73   }
 74 }
 75 
 76 void TowerInit()
 77 {
 78   for (int i = 0; i < TOWER_MAX_COUNT; i++)
 79   {
 80     towers[i] = (Tower){0};
 81   }
 82   towerCount = 0;
 83 
 84   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
 85   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
 86 
 87   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
 88   {
 89     if (towerModels[i].materials)
 90     {
 91       // assign the palette texture to the material of the model (0 is not used afaik)
 92       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 93     }
 94   }
 95 }
 96 
 97 static void TowerGunUpdate(Tower *tower)
 98 {
 99   if (tower->cooldown <= 0)
100   {
101     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
102     if (enemy)
103     {
104       tower->cooldown = 0.5f;
105       // shoot the enemy; determine future position of the enemy
106       float bulletSpeed = 4.0f;
107       float bulletDamage = 3.0f;
108       Vector2 velocity = enemy->simVelocity;
109       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
110       Vector2 towerPosition = {tower->x, tower->y};
111       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
112       for (int i = 0; i < 8; i++) {
113         velocity = enemy->simVelocity;
114         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
115         float distance = Vector2Distance(towerPosition, futurePosition);
116         float eta2 = distance / bulletSpeed;
117         if (fabs(eta - eta2) < 0.01f) {
118           break;
119         }
120         eta = (eta2 + eta) * 0.5f;
121       }
122       ProjectileTryAdd(PROJECTILE_TYPE_ARROW, enemy, 
123         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
124         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
125         bulletSpeed, bulletDamage);
126       enemy->futureDamage += bulletDamage;
127       tower->lastTargetPosition = futurePosition;
128     }
129   }
130   else
131   {
132     tower->cooldown -= gameTime.deltaTime;
133   }
134 }
135 
136 Tower *TowerGetAt(int16_t x, int16_t y)
137 {
138   for (int i = 0; i < towerCount; i++)
139   {
140     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
141     {
142       return &towers[i];
143     }
144   }
145   return 0;
146 }
147 
148 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
149 {
150   if (towerCount >= TOWER_MAX_COUNT)
151   {
152     return 0;
153   }
154 
155   Tower *tower = TowerGetAt(x, y);
156   if (tower)
157   {
158     return 0;
159   }
160 
161   tower = &towers[towerCount++];
162   tower->x = x;
163   tower->y = y;
164   tower->towerType = towerType;
165   tower->cooldown = 0.0f;
166   tower->damage = 0.0f;
167   return tower;
168 }
169 
170 Tower *GetTowerByType(uint8_t towerType)
171 {
172   for (int i = 0; i < towerCount; i++)
173   {
174     if (towers[i].towerType == towerType)
175     {
176       return &towers[i];
177     }
178   }
179   return 0;
180 }
181 
182 int GetTowerCosts(uint8_t towerType)
183 {
184   switch (towerType)
185   {
186   case TOWER_TYPE_BASE:
187     return 0;
188   case TOWER_TYPE_GUN:
189     return 6;
190   case TOWER_TYPE_WALL:
191     return 2;
192   }
193   return 0;
194 }
195 
196 float TowerGetMaxHealth(Tower *tower)
197 {
198   switch (tower->towerType)
199   {
200   case TOWER_TYPE_BASE:
201     return 10.0f;
202   case TOWER_TYPE_GUN:
203     return 3.0f;
204   case TOWER_TYPE_WALL:
205     return 5.0f;
206   }
207   return 0.0f;
208 }
209 
210 void TowerDraw()
211 {
212   for (int i = 0; i < towerCount; i++)
213   {
214     Tower tower = towers[i];
215     if (tower.towerType == TOWER_TYPE_NONE)
216     {
217       continue;
218     }
219 
220     switch (tower.towerType)
221     {
222     case TOWER_TYPE_GUN:
223       {
224         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
225         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
226         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
227         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
228           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
229       }
230       break;
231     default:
232       if (towerModels[tower.towerType].materials)
233       {
234         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
235       } else {
236         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
237       }
238       break;
239     }
240   }
241 }
242 
243 void TowerUpdate()
244 {
245   for (int i = 0; i < towerCount; i++)
246   {
247     Tower *tower = &towers[i];
248     switch (tower->towerType)
249     {
250     case TOWER_TYPE_GUN:
251       TowerGunUpdate(tower);
252       break;
253     }
254   }
255 }
256 
257 void TowerDrawHealthBars(Camera3D camera)
258 {
259   for (int i = 0; i < towerCount; i++)
260   {
261     Tower *tower = &towers[i];
262     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
263     {
264       continue;
265     }
266     
267     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
268     float maxHealth = TowerGetMaxHealth(tower);
269     float health = maxHealth - tower->damage;
270     float healthRatio = health / maxHealth;
271     
272     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
273   }
274 }
  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
Whenever you press the "Reset level" button, the decorations will look somewhat different.

In the InitLevel function, we now set the seed to the current time. In this case, using the time as a seed is desired: We want the game to always look different when we restart the level. So this is a conscious decision to use the time as a seed.

So let's continue: Another thing we'd want is to rotate the objects randomly, as it still looks quite orderly. We also should add a random function that returns a random value within a certain range for more convenient use. So let's add some rotation to the objects:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 //# Variables
  7 GUIState guiState = {0};
  8 GameTime gameTime = {0};
  9 
 10 Model floorTileAModel = {0};
 11 Model floorTileBModel = {0};
 12 Model grassPatchModel[1] = {0};
 13 
 14 Texture2D palette, spriteSheet;
 15 
 16 Level levels[] = {
 17   [0] = {
 18     .state = LEVEL_STATE_BUILDING,
 19     .initialGold = 20,
 20     .waves[0] = {
 21       .enemyType = ENEMY_TYPE_MINION,
 22       .wave = 0,
 23       .count = 10,
 24       .interval = 2.5f,
 25       .delay = 1.0f,
 26       .spawnPosition = {0, 6},
 27     },
 28     .waves[1] = {
 29       .enemyType = ENEMY_TYPE_MINION,
 30       .wave = 1,
 31       .count = 20,
 32       .interval = 1.5f,
 33       .delay = 1.0f,
 34       .spawnPosition = {0, 6},
 35     },
 36     .waves[2] = {
 37       .enemyType = ENEMY_TYPE_MINION,
 38       .wave = 2,
 39       .count = 30,
 40       .interval = 1.2f,
 41       .delay = 1.0f,
 42       .spawnPosition = {0, 6},
 43     }
 44   },
 45 };
 46 
 47 Level *currentLevel = levels;
 48 
 49 //# Game
 50 
 51 static Model LoadGLBModel(char *filename)
 52 {
 53   Model model = LoadModel(TextFormat("data/%s.glb",filename));
 54   if (model.materialCount > 1)
 55   {
 56     model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 57   }
 58   return model;
 59 }
 60 
 61 void LoadAssets()
 62 {
 63   // load a sprite sheet that contains all units
 64   spriteSheet = LoadTexture("data/spritesheet.png");
 65   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
 66 
 67   // we'll use a palette texture to colorize the all buildings and environment art
 68   palette = LoadTexture("data/palette.png");
 69   // The texture uses gradients on very small space, so we'll enable bilinear filtering
 70   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
 71 
 72   floorTileAModel = LoadGLBModel("floor-tile-a");
 73   floorTileBModel = LoadGLBModel("floor-tile-b");
 74   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
 75 }
 76 
 77 void InitLevel(Level *level)
 78 {
 79   level->seed = (int)(GetTime() * 100.0f);
 80 
 81   TowerInit();
 82   EnemyInit();
 83   ProjectileInit();
 84   ParticleInit();
 85   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
 86 
 87   level->placementMode = 0;
 88   level->state = LEVEL_STATE_BUILDING;
 89   level->nextState = LEVEL_STATE_NONE;
 90   level->playerGold = level->initialGold;
 91   level->currentWave = 0;
 92 
 93   Camera *camera = &level->camera;
 94   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
 95   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
 96   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
 97   camera->fovy = 10.0f;
 98   camera->projection = CAMERA_ORTHOGRAPHIC;
 99 }
100 
101 void DrawLevelHud(Level *level)
102 {
103   const char *text = TextFormat("Gold: %d", level->playerGold);
104   Font font = GetFontDefault();
105   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
106   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
107 }
108 
109 void DrawLevelReportLostWave(Level *level)
110 {
111   BeginMode3D(level->camera);
112   DrawLevelGround(level);
113   TowerDraw();
114   EnemyDraw();
115   ProjectileDraw();
116   ParticleDraw();
117   guiState.isBlocked = 0;
118   EndMode3D();
119 
120   TowerDrawHealthBars(level->camera);
121 
122   const char *text = "Wave lost";
123   int textWidth = MeasureText(text, 20);
124   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
125 
126   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
127   {
128     level->nextState = LEVEL_STATE_RESET;
129   }
130 }
131 
132 int HasLevelNextWave(Level *level)
133 {
134   for (int i = 0; i < 10; i++)
135   {
136     EnemyWave *wave = &level->waves[i];
137     if (wave->wave == level->currentWave)
138     {
139       return 1;
140     }
141   }
142   return 0;
143 }
144 
145 void DrawLevelReportWonWave(Level *level)
146 {
147   BeginMode3D(level->camera);
148   DrawLevelGround(level);
149   TowerDraw();
150   EnemyDraw();
151   ProjectileDraw();
152   ParticleDraw();
153   guiState.isBlocked = 0;
154   EndMode3D();
155 
156   TowerDrawHealthBars(level->camera);
157 
158   const char *text = "Wave won";
159   int textWidth = MeasureText(text, 20);
160   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
161 
162 
163   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
164   {
165     level->nextState = LEVEL_STATE_RESET;
166   }
167 
168   if (HasLevelNextWave(level))
169   {
170     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
171     {
172       level->nextState = LEVEL_STATE_BUILDING;
173     }
174   }
175   else {
176     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
177     {
178       level->nextState = LEVEL_STATE_WON_LEVEL;
179     }
180   }
181 }
182 
183 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
184 {
185   static ButtonState buttonStates[8] = {0};
186   int cost = GetTowerCosts(towerType);
187   const char *text = TextFormat("%s: %d", name, cost);
188   buttonStates[towerType].isSelected = level->placementMode == towerType;
189   buttonStates[towerType].isDisabled = level->playerGold < cost;
190   if (Button(text, x, y, width, height, &buttonStates[towerType]))
191   {
192     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
193   }
194 }
195 
196 float GetRandomFloat(float min, float max) 197 { 198 int random = GetRandomValue(0, 0xfffffff); 199 return ((float)random / (float)0xfffffff) * (max - min) + min; 200 } 201
202 void DrawLevelGround(Level *level) 203 { 204 // draw checkerboard ground pattern 205 for (int x = -5; x <= 5; x += 1) 206 { 207 for (int y = -5; y <= 5; y += 1) 208 { 209 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 210 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 211 } 212 } 213
214 // draw grass patches around the edges 215 const Vector3 up = {0, 1, 0};
216 const int layerCount = 2;
217 unsigned int oldSeed = GetRandomValue(0, 0xfffffff); 218 const float wiggle = 0.25f;
219 SetRandomSeed(level->seed); 220 for (int layer = 0; layer < layerCount; layer++) 221 { 222 int layerPos = 6 + layer; 223 for (int x = -6 + layer; x <= 6 + layer; x += 1) 224 {
225 DrawModelEx(grassPatchModel[0], 226 (Vector3){x + GetRandomFloat(-wiggle, wiggle), 0.0f, -layerPos + GetRandomFloat(-wiggle, wiggle)}, 227 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 228 DrawModelEx(grassPatchModel[0], 229 (Vector3){x + GetRandomFloat(-wiggle, wiggle), 0.0f, layerPos + GetRandomFloat(-wiggle, wiggle)}, 230 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
231 } 232 233 for (int z = -5 + layer; z <= 5 + layer; z += 1) 234 {
235 DrawModelEx(grassPatchModel[0], 236 (Vector3){-layerPos + GetRandomFloat(-wiggle, wiggle), 0.0f, z + GetRandomFloat(-wiggle, wiggle)}, 237 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 238 DrawModelEx(grassPatchModel[0], 239 (Vector3){layerPos + GetRandomFloat(-wiggle, wiggle), 0.0f, z + GetRandomFloat(-wiggle, wiggle)}, 240 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
241 } 242 } 243 SetRandomSeed(oldSeed); 244 } 245 246 void DrawLevelBuildingState(Level *level) 247 { 248 BeginMode3D(level->camera); 249 DrawLevelGround(level); 250 TowerDraw(); 251 EnemyDraw(); 252 ProjectileDraw(); 253 ParticleDraw(); 254 255 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 256 float planeDistance = ray.position.y / -ray.direction.y; 257 float planeX = ray.direction.x * planeDistance + ray.position.x; 258 float planeY = ray.direction.z * planeDistance + ray.position.z; 259 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 260 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 261 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5) 262 { 263 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 264 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 265 { 266 if (TowerTryAdd(level->placementMode, mapX, mapY)) 267 { 268 level->playerGold -= GetTowerCosts(level->placementMode); 269 level->placementMode = TOWER_TYPE_NONE; 270 } 271 } 272 } 273 274 guiState.isBlocked = 0; 275 276 EndMode3D(); 277 278 TowerDrawHealthBars(level->camera); 279 280 static ButtonState buildWallButtonState = {0}; 281 static ButtonState buildGunButtonState = {0}; 282 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 283 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN; 284 285 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 286 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_GUN, "Archer"); 287 288 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 289 { 290 level->nextState = LEVEL_STATE_RESET; 291 } 292 293 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 294 { 295 level->nextState = LEVEL_STATE_BATTLE; 296 } 297 298 const char *text = "Building phase"; 299 int textWidth = MeasureText(text, 20); 300 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 301 } 302 303 void InitBattleStateConditions(Level *level) 304 { 305 level->state = LEVEL_STATE_BATTLE; 306 level->nextState = LEVEL_STATE_NONE; 307 level->waveEndTimer = 0.0f; 308 for (int i = 0; i < 10; i++) 309 { 310 EnemyWave *wave = &level->waves[i]; 311 wave->spawned = 0; 312 wave->timeToSpawnNext = wave->delay; 313 } 314 } 315 316 void DrawLevelBattleState(Level *level) 317 { 318 BeginMode3D(level->camera); 319 DrawLevelGround(level); 320 TowerDraw(); 321 EnemyDraw(); 322 ProjectileDraw(); 323 ParticleDraw(); 324 guiState.isBlocked = 0; 325 EndMode3D(); 326 327 EnemyDrawHealthbars(level->camera); 328 TowerDrawHealthBars(level->camera); 329 330 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 331 { 332 level->nextState = LEVEL_STATE_RESET; 333 } 334 335 int maxCount = 0; 336 int remainingCount = 0; 337 for (int i = 0; i < 10; i++) 338 { 339 EnemyWave *wave = &level->waves[i]; 340 if (wave->wave != level->currentWave) 341 { 342 continue; 343 } 344 maxCount += wave->count; 345 remainingCount += wave->count - wave->spawned; 346 } 347 int aliveCount = EnemyCount(); 348 remainingCount += aliveCount; 349 350 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 351 int textWidth = MeasureText(text, 20); 352 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 353 } 354 355 void DrawLevel(Level *level) 356 { 357 switch (level->state) 358 { 359 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 360 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 361 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 362 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 363 default: break; 364 } 365 366 DrawLevelHud(level); 367 } 368 369 void UpdateLevel(Level *level) 370 { 371 if (level->state == LEVEL_STATE_BATTLE) 372 { 373 int activeWaves = 0; 374 for (int i = 0; i < 10; i++) 375 { 376 EnemyWave *wave = &level->waves[i]; 377 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 378 { 379 continue; 380 } 381 activeWaves++; 382 wave->timeToSpawnNext -= gameTime.deltaTime; 383 if (wave->timeToSpawnNext <= 0.0f) 384 { 385 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 386 if (enemy) 387 { 388 wave->timeToSpawnNext = wave->interval; 389 wave->spawned++; 390 } 391 } 392 } 393 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 394 level->waveEndTimer += gameTime.deltaTime; 395 if (level->waveEndTimer >= 2.0f) 396 { 397 level->nextState = LEVEL_STATE_LOST_WAVE; 398 } 399 } 400 else if (activeWaves == 0 && EnemyCount() == 0) 401 { 402 level->waveEndTimer += gameTime.deltaTime; 403 if (level->waveEndTimer >= 2.0f) 404 { 405 level->nextState = LEVEL_STATE_WON_WAVE; 406 } 407 } 408 } 409 410 PathFindingMapUpdate(); 411 EnemyUpdate(); 412 TowerUpdate(); 413 ProjectileUpdate(); 414 ParticleUpdate(); 415 416 if (level->nextState == LEVEL_STATE_RESET) 417 { 418 InitLevel(level); 419 } 420 421 if (level->nextState == LEVEL_STATE_BATTLE) 422 { 423 InitBattleStateConditions(level); 424 } 425 426 if (level->nextState == LEVEL_STATE_WON_WAVE) 427 { 428 level->currentWave++; 429 level->state = LEVEL_STATE_WON_WAVE; 430 } 431 432 if (level->nextState == LEVEL_STATE_LOST_WAVE) 433 { 434 level->state = LEVEL_STATE_LOST_WAVE; 435 } 436 437 if (level->nextState == LEVEL_STATE_BUILDING) 438 { 439 level->state = LEVEL_STATE_BUILDING; 440 } 441 442 if (level->nextState == LEVEL_STATE_WON_LEVEL) 443 { 444 // make something of this later 445 InitLevel(level); 446 } 447 448 level->nextState = LEVEL_STATE_NONE; 449 } 450 451 float nextSpawnTime = 0.0f; 452 453 void ResetGame() 454 { 455 InitLevel(currentLevel); 456 } 457 458 void InitGame() 459 { 460 TowerInit(); 461 EnemyInit(); 462 ProjectileInit(); 463 ParticleInit(); 464 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 465 466 currentLevel = levels; 467 InitLevel(currentLevel); 468 } 469 470 //# Immediate GUI functions 471 472 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 473 { 474 const float healthBarHeight = 6.0f; 475 const float healthBarOffset = 15.0f; 476 const float inset = 2.0f; 477 const float innerWidth = healthBarWidth - inset * 2; 478 const float innerHeight = healthBarHeight - inset * 2; 479 480 Vector2 screenPos = GetWorldToScreen(position, camera); 481 float centerX = screenPos.x - healthBarWidth * 0.5f; 482 float topY = screenPos.y - healthBarOffset; 483 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 484 float healthWidth = innerWidth * healthRatio; 485 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 486 } 487 488 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 489 { 490 Rectangle bounds = {x, y, width, height}; 491 int isPressed = 0; 492 int isSelected = state && state->isSelected; 493 int isDisabled = state && state->isDisabled; 494 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 495 { 496 Color color = isSelected ? DARKGRAY : GRAY; 497 DrawRectangle(x, y, width, height, color); 498 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 499 { 500 isPressed = 1; 501 } 502 guiState.isBlocked = 1; 503 } 504 else 505 { 506 Color color = isSelected ? WHITE : LIGHTGRAY; 507 DrawRectangle(x, y, width, height, color); 508 } 509 Font font = GetFontDefault(); 510 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 511 Color textColor = isDisabled ? GRAY : BLACK; 512 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 513 return isPressed; 514 } 515 516 //# Main game loop 517 518 void GameUpdate() 519 { 520 float dt = GetFrameTime(); 521 // cap maximum delta time to 0.1 seconds to prevent large time steps 522 if (dt > 0.1f) dt = 0.1f; 523 gameTime.time += dt; 524 gameTime.deltaTime = dt; 525 526 UpdateLevel(currentLevel); 527 } 528 529 int main(void) 530 { 531 int screenWidth, screenHeight; 532 GetPreferredSize(&screenWidth, &screenHeight); 533 InitWindow(screenWidth, screenHeight, "Tower defense"); 534 SetTargetFPS(30); 535 536 LoadAssets(); 537 InitGame(); 538 539 while (!WindowShouldClose()) 540 { 541 if (IsPaused()) { 542 // canvas is not visible in browser - do nothing 543 continue; 544 } 545 546 BeginDrawing(); 547 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 548 549 GameUpdate(); 550 DrawLevel(currentLevel); 551 552 EndDrawing(); 553 } 554 555 CloseWindow(); 556 557 return 0; 558 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
 30 #define TOWER_TYPE_NONE 0
 31 #define TOWER_TYPE_BASE 1
 32 #define TOWER_TYPE_GUN 2
 33 #define TOWER_TYPE_WALL 3
 34 #define TOWER_TYPE_COUNT 4
 35 
 36 typedef struct Tower
 37 {
 38   int16_t x, y;
 39   uint8_t towerType;
 40   Vector2 lastTargetPosition;
 41   float cooldown;
 42   float damage;
 43 } Tower;
 44 
 45 typedef struct GameTime
 46 {
 47   float time;
 48   float deltaTime;
 49 } GameTime;
 50 
 51 typedef struct ButtonState {
 52   char isSelected;
 53   char isDisabled;
 54 } ButtonState;
 55 
 56 typedef struct GUIState {
 57   int isBlocked;
 58 } GUIState;
 59 
 60 typedef enum LevelState
 61 {
 62   LEVEL_STATE_NONE,
 63   LEVEL_STATE_BUILDING,
 64   LEVEL_STATE_BATTLE,
 65   LEVEL_STATE_WON_WAVE,
 66   LEVEL_STATE_LOST_WAVE,
 67   LEVEL_STATE_WON_LEVEL,
 68   LEVEL_STATE_RESET,
 69 } LevelState;
 70 
 71 typedef struct EnemyWave {
 72   uint8_t enemyType;
 73   uint8_t wave;
 74   uint16_t count;
 75   float interval;
 76   float delay;
 77   Vector2 spawnPosition;
 78 
 79   uint16_t spawned;
 80   float timeToSpawnNext;
 81 } EnemyWave;
 82 
 83 typedef struct Level
 84 {
 85   int seed;
 86   LevelState state;
 87   LevelState nextState;
 88   Camera3D camera;
 89   int placementMode;
 90 
 91   int initialGold;
 92   int playerGold;
 93 
 94   EnemyWave waves[10];
 95   int currentWave;
 96   float waveEndTimer;
 97 } Level;
 98 
 99 typedef struct DeltaSrc
100 {
101   char x, y;
102 } DeltaSrc;
103 
104 typedef struct PathfindingMap
105 {
106   int width, height;
107   float scale;
108   float *distances;
109   long *towerIndex; 
110   DeltaSrc *deltaSrc;
111   float maxDistance;
112   Matrix toMapSpace;
113   Matrix toWorldSpace;
114 } PathfindingMap;
115 
116 // when we execute the pathfinding algorithm, we need to store the active nodes
117 // in a queue. Each node has a position, a distance from the start, and the
118 // position of the node that we came from.
119 typedef struct PathfindingNode
120 {
121   int16_t x, y, fromX, fromY;
122   float distance;
123 } PathfindingNode;
124 
125 typedef struct EnemyId
126 {
127   uint16_t index;
128   uint16_t generation;
129 } EnemyId;
130 
131 typedef struct EnemyClassConfig
132 {
133   float speed;
134   float health;
135   float radius;
136   float maxAcceleration;
137   float requiredContactTime;
138   float explosionDamage;
139   float explosionRange;
140   float explosionPushbackPower;
141   int goldValue;
142 } EnemyClassConfig;
143 
144 typedef struct Enemy
145 {
146   int16_t currentX, currentY;
147   int16_t nextX, nextY;
148   Vector2 simPosition;
149   Vector2 simVelocity;
150   uint16_t generation;
151   float walkedDistance;
152   float startMovingTime;
153   float damage, futureDamage;
154   float contactTime;
155   uint8_t enemyType;
156   uint8_t movePathCount;
157   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
158 } Enemy;
159 
160 // a unit that uses sprites to be drawn
161 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
162 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
163 typedef struct SpriteUnit
164 {
165   Rectangle srcRect;
166   Vector2 offset;
167   int frameCount;
168   float frameDuration;
169   Rectangle srcWeaponIdleRect;
170   Vector2 srcWeaponIdleOffset;
171   Rectangle srcWeaponCooldownRect;
172   Vector2 srcWeaponCooldownOffset;
173 } SpriteUnit;
174 
175 #define PROJECTILE_MAX_COUNT 1200
176 #define PROJECTILE_TYPE_NONE 0
177 #define PROJECTILE_TYPE_ARROW 1
178 
179 typedef struct Projectile
180 {
181   uint8_t projectileType;
182   float shootTime;
183   float arrivalTime;
184   float distance;
185   float damage;
186   Vector3 position;
187   Vector3 target;
188   Vector3 directionNormal;
189   EnemyId targetEnemy;
190 } Projectile;
191 
192 //# Function declarations
193 float TowerGetMaxHealth(Tower *tower);
194 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
195 int EnemyAddDamage(Enemy *enemy, float damage);
196 
197 //# Enemy functions
198 void EnemyInit();
199 void EnemyDraw();
200 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
201 void EnemyUpdate();
202 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
203 float EnemyGetMaxHealth(Enemy *enemy);
204 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
205 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
206 EnemyId EnemyGetId(Enemy *enemy);
207 Enemy *EnemyTryResolve(EnemyId enemyId);
208 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
209 int EnemyAddDamage(Enemy *enemy, float damage);
210 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
211 int EnemyCount();
212 void EnemyDrawHealthbars(Camera3D camera);
213 
214 //# Tower functions
215 void TowerInit();
216 Tower *TowerGetAt(int16_t x, int16_t y);
217 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
218 Tower *GetTowerByType(uint8_t towerType);
219 int GetTowerCosts(uint8_t towerType);
220 float TowerGetMaxHealth(Tower *tower);
221 void TowerDraw();
222 void TowerUpdate();
223 void TowerDrawHealthBars(Camera3D camera);
224 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
225 
226 //# Particles
227 void ParticleInit();
228 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
229 void ParticleUpdate();
230 void ParticleDraw();
231 
232 //# Projectiles
233 void ProjectileInit();
234 void ProjectileDraw();
235 void ProjectileUpdate();
236 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage);
237 
238 //# Pathfinding map
239 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
240 float PathFindingGetDistance(int mapX, int mapY);
241 Vector2 PathFindingGetGradient(Vector3 world);
242 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
243 void PathFindingMapUpdate();
244 void PathFindingMapDraw();
245 
246 //# UI
247 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
248 
249 //# Level
250 void DrawLevelGround(Level *level);
251 
252 //# variables
253 extern Level *currentLevel;
254 extern Enemy enemies[ENEMY_MAX_COUNT];
255 extern int enemyCount;
256 extern EnemyClassConfig enemyClassConfigs[];
257 
258 extern GUIState guiState;
259 extern GameTime gameTime;
260 extern Tower towers[TOWER_MAX_COUNT];
261 extern int towerCount;
262 
263 extern Texture2D palette, spriteSheet;
264 
265 #endif
  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}, 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 EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522   }
523 }
  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()
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 < towerCount; i++)
131   {
132     Tower *tower = &towers[i];
133     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134     {
135       continue;
136     }
137     int16_t mapX, mapY;
138     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139     // this would not work correctly and needs to be refined to allow towers covering multiple cells
140     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141     // one cell. For now.
142     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143     {
144       continue;
145     }
146     int index = mapY * width + mapX;
147     pathfindingMap.towerIndex[index] = i;
148   }
149 
150   // we start at the castle and add the castle to the queue
151   pathfindingMap.maxDistance = 0.0f;
152   pathfindingNodeQueueCount = 0;
153   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154   PathfindingNode *node = 0;
155   while ((node = PathFindingNodePop()))
156   {
157     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158     {
159       continue;
160     }
161     int index = node->y * width + node->x;
162     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163     {
164       continue;
165     }
166 
167     int deltaX = node->x - node->fromX;
168     int deltaY = node->y - node->fromY;
169     // even if the cell is blocked by a tower, we still may want to store the direction
170     // (though this might not be needed, IDK right now)
171     pathfindingMap.deltaSrc[index].x = (char) deltaX;
172     pathfindingMap.deltaSrc[index].y = (char) deltaY;
173 
174     // we skip nodes that are blocked by towers
175     if (pathfindingMap.towerIndex[index] >= 0)
176     {
177       node->distance += 8.0f;
178     }
179     pathfindingMap.distances[index] = node->distance;
180     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185   }
186 }
187 
188 void PathFindingMapDraw()
189 {
190   float cellSize = pathfindingMap.scale * 0.9f;
191   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192   for (int x = 0; x < pathfindingMap.width; x++)
193   {
194     for (int y = 0; y < pathfindingMap.height; y++)
195     {
196       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200       // animate the distance "wave" to show how the pathfinding algorithm expands
201       // from the castle
202       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203       {
204         color = BLACK;
205       }
206       DrawCube(position, cellSize, 0.1f, cellSize, color);
207     }
208   }
209 }
210 
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213   int16_t mapX, mapY;
214   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215   {
216     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217     return (Vector2){(float)-delta.x, (float)-delta.y};
218   }
219   // fallback to a simple gradient calculation
220   float n = PathFindingGetDistance(mapX, mapY - 1);
221   float s = PathFindingGetDistance(mapX, mapY + 1);
222   float w = PathFindingGetDistance(mapX - 1, mapY);
223   float e = PathFindingGetDistance(mapX + 1, mapY);
224   return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 void ProjectileInit()
  8 {
  9   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 10   {
 11     projectiles[i] = (Projectile){0};
 12   }
 13 }
 14 
 15 void ProjectileDraw()
 16 {
 17   for (int i = 0; i < projectileCount; i++)
 18   {
 19     Projectile projectile = projectiles[i];
 20     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 21     {
 22       continue;
 23     }
 24     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 25     if (transition >= 1.0f)
 26     {
 27       continue;
 28     }
 29     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 30     {
 31       float t = transition + transitionOffset * 0.3f;
 32       if (t > 1.0f)
 33       {
 34         break;
 35       }
 36       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 37       Color color = RED;
 38       if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 39       {
 40         // make tip red but quickly fade to brown
 41         color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 42         // fake a ballista flight path using parabola equation
 43         float parabolaT = t - 0.5f;
 44         parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 45         position.y += 0.15f * parabolaT * projectile.distance;
 46       }
 47 
 48       float size = 0.06f * (transitionOffset + 0.25f);
 49       DrawCube(position, size, size, size, color);
 50     }
 51   }
 52 }
 53 
 54 void ProjectileUpdate()
 55 {
 56   for (int i = 0; i < projectileCount; i++)
 57   {
 58     Projectile *projectile = &projectiles[i];
 59     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 60     {
 61       continue;
 62     }
 63     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 64     if (transition >= 1.0f)
 65     {
 66       projectile->projectileType = PROJECTILE_TYPE_NONE;
 67       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 68       if (enemy)
 69       {
 70         EnemyAddDamage(enemy, projectile->damage);
 71       }
 72       continue;
 73     }
 74   }
 75 }
 76 
 77 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage)
 78 {
 79   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       projectile->projectileType = projectileType;
 85       projectile->shootTime = gameTime.time;
 86       float distance = Vector3Distance(position, target);
 87       projectile->arrivalTime = gameTime.time + distance / speed;
 88       projectile->damage = damage;
 89       projectile->position = position;
 90       projectile->target = target;
 91       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
 92       projectile->distance = distance;
 93       projectile->targetEnemy = EnemyGetId(enemy);
 94       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 95       return projectile;
 96     }
 97   }
 98   return 0;
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 Tower towers[TOWER_MAX_COUNT];
  5 int towerCount = 0;
  6 
  7 Model towerModels[TOWER_TYPE_COUNT];
  8 
  9 // definition of our archer unit
 10 SpriteUnit archerUnit = {
 11     .srcRect = {0, 0, 16, 16},
 12     .offset = {7, 1},
 13     .frameCount = 1,
 14     .frameDuration = 0.0f,
 15     .srcWeaponIdleRect = {16, 0, 6, 16},
 16     .srcWeaponIdleOffset = {8, 0},
 17     .srcWeaponCooldownRect = {22, 0, 11, 16},
 18     .srcWeaponCooldownOffset = {10, 0},
 19 };
 20 
 21 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 22 {
 23   float xScale = flip ? -1.0f : 1.0f;
 24   Camera3D camera = currentLevel->camera;
 25   float size = 0.5f;
 26   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 27   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 28   // we want the sprite to face the camera, so we need to calculate the up vector
 29   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 30   Vector3 up = {0, 1, 0};
 31   Vector3 right = Vector3CrossProduct(forward, up);
 32   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 33 
 34   Rectangle srcRect = unit.srcRect;
 35   if (unit.frameCount > 1)
 36   {
 37     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 38   }
 39   if (flip)
 40   {
 41     srcRect.x += srcRect.width;
 42     srcRect.width = -srcRect.width;
 43   }
 44   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 45 
 46   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 47   {
 48     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 49     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 50     srcRect = unit.srcWeaponCooldownRect;
 51     if (flip)
 52     {
 53       // position.x = flip * scale.x * 0.5f;
 54       srcRect.x += srcRect.width;
 55       srcRect.width = -srcRect.width;
 56       offset.x = scale.x - offset.x;
 57     }
 58     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 59   }
 60   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
 61   {
 62     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
 63     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
 64     srcRect = unit.srcWeaponIdleRect;
 65     if (flip)
 66     {
 67       // position.x = flip * scale.x * 0.5f;
 68       srcRect.x += srcRect.width;
 69       srcRect.width = -srcRect.width;
 70       offset.x = scale.x - offset.x;
 71     }
 72     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 73   }
 74 }
 75 
 76 void TowerInit()
 77 {
 78   for (int i = 0; i < TOWER_MAX_COUNT; i++)
 79   {
 80     towers[i] = (Tower){0};
 81   }
 82   towerCount = 0;
 83 
 84   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
 85   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
 86 
 87   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
 88   {
 89     if (towerModels[i].materials)
 90     {
 91       // assign the palette texture to the material of the model (0 is not used afaik)
 92       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 93     }
 94   }
 95 }
 96 
 97 static void TowerGunUpdate(Tower *tower)
 98 {
 99   if (tower->cooldown <= 0)
100   {
101     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
102     if (enemy)
103     {
104       tower->cooldown = 0.5f;
105       // shoot the enemy; determine future position of the enemy
106       float bulletSpeed = 4.0f;
107       float bulletDamage = 3.0f;
108       Vector2 velocity = enemy->simVelocity;
109       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
110       Vector2 towerPosition = {tower->x, tower->y};
111       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
112       for (int i = 0; i < 8; i++) {
113         velocity = enemy->simVelocity;
114         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
115         float distance = Vector2Distance(towerPosition, futurePosition);
116         float eta2 = distance / bulletSpeed;
117         if (fabs(eta - eta2) < 0.01f) {
118           break;
119         }
120         eta = (eta2 + eta) * 0.5f;
121       }
122       ProjectileTryAdd(PROJECTILE_TYPE_ARROW, enemy, 
123         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
124         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
125         bulletSpeed, bulletDamage);
126       enemy->futureDamage += bulletDamage;
127       tower->lastTargetPosition = futurePosition;
128     }
129   }
130   else
131   {
132     tower->cooldown -= gameTime.deltaTime;
133   }
134 }
135 
136 Tower *TowerGetAt(int16_t x, int16_t y)
137 {
138   for (int i = 0; i < towerCount; i++)
139   {
140     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
141     {
142       return &towers[i];
143     }
144   }
145   return 0;
146 }
147 
148 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
149 {
150   if (towerCount >= TOWER_MAX_COUNT)
151   {
152     return 0;
153   }
154 
155   Tower *tower = TowerGetAt(x, y);
156   if (tower)
157   {
158     return 0;
159   }
160 
161   tower = &towers[towerCount++];
162   tower->x = x;
163   tower->y = y;
164   tower->towerType = towerType;
165   tower->cooldown = 0.0f;
166   tower->damage = 0.0f;
167   return tower;
168 }
169 
170 Tower *GetTowerByType(uint8_t towerType)
171 {
172   for (int i = 0; i < towerCount; i++)
173   {
174     if (towers[i].towerType == towerType)
175     {
176       return &towers[i];
177     }
178   }
179   return 0;
180 }
181 
182 int GetTowerCosts(uint8_t towerType)
183 {
184   switch (towerType)
185   {
186   case TOWER_TYPE_BASE:
187     return 0;
188   case TOWER_TYPE_GUN:
189     return 6;
190   case TOWER_TYPE_WALL:
191     return 2;
192   }
193   return 0;
194 }
195 
196 float TowerGetMaxHealth(Tower *tower)
197 {
198   switch (tower->towerType)
199   {
200   case TOWER_TYPE_BASE:
201     return 10.0f;
202   case TOWER_TYPE_GUN:
203     return 3.0f;
204   case TOWER_TYPE_WALL:
205     return 5.0f;
206   }
207   return 0.0f;
208 }
209 
210 void TowerDraw()
211 {
212   for (int i = 0; i < towerCount; i++)
213   {
214     Tower tower = towers[i];
215     if (tower.towerType == TOWER_TYPE_NONE)
216     {
217       continue;
218     }
219 
220     switch (tower.towerType)
221     {
222     case TOWER_TYPE_GUN:
223       {
224         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
225         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
226         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
227         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
228           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
229       }
230       break;
231     default:
232       if (towerModels[tower.towerType].materials)
233       {
234         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
235       } else {
236         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
237       }
238       break;
239     }
240   }
241 }
242 
243 void TowerUpdate()
244 {
245   for (int i = 0; i < towerCount; i++)
246   {
247     Tower *tower = &towers[i];
248     switch (tower->towerType)
249     {
250     case TOWER_TYPE_GUN:
251       TowerGunUpdate(tower);
252       break;
253     }
254   }
255 }
256 
257 void TowerDrawHealthBars(Camera3D camera)
258 {
259   for (int i = 0; i < towerCount; i++)
260   {
261     Tower *tower = &towers[i];
262     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
263     {
264       continue;
265     }
266     
267     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
268     float maxHealth = TowerGetMaxHealth(tower);
269     float health = maxHealth - tower->damage;
270     float healthRatio = health / maxHealth;
271     
272     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
273   }
274 }
  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
Random rotation, yay!

Amazing! There is almost no visible pattern any more! But it's all the same model. Let's add a second model to the decoration set and choose randomly between the two models:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 //# Variables
  7 GUIState guiState = {0};
  8 GameTime gameTime = {0};
  9 
 10 Model floorTileAModel = {0};
 11 Model floorTileBModel = {0};
12 Model treeModel[2] = {0};
13 Model grassPatchModel[1] = {0}; 14 15 Texture2D palette, spriteSheet; 16 17 Level levels[] = { 18 [0] = { 19 .state = LEVEL_STATE_BUILDING, 20 .initialGold = 20, 21 .waves[0] = { 22 .enemyType = ENEMY_TYPE_MINION, 23 .wave = 0, 24 .count = 10, 25 .interval = 2.5f, 26 .delay = 1.0f, 27 .spawnPosition = {0, 6}, 28 }, 29 .waves[1] = { 30 .enemyType = ENEMY_TYPE_MINION, 31 .wave = 1, 32 .count = 20, 33 .interval = 1.5f, 34 .delay = 1.0f, 35 .spawnPosition = {0, 6}, 36 }, 37 .waves[2] = { 38 .enemyType = ENEMY_TYPE_MINION, 39 .wave = 2, 40 .count = 30, 41 .interval = 1.2f, 42 .delay = 1.0f, 43 .spawnPosition = {0, 6}, 44 } 45 }, 46 }; 47 48 Level *currentLevel = levels; 49 50 //# Game 51 52 static Model LoadGLBModel(char *filename) 53 { 54 Model model = LoadModel(TextFormat("data/%s.glb",filename)); 55 if (model.materialCount > 1) 56 { 57 model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 58 } 59 return model; 60 } 61 62 void LoadAssets() 63 { 64 // load a sprite sheet that contains all units 65 spriteSheet = LoadTexture("data/spritesheet.png"); 66 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR); 67 68 // we'll use a palette texture to colorize the all buildings and environment art 69 palette = LoadTexture("data/palette.png"); 70 // The texture uses gradients on very small space, so we'll enable bilinear filtering 71 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 72 73 floorTileAModel = LoadGLBModel("floor-tile-a");
74 floorTileBModel = LoadGLBModel("floor-tile-b"); 75 treeModel[0] = LoadGLBModel("leaftree-large-1-a"); 76 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
77 grassPatchModel[0] = LoadGLBModel("grass-patch-1"); 78 } 79 80 void InitLevel(Level *level) 81 { 82 level->seed = (int)(GetTime() * 100.0f); 83 84 TowerInit(); 85 EnemyInit(); 86 ProjectileInit(); 87 ParticleInit(); 88 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 89 90 level->placementMode = 0; 91 level->state = LEVEL_STATE_BUILDING; 92 level->nextState = LEVEL_STATE_NONE; 93 level->playerGold = level->initialGold; 94 level->currentWave = 0; 95 96 Camera *camera = &level->camera; 97 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 98 camera->target = (Vector3){0.0f, 0.0f, 0.0f}; 99 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 100 camera->fovy = 10.0f; 101 camera->projection = CAMERA_ORTHOGRAPHIC; 102 } 103 104 void DrawLevelHud(Level *level) 105 { 106 const char *text = TextFormat("Gold: %d", level->playerGold); 107 Font font = GetFontDefault(); 108 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 109 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 110 } 111 112 void DrawLevelReportLostWave(Level *level) 113 { 114 BeginMode3D(level->camera); 115 DrawLevelGround(level); 116 TowerDraw(); 117 EnemyDraw(); 118 ProjectileDraw(); 119 ParticleDraw(); 120 guiState.isBlocked = 0; 121 EndMode3D(); 122 123 TowerDrawHealthBars(level->camera); 124 125 const char *text = "Wave lost"; 126 int textWidth = MeasureText(text, 20); 127 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 128 129 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 130 { 131 level->nextState = LEVEL_STATE_RESET; 132 } 133 } 134 135 int HasLevelNextWave(Level *level) 136 { 137 for (int i = 0; i < 10; i++) 138 { 139 EnemyWave *wave = &level->waves[i]; 140 if (wave->wave == level->currentWave) 141 { 142 return 1; 143 } 144 } 145 return 0; 146 } 147 148 void DrawLevelReportWonWave(Level *level) 149 { 150 BeginMode3D(level->camera); 151 DrawLevelGround(level); 152 TowerDraw(); 153 EnemyDraw(); 154 ProjectileDraw(); 155 ParticleDraw(); 156 guiState.isBlocked = 0; 157 EndMode3D(); 158 159 TowerDrawHealthBars(level->camera); 160 161 const char *text = "Wave won"; 162 int textWidth = MeasureText(text, 20); 163 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 164 165 166 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 167 { 168 level->nextState = LEVEL_STATE_RESET; 169 } 170 171 if (HasLevelNextWave(level)) 172 { 173 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 174 { 175 level->nextState = LEVEL_STATE_BUILDING; 176 } 177 } 178 else { 179 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 180 { 181 level->nextState = LEVEL_STATE_WON_LEVEL; 182 } 183 } 184 } 185 186 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 187 { 188 static ButtonState buttonStates[8] = {0}; 189 int cost = GetTowerCosts(towerType); 190 const char *text = TextFormat("%s: %d", name, cost); 191 buttonStates[towerType].isSelected = level->placementMode == towerType; 192 buttonStates[towerType].isDisabled = level->playerGold < cost; 193 if (Button(text, x, y, width, height, &buttonStates[towerType])) 194 { 195 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 196 } 197 } 198 199 float GetRandomFloat(float min, float max) 200 { 201 int random = GetRandomValue(0, 0xfffffff); 202 return ((float)random / (float)0xfffffff) * (max - min) + min; 203 } 204 205 void DrawLevelGround(Level *level) 206 { 207 // draw checkerboard ground pattern 208 for (int x = -5; x <= 5; x += 1) 209 { 210 for (int y = -5; y <= 5; y += 1) 211 { 212 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 213 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 214 }
215 } 216 217 Model borderModels[2]; 218 borderModels[0] = grassPatchModel[0]; 219 borderModels[1] = treeModel[1];
220 221 // draw grass patches around the edges 222 const Vector3 up = {0, 1, 0}; 223 const int layerCount = 2; 224 unsigned int oldSeed = GetRandomValue(0, 0xfffffff); 225 const float wiggle = 0.25f; 226 SetRandomSeed(level->seed); 227 for (int layer = 0; layer < layerCount; layer++) 228 { 229 int layerPos = 6 + layer; 230 for (int x = -6 + layer; x <= 6 + layer; x += 1) 231 {
232 DrawModelEx(borderModels[GetRandomValue(0, 1)],
233 (Vector3){x + GetRandomFloat(-wiggle, wiggle), 0.0f, -layerPos + GetRandomFloat(-wiggle, wiggle)}, 234 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
235 DrawModelEx(borderModels[GetRandomValue(0, 1)],
236 (Vector3){x + GetRandomFloat(-wiggle, wiggle), 0.0f, layerPos + GetRandomFloat(-wiggle, wiggle)}, 237 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 238 } 239 240 for (int z = -5 + layer; z <= 5 + layer; z += 1) 241 {
242 DrawModelEx(borderModels[GetRandomValue(0, 1)],
243 (Vector3){-layerPos + GetRandomFloat(-wiggle, wiggle), 0.0f, z + GetRandomFloat(-wiggle, wiggle)}, 244 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
245 DrawModelEx(borderModels[GetRandomValue(0, 1)],
246 (Vector3){layerPos + GetRandomFloat(-wiggle, wiggle), 0.0f, z + GetRandomFloat(-wiggle, wiggle)}, 247 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 248 } 249 } 250 SetRandomSeed(oldSeed); 251 } 252 253 void DrawLevelBuildingState(Level *level) 254 { 255 BeginMode3D(level->camera); 256 DrawLevelGround(level); 257 TowerDraw(); 258 EnemyDraw(); 259 ProjectileDraw(); 260 ParticleDraw(); 261 262 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 263 float planeDistance = ray.position.y / -ray.direction.y; 264 float planeX = ray.direction.x * planeDistance + ray.position.x; 265 float planeY = ray.direction.z * planeDistance + ray.position.z; 266 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 267 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 268 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5) 269 { 270 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 271 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 272 { 273 if (TowerTryAdd(level->placementMode, mapX, mapY)) 274 { 275 level->playerGold -= GetTowerCosts(level->placementMode); 276 level->placementMode = TOWER_TYPE_NONE; 277 } 278 } 279 } 280 281 guiState.isBlocked = 0; 282 283 EndMode3D(); 284 285 TowerDrawHealthBars(level->camera); 286 287 static ButtonState buildWallButtonState = {0}; 288 static ButtonState buildGunButtonState = {0}; 289 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 290 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN; 291 292 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 293 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_GUN, "Archer"); 294 295 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 296 { 297 level->nextState = LEVEL_STATE_RESET; 298 } 299 300 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 301 { 302 level->nextState = LEVEL_STATE_BATTLE; 303 } 304 305 const char *text = "Building phase"; 306 int textWidth = MeasureText(text, 20); 307 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 308 } 309 310 void InitBattleStateConditions(Level *level) 311 { 312 level->state = LEVEL_STATE_BATTLE; 313 level->nextState = LEVEL_STATE_NONE; 314 level->waveEndTimer = 0.0f; 315 for (int i = 0; i < 10; i++) 316 { 317 EnemyWave *wave = &level->waves[i]; 318 wave->spawned = 0; 319 wave->timeToSpawnNext = wave->delay; 320 } 321 } 322 323 void DrawLevelBattleState(Level *level) 324 { 325 BeginMode3D(level->camera); 326 DrawLevelGround(level); 327 TowerDraw(); 328 EnemyDraw(); 329 ProjectileDraw(); 330 ParticleDraw(); 331 guiState.isBlocked = 0; 332 EndMode3D(); 333 334 EnemyDrawHealthbars(level->camera); 335 TowerDrawHealthBars(level->camera); 336 337 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 338 { 339 level->nextState = LEVEL_STATE_RESET; 340 } 341 342 int maxCount = 0; 343 int remainingCount = 0; 344 for (int i = 0; i < 10; i++) 345 { 346 EnemyWave *wave = &level->waves[i]; 347 if (wave->wave != level->currentWave) 348 { 349 continue; 350 } 351 maxCount += wave->count; 352 remainingCount += wave->count - wave->spawned; 353 } 354 int aliveCount = EnemyCount(); 355 remainingCount += aliveCount; 356 357 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 358 int textWidth = MeasureText(text, 20); 359 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 360 } 361 362 void DrawLevel(Level *level) 363 { 364 switch (level->state) 365 { 366 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 367 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 368 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 369 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 370 default: break; 371 } 372 373 DrawLevelHud(level); 374 } 375 376 void UpdateLevel(Level *level) 377 { 378 if (level->state == LEVEL_STATE_BATTLE) 379 { 380 int activeWaves = 0; 381 for (int i = 0; i < 10; i++) 382 { 383 EnemyWave *wave = &level->waves[i]; 384 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 385 { 386 continue; 387 } 388 activeWaves++; 389 wave->timeToSpawnNext -= gameTime.deltaTime; 390 if (wave->timeToSpawnNext <= 0.0f) 391 { 392 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 393 if (enemy) 394 { 395 wave->timeToSpawnNext = wave->interval; 396 wave->spawned++; 397 } 398 } 399 } 400 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 401 level->waveEndTimer += gameTime.deltaTime; 402 if (level->waveEndTimer >= 2.0f) 403 { 404 level->nextState = LEVEL_STATE_LOST_WAVE; 405 } 406 } 407 else if (activeWaves == 0 && EnemyCount() == 0) 408 { 409 level->waveEndTimer += gameTime.deltaTime; 410 if (level->waveEndTimer >= 2.0f) 411 { 412 level->nextState = LEVEL_STATE_WON_WAVE; 413 } 414 } 415 } 416 417 PathFindingMapUpdate(); 418 EnemyUpdate(); 419 TowerUpdate(); 420 ProjectileUpdate(); 421 ParticleUpdate(); 422 423 if (level->nextState == LEVEL_STATE_RESET) 424 { 425 InitLevel(level); 426 } 427 428 if (level->nextState == LEVEL_STATE_BATTLE) 429 { 430 InitBattleStateConditions(level); 431 } 432 433 if (level->nextState == LEVEL_STATE_WON_WAVE) 434 { 435 level->currentWave++; 436 level->state = LEVEL_STATE_WON_WAVE; 437 } 438 439 if (level->nextState == LEVEL_STATE_LOST_WAVE) 440 { 441 level->state = LEVEL_STATE_LOST_WAVE; 442 } 443 444 if (level->nextState == LEVEL_STATE_BUILDING) 445 { 446 level->state = LEVEL_STATE_BUILDING; 447 } 448 449 if (level->nextState == LEVEL_STATE_WON_LEVEL) 450 { 451 // make something of this later 452 InitLevel(level); 453 } 454 455 level->nextState = LEVEL_STATE_NONE; 456 } 457 458 float nextSpawnTime = 0.0f; 459 460 void ResetGame() 461 { 462 InitLevel(currentLevel); 463 } 464 465 void InitGame() 466 { 467 TowerInit(); 468 EnemyInit(); 469 ProjectileInit(); 470 ParticleInit(); 471 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 472 473 currentLevel = levels; 474 InitLevel(currentLevel); 475 } 476 477 //# Immediate GUI functions 478 479 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 480 { 481 const float healthBarHeight = 6.0f; 482 const float healthBarOffset = 15.0f; 483 const float inset = 2.0f; 484 const float innerWidth = healthBarWidth - inset * 2; 485 const float innerHeight = healthBarHeight - inset * 2; 486 487 Vector2 screenPos = GetWorldToScreen(position, camera); 488 float centerX = screenPos.x - healthBarWidth * 0.5f; 489 float topY = screenPos.y - healthBarOffset; 490 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 491 float healthWidth = innerWidth * healthRatio; 492 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 493 } 494 495 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 496 { 497 Rectangle bounds = {x, y, width, height}; 498 int isPressed = 0; 499 int isSelected = state && state->isSelected; 500 int isDisabled = state && state->isDisabled; 501 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 502 { 503 Color color = isSelected ? DARKGRAY : GRAY; 504 DrawRectangle(x, y, width, height, color); 505 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 506 { 507 isPressed = 1; 508 } 509 guiState.isBlocked = 1; 510 } 511 else 512 { 513 Color color = isSelected ? WHITE : LIGHTGRAY; 514 DrawRectangle(x, y, width, height, color); 515 } 516 Font font = GetFontDefault(); 517 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 518 Color textColor = isDisabled ? GRAY : BLACK; 519 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 520 return isPressed; 521 } 522 523 //# Main game loop 524 525 void GameUpdate() 526 { 527 float dt = GetFrameTime(); 528 // cap maximum delta time to 0.1 seconds to prevent large time steps 529 if (dt > 0.1f) dt = 0.1f; 530 gameTime.time += dt; 531 gameTime.deltaTime = dt; 532 533 UpdateLevel(currentLevel); 534 } 535 536 int main(void) 537 { 538 int screenWidth, screenHeight; 539 GetPreferredSize(&screenWidth, &screenHeight); 540 InitWindow(screenWidth, screenHeight, "Tower defense"); 541 SetTargetFPS(30); 542 543 LoadAssets(); 544 InitGame(); 545 546 while (!WindowShouldClose()) 547 { 548 if (IsPaused()) { 549 // canvas is not visible in browser - do nothing 550 continue; 551 } 552 553 BeginDrawing(); 554 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 555 556 GameUpdate(); 557 DrawLevel(currentLevel); 558 559 EndDrawing(); 560 } 561 562 CloseWindow(); 563 564 return 0; 565 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
 30 #define TOWER_TYPE_NONE 0
 31 #define TOWER_TYPE_BASE 1
 32 #define TOWER_TYPE_GUN 2
 33 #define TOWER_TYPE_WALL 3
 34 #define TOWER_TYPE_COUNT 4
 35 
 36 typedef struct Tower
 37 {
 38   int16_t x, y;
 39   uint8_t towerType;
 40   Vector2 lastTargetPosition;
 41   float cooldown;
 42   float damage;
 43 } Tower;
 44 
 45 typedef struct GameTime
 46 {
 47   float time;
 48   float deltaTime;
 49 } GameTime;
 50 
 51 typedef struct ButtonState {
 52   char isSelected;
 53   char isDisabled;
 54 } ButtonState;
 55 
 56 typedef struct GUIState {
 57   int isBlocked;
 58 } GUIState;
 59 
 60 typedef enum LevelState
 61 {
 62   LEVEL_STATE_NONE,
 63   LEVEL_STATE_BUILDING,
 64   LEVEL_STATE_BATTLE,
 65   LEVEL_STATE_WON_WAVE,
 66   LEVEL_STATE_LOST_WAVE,
 67   LEVEL_STATE_WON_LEVEL,
 68   LEVEL_STATE_RESET,
 69 } LevelState;
 70 
 71 typedef struct EnemyWave {
 72   uint8_t enemyType;
 73   uint8_t wave;
 74   uint16_t count;
 75   float interval;
 76   float delay;
 77   Vector2 spawnPosition;
 78 
 79   uint16_t spawned;
 80   float timeToSpawnNext;
 81 } EnemyWave;
 82 
 83 typedef struct Level
 84 {
 85   int seed;
 86   LevelState state;
 87   LevelState nextState;
 88   Camera3D camera;
 89   int placementMode;
 90 
 91   int initialGold;
 92   int playerGold;
 93 
 94   EnemyWave waves[10];
 95   int currentWave;
 96   float waveEndTimer;
 97 } Level;
 98 
 99 typedef struct DeltaSrc
100 {
101   char x, y;
102 } DeltaSrc;
103 
104 typedef struct PathfindingMap
105 {
106   int width, height;
107   float scale;
108   float *distances;
109   long *towerIndex; 
110   DeltaSrc *deltaSrc;
111   float maxDistance;
112   Matrix toMapSpace;
113   Matrix toWorldSpace;
114 } PathfindingMap;
115 
116 // when we execute the pathfinding algorithm, we need to store the active nodes
117 // in a queue. Each node has a position, a distance from the start, and the
118 // position of the node that we came from.
119 typedef struct PathfindingNode
120 {
121   int16_t x, y, fromX, fromY;
122   float distance;
123 } PathfindingNode;
124 
125 typedef struct EnemyId
126 {
127   uint16_t index;
128   uint16_t generation;
129 } EnemyId;
130 
131 typedef struct EnemyClassConfig
132 {
133   float speed;
134   float health;
135   float radius;
136   float maxAcceleration;
137   float requiredContactTime;
138   float explosionDamage;
139   float explosionRange;
140   float explosionPushbackPower;
141   int goldValue;
142 } EnemyClassConfig;
143 
144 typedef struct Enemy
145 {
146   int16_t currentX, currentY;
147   int16_t nextX, nextY;
148   Vector2 simPosition;
149   Vector2 simVelocity;
150   uint16_t generation;
151   float walkedDistance;
152   float startMovingTime;
153   float damage, futureDamage;
154   float contactTime;
155   uint8_t enemyType;
156   uint8_t movePathCount;
157   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
158 } Enemy;
159 
160 // a unit that uses sprites to be drawn
161 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
162 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
163 typedef struct SpriteUnit
164 {
165   Rectangle srcRect;
166   Vector2 offset;
167   int frameCount;
168   float frameDuration;
169   Rectangle srcWeaponIdleRect;
170   Vector2 srcWeaponIdleOffset;
171   Rectangle srcWeaponCooldownRect;
172   Vector2 srcWeaponCooldownOffset;
173 } SpriteUnit;
174 
175 #define PROJECTILE_MAX_COUNT 1200
176 #define PROJECTILE_TYPE_NONE 0
177 #define PROJECTILE_TYPE_ARROW 1
178 
179 typedef struct Projectile
180 {
181   uint8_t projectileType;
182   float shootTime;
183   float arrivalTime;
184   float distance;
185   float damage;
186   Vector3 position;
187   Vector3 target;
188   Vector3 directionNormal;
189   EnemyId targetEnemy;
190 } Projectile;
191 
192 //# Function declarations
193 float TowerGetMaxHealth(Tower *tower);
194 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
195 int EnemyAddDamage(Enemy *enemy, float damage);
196 
197 //# Enemy functions
198 void EnemyInit();
199 void EnemyDraw();
200 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
201 void EnemyUpdate();
202 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
203 float EnemyGetMaxHealth(Enemy *enemy);
204 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
205 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
206 EnemyId EnemyGetId(Enemy *enemy);
207 Enemy *EnemyTryResolve(EnemyId enemyId);
208 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
209 int EnemyAddDamage(Enemy *enemy, float damage);
210 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
211 int EnemyCount();
212 void EnemyDrawHealthbars(Camera3D camera);
213 
214 //# Tower functions
215 void TowerInit();
216 Tower *TowerGetAt(int16_t x, int16_t y);
217 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
218 Tower *GetTowerByType(uint8_t towerType);
219 int GetTowerCosts(uint8_t towerType);
220 float TowerGetMaxHealth(Tower *tower);
221 void TowerDraw();
222 void TowerUpdate();
223 void TowerDrawHealthBars(Camera3D camera);
224 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
225 
226 //# Particles
227 void ParticleInit();
228 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
229 void ParticleUpdate();
230 void ParticleDraw();
231 
232 //# Projectiles
233 void ProjectileInit();
234 void ProjectileDraw();
235 void ProjectileUpdate();
236 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage);
237 
238 //# Pathfinding map
239 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
240 float PathFindingGetDistance(int mapX, int mapY);
241 Vector2 PathFindingGetGradient(Vector3 world);
242 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
243 void PathFindingMapUpdate();
244 void PathFindingMapDraw();
245 
246 //# UI
247 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
248 
249 //# Level
250 void DrawLevelGround(Level *level);
251 
252 //# variables
253 extern Level *currentLevel;
254 extern Enemy enemies[ENEMY_MAX_COUNT];
255 extern int enemyCount;
256 extern EnemyClassConfig enemyClassConfigs[];
257 
258 extern GUIState guiState;
259 extern GameTime gameTime;
260 extern Tower towers[TOWER_MAX_COUNT];
261 extern int towerCount;
262 
263 extern Texture2D palette, spriteSheet;
264 
265 #endif
  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}, 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 EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522   }
523 }
  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()
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 < towerCount; i++)
131   {
132     Tower *tower = &towers[i];
133     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134     {
135       continue;
136     }
137     int16_t mapX, mapY;
138     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139     // this would not work correctly and needs to be refined to allow towers covering multiple cells
140     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141     // one cell. For now.
142     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143     {
144       continue;
145     }
146     int index = mapY * width + mapX;
147     pathfindingMap.towerIndex[index] = i;
148   }
149 
150   // we start at the castle and add the castle to the queue
151   pathfindingMap.maxDistance = 0.0f;
152   pathfindingNodeQueueCount = 0;
153   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154   PathfindingNode *node = 0;
155   while ((node = PathFindingNodePop()))
156   {
157     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158     {
159       continue;
160     }
161     int index = node->y * width + node->x;
162     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163     {
164       continue;
165     }
166 
167     int deltaX = node->x - node->fromX;
168     int deltaY = node->y - node->fromY;
169     // even if the cell is blocked by a tower, we still may want to store the direction
170     // (though this might not be needed, IDK right now)
171     pathfindingMap.deltaSrc[index].x = (char) deltaX;
172     pathfindingMap.deltaSrc[index].y = (char) deltaY;
173 
174     // we skip nodes that are blocked by towers
175     if (pathfindingMap.towerIndex[index] >= 0)
176     {
177       node->distance += 8.0f;
178     }
179     pathfindingMap.distances[index] = node->distance;
180     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185   }
186 }
187 
188 void PathFindingMapDraw()
189 {
190   float cellSize = pathfindingMap.scale * 0.9f;
191   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192   for (int x = 0; x < pathfindingMap.width; x++)
193   {
194     for (int y = 0; y < pathfindingMap.height; y++)
195     {
196       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200       // animate the distance "wave" to show how the pathfinding algorithm expands
201       // from the castle
202       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203       {
204         color = BLACK;
205       }
206       DrawCube(position, cellSize, 0.1f, cellSize, color);
207     }
208   }
209 }
210 
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213   int16_t mapX, mapY;
214   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215   {
216     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217     return (Vector2){(float)-delta.x, (float)-delta.y};
218   }
219   // fallback to a simple gradient calculation
220   float n = PathFindingGetDistance(mapX, mapY - 1);
221   float s = PathFindingGetDistance(mapX, mapY + 1);
222   float w = PathFindingGetDistance(mapX - 1, mapY);
223   float e = PathFindingGetDistance(mapX + 1, mapY);
224   return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 void ProjectileInit()
  8 {
  9   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 10   {
 11     projectiles[i] = (Projectile){0};
 12   }
 13 }
 14 
 15 void ProjectileDraw()
 16 {
 17   for (int i = 0; i < projectileCount; i++)
 18   {
 19     Projectile projectile = projectiles[i];
 20     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 21     {
 22       continue;
 23     }
 24     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 25     if (transition >= 1.0f)
 26     {
 27       continue;
 28     }
 29     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 30     {
 31       float t = transition + transitionOffset * 0.3f;
 32       if (t > 1.0f)
 33       {
 34         break;
 35       }
 36       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 37       Color color = RED;
 38       if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 39       {
 40         // make tip red but quickly fade to brown
 41         color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 42         // fake a ballista flight path using parabola equation
 43         float parabolaT = t - 0.5f;
 44         parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 45         position.y += 0.15f * parabolaT * projectile.distance;
 46       }
 47 
 48       float size = 0.06f * (transitionOffset + 0.25f);
 49       DrawCube(position, size, size, size, color);
 50     }
 51   }
 52 }
 53 
 54 void ProjectileUpdate()
 55 {
 56   for (int i = 0; i < projectileCount; i++)
 57   {
 58     Projectile *projectile = &projectiles[i];
 59     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 60     {
 61       continue;
 62     }
 63     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 64     if (transition >= 1.0f)
 65     {
 66       projectile->projectileType = PROJECTILE_TYPE_NONE;
 67       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 68       if (enemy)
 69       {
 70         EnemyAddDamage(enemy, projectile->damage);
 71       }
 72       continue;
 73     }
 74   }
 75 }
 76 
 77 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage)
 78 {
 79   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       projectile->projectileType = projectileType;
 85       projectile->shootTime = gameTime.time;
 86       float distance = Vector3Distance(position, target);
 87       projectile->arrivalTime = gameTime.time + distance / speed;
 88       projectile->damage = damage;
 89       projectile->position = position;
 90       projectile->target = target;
 91       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
 92       projectile->distance = distance;
 93       projectile->targetEnemy = EnemyGetId(enemy);
 94       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 95       return projectile;
 96     }
 97   }
 98   return 0;
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 Tower towers[TOWER_MAX_COUNT];
  5 int towerCount = 0;
  6 
  7 Model towerModels[TOWER_TYPE_COUNT];
  8 
  9 // definition of our archer unit
 10 SpriteUnit archerUnit = {
 11     .srcRect = {0, 0, 16, 16},
 12     .offset = {7, 1},
 13     .frameCount = 1,
 14     .frameDuration = 0.0f,
 15     .srcWeaponIdleRect = {16, 0, 6, 16},
 16     .srcWeaponIdleOffset = {8, 0},
 17     .srcWeaponCooldownRect = {22, 0, 11, 16},
 18     .srcWeaponCooldownOffset = {10, 0},
 19 };
 20 
 21 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 22 {
 23   float xScale = flip ? -1.0f : 1.0f;
 24   Camera3D camera = currentLevel->camera;
 25   float size = 0.5f;
 26   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 27   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 28   // we want the sprite to face the camera, so we need to calculate the up vector
 29   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 30   Vector3 up = {0, 1, 0};
 31   Vector3 right = Vector3CrossProduct(forward, up);
 32   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 33 
 34   Rectangle srcRect = unit.srcRect;
 35   if (unit.frameCount > 1)
 36   {
 37     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 38   }
 39   if (flip)
 40   {
 41     srcRect.x += srcRect.width;
 42     srcRect.width = -srcRect.width;
 43   }
 44   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 45 
 46   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 47   {
 48     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 49     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 50     srcRect = unit.srcWeaponCooldownRect;
 51     if (flip)
 52     {
 53       // position.x = flip * scale.x * 0.5f;
 54       srcRect.x += srcRect.width;
 55       srcRect.width = -srcRect.width;
 56       offset.x = scale.x - offset.x;
 57     }
 58     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 59   }
 60   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
 61   {
 62     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
 63     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
 64     srcRect = unit.srcWeaponIdleRect;
 65     if (flip)
 66     {
 67       // position.x = flip * scale.x * 0.5f;
 68       srcRect.x += srcRect.width;
 69       srcRect.width = -srcRect.width;
 70       offset.x = scale.x - offset.x;
 71     }
 72     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 73   }
 74 }
 75 
 76 void TowerInit()
 77 {
 78   for (int i = 0; i < TOWER_MAX_COUNT; i++)
 79   {
 80     towers[i] = (Tower){0};
 81   }
 82   towerCount = 0;
 83 
 84   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
 85   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
 86 
 87   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
 88   {
 89     if (towerModels[i].materials)
 90     {
 91       // assign the palette texture to the material of the model (0 is not used afaik)
 92       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 93     }
 94   }
 95 }
 96 
 97 static void TowerGunUpdate(Tower *tower)
 98 {
 99   if (tower->cooldown <= 0)
100   {
101     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
102     if (enemy)
103     {
104       tower->cooldown = 0.5f;
105       // shoot the enemy; determine future position of the enemy
106       float bulletSpeed = 4.0f;
107       float bulletDamage = 3.0f;
108       Vector2 velocity = enemy->simVelocity;
109       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
110       Vector2 towerPosition = {tower->x, tower->y};
111       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
112       for (int i = 0; i < 8; i++) {
113         velocity = enemy->simVelocity;
114         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
115         float distance = Vector2Distance(towerPosition, futurePosition);
116         float eta2 = distance / bulletSpeed;
117         if (fabs(eta - eta2) < 0.01f) {
118           break;
119         }
120         eta = (eta2 + eta) * 0.5f;
121       }
122       ProjectileTryAdd(PROJECTILE_TYPE_ARROW, enemy, 
123         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
124         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
125         bulletSpeed, bulletDamage);
126       enemy->futureDamage += bulletDamage;
127       tower->lastTargetPosition = futurePosition;
128     }
129   }
130   else
131   {
132     tower->cooldown -= gameTime.deltaTime;
133   }
134 }
135 
136 Tower *TowerGetAt(int16_t x, int16_t y)
137 {
138   for (int i = 0; i < towerCount; i++)
139   {
140     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
141     {
142       return &towers[i];
143     }
144   }
145   return 0;
146 }
147 
148 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
149 {
150   if (towerCount >= TOWER_MAX_COUNT)
151   {
152     return 0;
153   }
154 
155   Tower *tower = TowerGetAt(x, y);
156   if (tower)
157   {
158     return 0;
159   }
160 
161   tower = &towers[towerCount++];
162   tower->x = x;
163   tower->y = y;
164   tower->towerType = towerType;
165   tower->cooldown = 0.0f;
166   tower->damage = 0.0f;
167   return tower;
168 }
169 
170 Tower *GetTowerByType(uint8_t towerType)
171 {
172   for (int i = 0; i < towerCount; i++)
173   {
174     if (towers[i].towerType == towerType)
175     {
176       return &towers[i];
177     }
178   }
179   return 0;
180 }
181 
182 int GetTowerCosts(uint8_t towerType)
183 {
184   switch (towerType)
185   {
186   case TOWER_TYPE_BASE:
187     return 0;
188   case TOWER_TYPE_GUN:
189     return 6;
190   case TOWER_TYPE_WALL:
191     return 2;
192   }
193   return 0;
194 }
195 
196 float TowerGetMaxHealth(Tower *tower)
197 {
198   switch (tower->towerType)
199   {
200   case TOWER_TYPE_BASE:
201     return 10.0f;
202   case TOWER_TYPE_GUN:
203     return 3.0f;
204   case TOWER_TYPE_WALL:
205     return 5.0f;
206   }
207   return 0.0f;
208 }
209 
210 void TowerDraw()
211 {
212   for (int i = 0; i < towerCount; i++)
213   {
214     Tower tower = towers[i];
215     if (tower.towerType == TOWER_TYPE_NONE)
216     {
217       continue;
218     }
219 
220     switch (tower.towerType)
221     {
222     case TOWER_TYPE_GUN:
223       {
224         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
225         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
226         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
227         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
228           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
229       }
230       break;
231     default:
232       if (towerModels[tower.towerType].materials)
233       {
234         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
235       } else {
236         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
237       }
238       break;
239     }
240   }
241 }
242 
243 void TowerUpdate()
244 {
245   for (int i = 0; i < towerCount; i++)
246   {
247     Tower *tower = &towers[i];
248     switch (tower->towerType)
249     {
250     case TOWER_TYPE_GUN:
251       TowerGunUpdate(tower);
252       break;
253     }
254   }
255 }
256 
257 void TowerDrawHealthBars(Camera3D camera)
258 {
259   for (int i = 0; i < towerCount; i++)
260   {
261     Tower *tower = &towers[i];
262     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
263     {
264       continue;
265     }
266     
267     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
268     float maxHealth = TowerGetMaxHealth(tower);
269     float health = maxHealth - tower->damage;
270     float healthRatio = health / maxHealth;
271     
272     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
273   }
274 }
  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
Grass and trees - a nice combination!

What we did here is to create an array of models that we can choose from. The draw loop now selects a random model from the array and draws it. Nothing fancy, but it looks quite nice. Let's extend it with all we have so far:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 //# Variables
  7 GUIState guiState = {0};
  8 GameTime gameTime = {0};
  9 
 10 Model floorTileAModel = {0};
 11 Model floorTileBModel = {0};
 12 Model treeModel[2] = {0};
13 Model firTreeModel[2] = {0}; 14 Model rockModels[5] = {0};
15 Model grassPatchModel[1] = {0}; 16 17 Texture2D palette, spriteSheet; 18 19 Level levels[] = { 20 [0] = { 21 .state = LEVEL_STATE_BUILDING, 22 .initialGold = 20, 23 .waves[0] = { 24 .enemyType = ENEMY_TYPE_MINION, 25 .wave = 0, 26 .count = 10, 27 .interval = 2.5f, 28 .delay = 1.0f, 29 .spawnPosition = {0, 6}, 30 }, 31 .waves[1] = { 32 .enemyType = ENEMY_TYPE_MINION, 33 .wave = 1, 34 .count = 20, 35 .interval = 1.5f, 36 .delay = 1.0f, 37 .spawnPosition = {0, 6}, 38 }, 39 .waves[2] = { 40 .enemyType = ENEMY_TYPE_MINION, 41 .wave = 2, 42 .count = 30, 43 .interval = 1.2f, 44 .delay = 1.0f, 45 .spawnPosition = {0, 6}, 46 } 47 }, 48 }; 49 50 Level *currentLevel = levels; 51 52 //# Game 53 54 static Model LoadGLBModel(char *filename) 55 { 56 Model model = LoadModel(TextFormat("data/%s.glb",filename)); 57 if (model.materialCount > 1) 58 { 59 model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 60 } 61 return model; 62 } 63 64 void LoadAssets() 65 { 66 // load a sprite sheet that contains all units 67 spriteSheet = LoadTexture("data/spritesheet.png"); 68 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR); 69 70 // we'll use a palette texture to colorize the all buildings and environment art 71 palette = LoadTexture("data/palette.png"); 72 // The texture uses gradients on very small space, so we'll enable bilinear filtering 73 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 74 75 floorTileAModel = LoadGLBModel("floor-tile-a"); 76 floorTileBModel = LoadGLBModel("floor-tile-b"); 77 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
78 treeModel[1] = LoadGLBModel("leaftree-large-1-b"); 79 firTreeModel[0] = LoadGLBModel("firtree-1-a"); 80 firTreeModel[1] = LoadGLBModel("firtree-1-b"); 81 rockModels[0] = LoadGLBModel("rock-1"); 82 rockModels[1] = LoadGLBModel("rock-2"); 83 rockModels[2] = LoadGLBModel("rock-3"); 84 rockModels[3] = LoadGLBModel("rock-4"); 85 rockModels[4] = LoadGLBModel("rock-5");
86 grassPatchModel[0] = LoadGLBModel("grass-patch-1"); 87 } 88 89 void InitLevel(Level *level) 90 { 91 level->seed = (int)(GetTime() * 100.0f); 92 93 TowerInit(); 94 EnemyInit(); 95 ProjectileInit(); 96 ParticleInit(); 97 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 98 99 level->placementMode = 0; 100 level->state = LEVEL_STATE_BUILDING; 101 level->nextState = LEVEL_STATE_NONE; 102 level->playerGold = level->initialGold; 103 level->currentWave = 0; 104 105 Camera *camera = &level->camera; 106 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 107 camera->target = (Vector3){0.0f, 0.0f, 0.0f}; 108 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 109 camera->fovy = 10.0f; 110 camera->projection = CAMERA_ORTHOGRAPHIC; 111 } 112 113 void DrawLevelHud(Level *level) 114 { 115 const char *text = TextFormat("Gold: %d", level->playerGold); 116 Font font = GetFontDefault(); 117 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 118 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 119 } 120 121 void DrawLevelReportLostWave(Level *level) 122 { 123 BeginMode3D(level->camera); 124 DrawLevelGround(level); 125 TowerDraw(); 126 EnemyDraw(); 127 ProjectileDraw(); 128 ParticleDraw(); 129 guiState.isBlocked = 0; 130 EndMode3D(); 131 132 TowerDrawHealthBars(level->camera); 133 134 const char *text = "Wave lost"; 135 int textWidth = MeasureText(text, 20); 136 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 137 138 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 139 { 140 level->nextState = LEVEL_STATE_RESET; 141 } 142 } 143 144 int HasLevelNextWave(Level *level) 145 { 146 for (int i = 0; i < 10; i++) 147 { 148 EnemyWave *wave = &level->waves[i]; 149 if (wave->wave == level->currentWave) 150 { 151 return 1; 152 } 153 } 154 return 0; 155 } 156 157 void DrawLevelReportWonWave(Level *level) 158 { 159 BeginMode3D(level->camera); 160 DrawLevelGround(level); 161 TowerDraw(); 162 EnemyDraw(); 163 ProjectileDraw(); 164 ParticleDraw(); 165 guiState.isBlocked = 0; 166 EndMode3D(); 167 168 TowerDrawHealthBars(level->camera); 169 170 const char *text = "Wave won"; 171 int textWidth = MeasureText(text, 20); 172 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 173 174 175 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 176 { 177 level->nextState = LEVEL_STATE_RESET; 178 } 179 180 if (HasLevelNextWave(level)) 181 { 182 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 183 { 184 level->nextState = LEVEL_STATE_BUILDING; 185 } 186 } 187 else { 188 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 189 { 190 level->nextState = LEVEL_STATE_WON_LEVEL; 191 } 192 } 193 } 194 195 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 196 { 197 static ButtonState buttonStates[8] = {0}; 198 int cost = GetTowerCosts(towerType); 199 const char *text = TextFormat("%s: %d", name, cost); 200 buttonStates[towerType].isSelected = level->placementMode == towerType; 201 buttonStates[towerType].isDisabled = level->playerGold < cost; 202 if (Button(text, x, y, width, height, &buttonStates[towerType])) 203 { 204 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 205 } 206 } 207 208 float GetRandomFloat(float min, float max) 209 { 210 int random = GetRandomValue(0, 0xfffffff); 211 return ((float)random / (float)0xfffffff) * (max - min) + min; 212 } 213 214 void DrawLevelGround(Level *level) 215 { 216 // draw checkerboard ground pattern 217 for (int x = -5; x <= 5; x += 1) 218 { 219 for (int y = -5; y <= 5; y += 1) 220 { 221 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel; 222 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE); 223 } 224 } 225
226 Model borderModels[64]; 227 int modelCount = 0; 228 int rockCount = 5; 229 int firTreeCount = 2; 230 int leafTreeCount = 2; 231 232 borderModels[modelCount++] = grassPatchModel[0]; 233 for (int i = 0; i < rockCount; i++) 234 { 235 borderModels[modelCount++] = rockModels[i]; 236 } 237 for (int i = 0; i < firTreeCount; i++) 238 { 239 borderModels[modelCount++] = firTreeModel[i]; 240 } 241 for (int i = 0; i < leafTreeCount; i++) 242 { 243 borderModels[modelCount++] = treeModel[i]; 244 }
245 246 // draw grass patches around the edges 247 const Vector3 up = {0, 1, 0}; 248 const int layerCount = 2; 249 unsigned int oldSeed = GetRandomValue(0, 0xfffffff); 250 const float wiggle = 0.25f; 251 SetRandomSeed(level->seed); 252 for (int layer = 0; layer < layerCount; layer++) 253 { 254 int layerPos = 6 + layer; 255 for (int x = -6 + layer; x <= 6 + layer; x += 1) 256 {
257 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
258 (Vector3){x + GetRandomFloat(-wiggle, wiggle), 0.0f, -layerPos + GetRandomFloat(-wiggle, wiggle)}, 259 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
260 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
261 (Vector3){x + GetRandomFloat(-wiggle, wiggle), 0.0f, layerPos + GetRandomFloat(-wiggle, wiggle)}, 262 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 263 } 264 265 for (int z = -5 + layer; z <= 5 + layer; z += 1) 266 {
267 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
268 (Vector3){-layerPos + GetRandomFloat(-wiggle, wiggle), 0.0f, z + GetRandomFloat(-wiggle, wiggle)}, 269 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
270 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
271 (Vector3){layerPos + GetRandomFloat(-wiggle, wiggle), 0.0f, z + GetRandomFloat(-wiggle, wiggle)}, 272 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 273 } 274 } 275 SetRandomSeed(oldSeed); 276 } 277 278 void DrawLevelBuildingState(Level *level) 279 { 280 BeginMode3D(level->camera); 281 DrawLevelGround(level); 282 TowerDraw(); 283 EnemyDraw(); 284 ProjectileDraw(); 285 ParticleDraw(); 286 287 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 288 float planeDistance = ray.position.y / -ray.direction.y; 289 float planeX = ray.direction.x * planeDistance + ray.position.x; 290 float planeY = ray.direction.z * planeDistance + ray.position.z; 291 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 292 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 293 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5) 294 { 295 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 296 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 297 { 298 if (TowerTryAdd(level->placementMode, mapX, mapY)) 299 { 300 level->playerGold -= GetTowerCosts(level->placementMode); 301 level->placementMode = TOWER_TYPE_NONE; 302 } 303 } 304 } 305 306 guiState.isBlocked = 0; 307 308 EndMode3D(); 309 310 TowerDrawHealthBars(level->camera); 311 312 static ButtonState buildWallButtonState = {0}; 313 static ButtonState buildGunButtonState = {0}; 314 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 315 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN; 316 317 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 318 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_GUN, "Archer"); 319 320 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 321 { 322 level->nextState = LEVEL_STATE_RESET; 323 } 324 325 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 326 { 327 level->nextState = LEVEL_STATE_BATTLE; 328 } 329 330 const char *text = "Building phase"; 331 int textWidth = MeasureText(text, 20); 332 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 333 } 334 335 void InitBattleStateConditions(Level *level) 336 { 337 level->state = LEVEL_STATE_BATTLE; 338 level->nextState = LEVEL_STATE_NONE; 339 level->waveEndTimer = 0.0f; 340 for (int i = 0; i < 10; i++) 341 { 342 EnemyWave *wave = &level->waves[i]; 343 wave->spawned = 0; 344 wave->timeToSpawnNext = wave->delay; 345 } 346 } 347 348 void DrawLevelBattleState(Level *level) 349 { 350 BeginMode3D(level->camera); 351 DrawLevelGround(level); 352 TowerDraw(); 353 EnemyDraw(); 354 ProjectileDraw(); 355 ParticleDraw(); 356 guiState.isBlocked = 0; 357 EndMode3D(); 358 359 EnemyDrawHealthbars(level->camera); 360 TowerDrawHealthBars(level->camera); 361 362 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 363 { 364 level->nextState = LEVEL_STATE_RESET; 365 } 366 367 int maxCount = 0; 368 int remainingCount = 0; 369 for (int i = 0; i < 10; i++) 370 { 371 EnemyWave *wave = &level->waves[i]; 372 if (wave->wave != level->currentWave) 373 { 374 continue; 375 } 376 maxCount += wave->count; 377 remainingCount += wave->count - wave->spawned; 378 } 379 int aliveCount = EnemyCount(); 380 remainingCount += aliveCount; 381 382 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 383 int textWidth = MeasureText(text, 20); 384 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 385 } 386 387 void DrawLevel(Level *level) 388 { 389 switch (level->state) 390 { 391 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 392 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 393 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 394 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 395 default: break; 396 } 397 398 DrawLevelHud(level); 399 } 400 401 void UpdateLevel(Level *level) 402 { 403 if (level->state == LEVEL_STATE_BATTLE) 404 { 405 int activeWaves = 0; 406 for (int i = 0; i < 10; i++) 407 { 408 EnemyWave *wave = &level->waves[i]; 409 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 410 { 411 continue; 412 } 413 activeWaves++; 414 wave->timeToSpawnNext -= gameTime.deltaTime; 415 if (wave->timeToSpawnNext <= 0.0f) 416 { 417 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 418 if (enemy) 419 { 420 wave->timeToSpawnNext = wave->interval; 421 wave->spawned++; 422 } 423 } 424 } 425 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 426 level->waveEndTimer += gameTime.deltaTime; 427 if (level->waveEndTimer >= 2.0f) 428 { 429 level->nextState = LEVEL_STATE_LOST_WAVE; 430 } 431 } 432 else if (activeWaves == 0 && EnemyCount() == 0) 433 { 434 level->waveEndTimer += gameTime.deltaTime; 435 if (level->waveEndTimer >= 2.0f) 436 { 437 level->nextState = LEVEL_STATE_WON_WAVE; 438 } 439 } 440 } 441 442 PathFindingMapUpdate(); 443 EnemyUpdate(); 444 TowerUpdate(); 445 ProjectileUpdate(); 446 ParticleUpdate(); 447 448 if (level->nextState == LEVEL_STATE_RESET) 449 { 450 InitLevel(level); 451 } 452 453 if (level->nextState == LEVEL_STATE_BATTLE) 454 { 455 InitBattleStateConditions(level); 456 } 457 458 if (level->nextState == LEVEL_STATE_WON_WAVE) 459 { 460 level->currentWave++; 461 level->state = LEVEL_STATE_WON_WAVE; 462 } 463 464 if (level->nextState == LEVEL_STATE_LOST_WAVE) 465 { 466 level->state = LEVEL_STATE_LOST_WAVE; 467 } 468 469 if (level->nextState == LEVEL_STATE_BUILDING) 470 { 471 level->state = LEVEL_STATE_BUILDING; 472 } 473 474 if (level->nextState == LEVEL_STATE_WON_LEVEL) 475 { 476 // make something of this later 477 InitLevel(level); 478 } 479 480 level->nextState = LEVEL_STATE_NONE; 481 } 482 483 float nextSpawnTime = 0.0f; 484 485 void ResetGame() 486 { 487 InitLevel(currentLevel); 488 } 489 490 void InitGame() 491 { 492 TowerInit(); 493 EnemyInit(); 494 ProjectileInit(); 495 ParticleInit(); 496 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 497 498 currentLevel = levels; 499 InitLevel(currentLevel); 500 } 501 502 //# Immediate GUI functions 503 504 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 505 { 506 const float healthBarHeight = 6.0f; 507 const float healthBarOffset = 15.0f; 508 const float inset = 2.0f; 509 const float innerWidth = healthBarWidth - inset * 2; 510 const float innerHeight = healthBarHeight - inset * 2; 511 512 Vector2 screenPos = GetWorldToScreen(position, camera); 513 float centerX = screenPos.x - healthBarWidth * 0.5f; 514 float topY = screenPos.y - healthBarOffset; 515 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 516 float healthWidth = innerWidth * healthRatio; 517 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 518 } 519 520 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 521 { 522 Rectangle bounds = {x, y, width, height}; 523 int isPressed = 0; 524 int isSelected = state && state->isSelected; 525 int isDisabled = state && state->isDisabled; 526 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 527 { 528 Color color = isSelected ? DARKGRAY : GRAY; 529 DrawRectangle(x, y, width, height, color); 530 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 531 { 532 isPressed = 1; 533 } 534 guiState.isBlocked = 1; 535 } 536 else 537 { 538 Color color = isSelected ? WHITE : LIGHTGRAY; 539 DrawRectangle(x, y, width, height, color); 540 } 541 Font font = GetFontDefault(); 542 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 543 Color textColor = isDisabled ? GRAY : BLACK; 544 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 545 return isPressed; 546 } 547 548 //# Main game loop 549 550 void GameUpdate() 551 { 552 float dt = GetFrameTime(); 553 // cap maximum delta time to 0.1 seconds to prevent large time steps 554 if (dt > 0.1f) dt = 0.1f; 555 gameTime.time += dt; 556 gameTime.deltaTime = dt; 557 558 UpdateLevel(currentLevel); 559 } 560 561 int main(void) 562 { 563 int screenWidth, screenHeight; 564 GetPreferredSize(&screenWidth, &screenHeight); 565 InitWindow(screenWidth, screenHeight, "Tower defense"); 566 SetTargetFPS(30); 567 568 LoadAssets(); 569 InitGame(); 570 571 while (!WindowShouldClose()) 572 { 573 if (IsPaused()) { 574 // canvas is not visible in browser - do nothing 575 continue; 576 } 577 578 BeginDrawing(); 579 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 580 581 GameUpdate(); 582 DrawLevel(currentLevel); 583 584 EndDrawing(); 585 } 586 587 CloseWindow(); 588 589 return 0; 590 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
 30 #define TOWER_TYPE_NONE 0
 31 #define TOWER_TYPE_BASE 1
 32 #define TOWER_TYPE_GUN 2
 33 #define TOWER_TYPE_WALL 3
 34 #define TOWER_TYPE_COUNT 4
 35 
 36 typedef struct Tower
 37 {
 38   int16_t x, y;
 39   uint8_t towerType;
 40   Vector2 lastTargetPosition;
 41   float cooldown;
 42   float damage;
 43 } Tower;
 44 
 45 typedef struct GameTime
 46 {
 47   float time;
 48   float deltaTime;
 49 } GameTime;
 50 
 51 typedef struct ButtonState {
 52   char isSelected;
 53   char isDisabled;
 54 } ButtonState;
 55 
 56 typedef struct GUIState {
 57   int isBlocked;
 58 } GUIState;
 59 
 60 typedef enum LevelState
 61 {
 62   LEVEL_STATE_NONE,
 63   LEVEL_STATE_BUILDING,
 64   LEVEL_STATE_BATTLE,
 65   LEVEL_STATE_WON_WAVE,
 66   LEVEL_STATE_LOST_WAVE,
 67   LEVEL_STATE_WON_LEVEL,
 68   LEVEL_STATE_RESET,
 69 } LevelState;
 70 
 71 typedef struct EnemyWave {
 72   uint8_t enemyType;
 73   uint8_t wave;
 74   uint16_t count;
 75   float interval;
 76   float delay;
 77   Vector2 spawnPosition;
 78 
 79   uint16_t spawned;
 80   float timeToSpawnNext;
 81 } EnemyWave;
 82 
 83 typedef struct Level
 84 {
 85   int seed;
 86   LevelState state;
 87   LevelState nextState;
 88   Camera3D camera;
 89   int placementMode;
 90 
 91   int initialGold;
 92   int playerGold;
 93 
 94   EnemyWave waves[10];
 95   int currentWave;
 96   float waveEndTimer;
 97 } Level;
 98 
 99 typedef struct DeltaSrc
100 {
101   char x, y;
102 } DeltaSrc;
103 
104 typedef struct PathfindingMap
105 {
106   int width, height;
107   float scale;
108   float *distances;
109   long *towerIndex; 
110   DeltaSrc *deltaSrc;
111   float maxDistance;
112   Matrix toMapSpace;
113   Matrix toWorldSpace;
114 } PathfindingMap;
115 
116 // when we execute the pathfinding algorithm, we need to store the active nodes
117 // in a queue. Each node has a position, a distance from the start, and the
118 // position of the node that we came from.
119 typedef struct PathfindingNode
120 {
121   int16_t x, y, fromX, fromY;
122   float distance;
123 } PathfindingNode;
124 
125 typedef struct EnemyId
126 {
127   uint16_t index;
128   uint16_t generation;
129 } EnemyId;
130 
131 typedef struct EnemyClassConfig
132 {
133   float speed;
134   float health;
135   float radius;
136   float maxAcceleration;
137   float requiredContactTime;
138   float explosionDamage;
139   float explosionRange;
140   float explosionPushbackPower;
141   int goldValue;
142 } EnemyClassConfig;
143 
144 typedef struct Enemy
145 {
146   int16_t currentX, currentY;
147   int16_t nextX, nextY;
148   Vector2 simPosition;
149   Vector2 simVelocity;
150   uint16_t generation;
151   float walkedDistance;
152   float startMovingTime;
153   float damage, futureDamage;
154   float contactTime;
155   uint8_t enemyType;
156   uint8_t movePathCount;
157   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
158 } Enemy;
159 
160 // a unit that uses sprites to be drawn
161 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
162 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
163 typedef struct SpriteUnit
164 {
165   Rectangle srcRect;
166   Vector2 offset;
167   int frameCount;
168   float frameDuration;
169   Rectangle srcWeaponIdleRect;
170   Vector2 srcWeaponIdleOffset;
171   Rectangle srcWeaponCooldownRect;
172   Vector2 srcWeaponCooldownOffset;
173 } SpriteUnit;
174 
175 #define PROJECTILE_MAX_COUNT 1200
176 #define PROJECTILE_TYPE_NONE 0
177 #define PROJECTILE_TYPE_ARROW 1
178 
179 typedef struct Projectile
180 {
181   uint8_t projectileType;
182   float shootTime;
183   float arrivalTime;
184   float distance;
185   float damage;
186   Vector3 position;
187   Vector3 target;
188   Vector3 directionNormal;
189   EnemyId targetEnemy;
190 } Projectile;
191 
192 //# Function declarations
193 float TowerGetMaxHealth(Tower *tower);
194 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
195 int EnemyAddDamage(Enemy *enemy, float damage);
196 
197 //# Enemy functions
198 void EnemyInit();
199 void EnemyDraw();
200 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
201 void EnemyUpdate();
202 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
203 float EnemyGetMaxHealth(Enemy *enemy);
204 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
205 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
206 EnemyId EnemyGetId(Enemy *enemy);
207 Enemy *EnemyTryResolve(EnemyId enemyId);
208 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
209 int EnemyAddDamage(Enemy *enemy, float damage);
210 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
211 int EnemyCount();
212 void EnemyDrawHealthbars(Camera3D camera);
213 
214 //# Tower functions
215 void TowerInit();
216 Tower *TowerGetAt(int16_t x, int16_t y);
217 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
218 Tower *GetTowerByType(uint8_t towerType);
219 int GetTowerCosts(uint8_t towerType);
220 float TowerGetMaxHealth(Tower *tower);
221 void TowerDraw();
222 void TowerUpdate();
223 void TowerDrawHealthBars(Camera3D camera);
224 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
225 
226 //# Particles
227 void ParticleInit();
228 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
229 void ParticleUpdate();
230 void ParticleDraw();
231 
232 //# Projectiles
233 void ProjectileInit();
234 void ProjectileDraw();
235 void ProjectileUpdate();
236 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage);
237 
238 //# Pathfinding map
239 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
240 float PathFindingGetDistance(int mapX, int mapY);
241 Vector2 PathFindingGetGradient(Vector3 world);
242 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
243 void PathFindingMapUpdate();
244 void PathFindingMapDraw();
245 
246 //# UI
247 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
248 
249 //# Level
250 void DrawLevelGround(Level *level);
251 
252 //# variables
253 extern Level *currentLevel;
254 extern Enemy enemies[ENEMY_MAX_COUNT];
255 extern int enemyCount;
256 extern EnemyClassConfig enemyClassConfigs[];
257 
258 extern GUIState guiState;
259 extern GameTime gameTime;
260 extern Tower towers[TOWER_MAX_COUNT];
261 extern int towerCount;
262 
263 extern Texture2D palette, spriteSheet;
264 
265 #endif
  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}, 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 EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522   }
523 }
  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()
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 < towerCount; i++)
131   {
132     Tower *tower = &towers[i];
133     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134     {
135       continue;
136     }
137     int16_t mapX, mapY;
138     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139     // this would not work correctly and needs to be refined to allow towers covering multiple cells
140     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141     // one cell. For now.
142     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143     {
144       continue;
145     }
146     int index = mapY * width + mapX;
147     pathfindingMap.towerIndex[index] = i;
148   }
149 
150   // we start at the castle and add the castle to the queue
151   pathfindingMap.maxDistance = 0.0f;
152   pathfindingNodeQueueCount = 0;
153   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154   PathfindingNode *node = 0;
155   while ((node = PathFindingNodePop()))
156   {
157     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158     {
159       continue;
160     }
161     int index = node->y * width + node->x;
162     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163     {
164       continue;
165     }
166 
167     int deltaX = node->x - node->fromX;
168     int deltaY = node->y - node->fromY;
169     // even if the cell is blocked by a tower, we still may want to store the direction
170     // (though this might not be needed, IDK right now)
171     pathfindingMap.deltaSrc[index].x = (char) deltaX;
172     pathfindingMap.deltaSrc[index].y = (char) deltaY;
173 
174     // we skip nodes that are blocked by towers
175     if (pathfindingMap.towerIndex[index] >= 0)
176     {
177       node->distance += 8.0f;
178     }
179     pathfindingMap.distances[index] = node->distance;
180     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185   }
186 }
187 
188 void PathFindingMapDraw()
189 {
190   float cellSize = pathfindingMap.scale * 0.9f;
191   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192   for (int x = 0; x < pathfindingMap.width; x++)
193   {
194     for (int y = 0; y < pathfindingMap.height; y++)
195     {
196       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200       // animate the distance "wave" to show how the pathfinding algorithm expands
201       // from the castle
202       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203       {
204         color = BLACK;
205       }
206       DrawCube(position, cellSize, 0.1f, cellSize, color);
207     }
208   }
209 }
210 
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213   int16_t mapX, mapY;
214   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215   {
216     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217     return (Vector2){(float)-delta.x, (float)-delta.y};
218   }
219   // fallback to a simple gradient calculation
220   float n = PathFindingGetDistance(mapX, mapY - 1);
221   float s = PathFindingGetDistance(mapX, mapY + 1);
222   float w = PathFindingGetDistance(mapX - 1, mapY);
223   float e = PathFindingGetDistance(mapX + 1, mapY);
224   return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 void ProjectileInit()
  8 {
  9   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 10   {
 11     projectiles[i] = (Projectile){0};
 12   }
 13 }
 14 
 15 void ProjectileDraw()
 16 {
 17   for (int i = 0; i < projectileCount; i++)
 18   {
 19     Projectile projectile = projectiles[i];
 20     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 21     {
 22       continue;
 23     }
 24     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 25     if (transition >= 1.0f)
 26     {
 27       continue;
 28     }
 29     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 30     {
 31       float t = transition + transitionOffset * 0.3f;
 32       if (t > 1.0f)
 33       {
 34         break;
 35       }
 36       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 37       Color color = RED;
 38       if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 39       {
 40         // make tip red but quickly fade to brown
 41         color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 42         // fake a ballista flight path using parabola equation
 43         float parabolaT = t - 0.5f;
 44         parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 45         position.y += 0.15f * parabolaT * projectile.distance;
 46       }
 47 
 48       float size = 0.06f * (transitionOffset + 0.25f);
 49       DrawCube(position, size, size, size, color);
 50     }
 51   }
 52 }
 53 
 54 void ProjectileUpdate()
 55 {
 56   for (int i = 0; i < projectileCount; i++)
 57   {
 58     Projectile *projectile = &projectiles[i];
 59     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 60     {
 61       continue;
 62     }
 63     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 64     if (transition >= 1.0f)
 65     {
 66       projectile->projectileType = PROJECTILE_TYPE_NONE;
 67       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 68       if (enemy)
 69       {
 70         EnemyAddDamage(enemy, projectile->damage);
 71       }
 72       continue;
 73     }
 74   }
 75 }
 76 
 77 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage)
 78 {
 79   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       projectile->projectileType = projectileType;
 85       projectile->shootTime = gameTime.time;
 86       float distance = Vector3Distance(position, target);
 87       projectile->arrivalTime = gameTime.time + distance / speed;
 88       projectile->damage = damage;
 89       projectile->position = position;
 90       projectile->target = target;
 91       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
 92       projectile->distance = distance;
 93       projectile->targetEnemy = EnemyGetId(enemy);
 94       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 95       return projectile;
 96     }
 97   }
 98   return 0;
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 Tower towers[TOWER_MAX_COUNT];
  5 int towerCount = 0;
  6 
  7 Model towerModels[TOWER_TYPE_COUNT];
  8 
  9 // definition of our archer unit
 10 SpriteUnit archerUnit = {
 11     .srcRect = {0, 0, 16, 16},
 12     .offset = {7, 1},
 13     .frameCount = 1,
 14     .frameDuration = 0.0f,
 15     .srcWeaponIdleRect = {16, 0, 6, 16},
 16     .srcWeaponIdleOffset = {8, 0},
 17     .srcWeaponCooldownRect = {22, 0, 11, 16},
 18     .srcWeaponCooldownOffset = {10, 0},
 19 };
 20 
 21 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 22 {
 23   float xScale = flip ? -1.0f : 1.0f;
 24   Camera3D camera = currentLevel->camera;
 25   float size = 0.5f;
 26   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 27   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 28   // we want the sprite to face the camera, so we need to calculate the up vector
 29   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 30   Vector3 up = {0, 1, 0};
 31   Vector3 right = Vector3CrossProduct(forward, up);
 32   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 33 
 34   Rectangle srcRect = unit.srcRect;
 35   if (unit.frameCount > 1)
 36   {
 37     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 38   }
 39   if (flip)
 40   {
 41     srcRect.x += srcRect.width;
 42     srcRect.width = -srcRect.width;
 43   }
 44   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 45 
 46   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 47   {
 48     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 49     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 50     srcRect = unit.srcWeaponCooldownRect;
 51     if (flip)
 52     {
 53       // position.x = flip * scale.x * 0.5f;
 54       srcRect.x += srcRect.width;
 55       srcRect.width = -srcRect.width;
 56       offset.x = scale.x - offset.x;
 57     }
 58     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 59   }
 60   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
 61   {
 62     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
 63     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
 64     srcRect = unit.srcWeaponIdleRect;
 65     if (flip)
 66     {
 67       // position.x = flip * scale.x * 0.5f;
 68       srcRect.x += srcRect.width;
 69       srcRect.width = -srcRect.width;
 70       offset.x = scale.x - offset.x;
 71     }
 72     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 73   }
 74 }
 75 
 76 void TowerInit()
 77 {
 78   for (int i = 0; i < TOWER_MAX_COUNT; i++)
 79   {
 80     towers[i] = (Tower){0};
 81   }
 82   towerCount = 0;
 83 
 84   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
 85   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
 86 
 87   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
 88   {
 89     if (towerModels[i].materials)
 90     {
 91       // assign the palette texture to the material of the model (0 is not used afaik)
 92       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 93     }
 94   }
 95 }
 96 
 97 static void TowerGunUpdate(Tower *tower)
 98 {
 99   if (tower->cooldown <= 0)
100   {
101     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
102     if (enemy)
103     {
104       tower->cooldown = 0.5f;
105       // shoot the enemy; determine future position of the enemy
106       float bulletSpeed = 4.0f;
107       float bulletDamage = 3.0f;
108       Vector2 velocity = enemy->simVelocity;
109       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
110       Vector2 towerPosition = {tower->x, tower->y};
111       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
112       for (int i = 0; i < 8; i++) {
113         velocity = enemy->simVelocity;
114         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
115         float distance = Vector2Distance(towerPosition, futurePosition);
116         float eta2 = distance / bulletSpeed;
117         if (fabs(eta - eta2) < 0.01f) {
118           break;
119         }
120         eta = (eta2 + eta) * 0.5f;
121       }
122       ProjectileTryAdd(PROJECTILE_TYPE_ARROW, enemy, 
123         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
124         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
125         bulletSpeed, bulletDamage);
126       enemy->futureDamage += bulletDamage;
127       tower->lastTargetPosition = futurePosition;
128     }
129   }
130   else
131   {
132     tower->cooldown -= gameTime.deltaTime;
133   }
134 }
135 
136 Tower *TowerGetAt(int16_t x, int16_t y)
137 {
138   for (int i = 0; i < towerCount; i++)
139   {
140     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
141     {
142       return &towers[i];
143     }
144   }
145   return 0;
146 }
147 
148 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
149 {
150   if (towerCount >= TOWER_MAX_COUNT)
151   {
152     return 0;
153   }
154 
155   Tower *tower = TowerGetAt(x, y);
156   if (tower)
157   {
158     return 0;
159   }
160 
161   tower = &towers[towerCount++];
162   tower->x = x;
163   tower->y = y;
164   tower->towerType = towerType;
165   tower->cooldown = 0.0f;
166   tower->damage = 0.0f;
167   return tower;
168 }
169 
170 Tower *GetTowerByType(uint8_t towerType)
171 {
172   for (int i = 0; i < towerCount; i++)
173   {
174     if (towers[i].towerType == towerType)
175     {
176       return &towers[i];
177     }
178   }
179   return 0;
180 }
181 
182 int GetTowerCosts(uint8_t towerType)
183 {
184   switch (towerType)
185   {
186   case TOWER_TYPE_BASE:
187     return 0;
188   case TOWER_TYPE_GUN:
189     return 6;
190   case TOWER_TYPE_WALL:
191     return 2;
192   }
193   return 0;
194 }
195 
196 float TowerGetMaxHealth(Tower *tower)
197 {
198   switch (tower->towerType)
199   {
200   case TOWER_TYPE_BASE:
201     return 10.0f;
202   case TOWER_TYPE_GUN:
203     return 3.0f;
204   case TOWER_TYPE_WALL:
205     return 5.0f;
206   }
207   return 0.0f;
208 }
209 
210 void TowerDraw()
211 {
212   for (int i = 0; i < towerCount; i++)
213   {
214     Tower tower = towers[i];
215     if (tower.towerType == TOWER_TYPE_NONE)
216     {
217       continue;
218     }
219 
220     switch (tower.towerType)
221     {
222     case TOWER_TYPE_GUN:
223       {
224         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
225         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
226         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
227         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
228           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
229       }
230       break;
231     default:
232       if (towerModels[tower.towerType].materials)
233       {
234         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
235       } else {
236         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
237       }
238       break;
239     }
240   }
241 }
242 
243 void TowerUpdate()
244 {
245   for (int i = 0; i < towerCount; i++)
246   {
247     Tower *tower = &towers[i];
248     switch (tower->towerType)
249     {
250     case TOWER_TYPE_GUN:
251       TowerGunUpdate(tower);
252       break;
253     }
254   }
255 }
256 
257 void TowerDrawHealthBars(Camera3D camera)
258 {
259   for (int i = 0; i < towerCount; i++)
260   {
261     Tower *tower = &towers[i];
262     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
263     {
264       continue;
265     }
266     
267     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
268     float maxHealth = TowerGetMaxHealth(tower);
269     float health = maxHealth - tower->damage;
270     float healthRatio = health / maxHealth;
271     
272     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
273   }
274 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif
All available decorations randomly placed and rotated.

Now things are really coming together. Each drawframe (this is a bit inefficient, but let's ignore that for now) we fill a model array with all the models we have:

  1 Model borderModels[64];
  2 int modelCount = 0;
  3 int rockCount = 5;
  4 int firTreeCount = 2;
  5 int leafTreeCount = 2;
  6 
  7 borderModels[modelCount++] = grassPatchModel[0];
  8 for (int i = 0; i < rockCount; i++)
  9 {
 10   borderModels[modelCount++] = rockModels[i];
 11 }
 12 for (int i = 0; i < firTreeCount; i++)
 13 {
 14   borderModels[modelCount++] = firTreeModel[i];
 15 }
 16 for (int i = 0; i < leafTreeCount; i++)
 17 {
 18   borderModels[modelCount++] = treeModel[i];
 19 }

The borderModels array is our set, and the modelCount variable tells us how many models we have. The count of 64 is arbitrary, it just needs to be big enough to hold all models.

In the loop, we pick a random model through calling borderModels[GetRandomValue(0, modelCount - 1)]. It's important to subtract 1, otherwise we could randomly exceed the array bounds.

It's already looking quite fancy, but ... every time we reset the level, it kinda looks the same, even though the models and positions change. That's because the set of models is the same: The chance to pick a rock or a tree is always the same. There is currently also very little grass because we only have a single grass model. Let's give this a final touch and randomize the set of models we use in a way that the level looks sometimes more like a meadow and sometimes more like a rocky area:

  • 💾
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 //# Variables
  7 GUIState guiState = {0};
  8 GameTime gameTime = {0};
  9 
 10 Model floorTileAModel = {0};
 11 Model floorTileBModel = {0};
 12 Model treeModel[2] = {0};
 13 Model firTreeModel[2] = {0};
 14 Model rockModels[5] = {0};
 15 Model grassPatchModel[1] = {0};
 16 
 17 Texture2D palette, spriteSheet;
 18 
 19 Level levels[] = {
 20   [0] = {
 21     .state = LEVEL_STATE_BUILDING,
 22     .initialGold = 20,
 23     .waves[0] = {
 24       .enemyType = ENEMY_TYPE_MINION,
 25       .wave = 0,
 26       .count = 10,
 27       .interval = 2.5f,
 28       .delay = 1.0f,
 29       .spawnPosition = {0, 6},
 30     },
 31     .waves[1] = {
 32       .enemyType = ENEMY_TYPE_MINION,
 33       .wave = 1,
 34       .count = 20,
 35       .interval = 1.5f,
 36       .delay = 1.0f,
 37       .spawnPosition = {0, 6},
 38     },
 39     .waves[2] = {
 40       .enemyType = ENEMY_TYPE_MINION,
 41       .wave = 2,
 42       .count = 30,
 43       .interval = 1.2f,
 44       .delay = 1.0f,
 45       .spawnPosition = {0, 6},
 46     }
 47   },
 48 };
 49 
 50 Level *currentLevel = levels;
 51 
 52 //# Game
 53 
 54 static Model LoadGLBModel(char *filename)
 55 {
 56   Model model = LoadModel(TextFormat("data/%s.glb",filename));
 57   if (model.materialCount > 1)
 58   {
 59     model.materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 60   }
 61   return model;
 62 }
 63 
 64 void LoadAssets()
 65 {
 66   // load a sprite sheet that contains all units
 67   spriteSheet = LoadTexture("data/spritesheet.png");
 68   SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
 69 
 70   // we'll use a palette texture to colorize the all buildings and environment art
 71   palette = LoadTexture("data/palette.png");
 72   // The texture uses gradients on very small space, so we'll enable bilinear filtering
 73   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
 74 
 75   floorTileAModel = LoadGLBModel("floor-tile-a");
 76   floorTileBModel = LoadGLBModel("floor-tile-b");
 77   treeModel[0] = LoadGLBModel("leaftree-large-1-a");
 78   treeModel[1] = LoadGLBModel("leaftree-large-1-b");
 79   firTreeModel[0] = LoadGLBModel("firtree-1-a");
 80   firTreeModel[1] = LoadGLBModel("firtree-1-b");
 81   rockModels[0] = LoadGLBModel("rock-1");
 82   rockModels[1] = LoadGLBModel("rock-2");
 83   rockModels[2] = LoadGLBModel("rock-3");
 84   rockModels[3] = LoadGLBModel("rock-4");
 85   rockModels[4] = LoadGLBModel("rock-5");
 86   grassPatchModel[0] = LoadGLBModel("grass-patch-1");
 87 }
 88 
 89 void InitLevel(Level *level)
 90 {
 91   level->seed = (int)(GetTime() * 100.0f);
 92 
 93   TowerInit();
 94   EnemyInit();
 95   ProjectileInit();
 96   ParticleInit();
 97   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
 98 
 99   level->placementMode = 0;
100   level->state = LEVEL_STATE_BUILDING;
101   level->nextState = LEVEL_STATE_NONE;
102   level->playerGold = level->initialGold;
103   level->currentWave = 0;
104 
105   Camera *camera = &level->camera;
106   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
107   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
108   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
109   camera->fovy = 10.0f;
110   camera->projection = CAMERA_ORTHOGRAPHIC;
111 }
112 
113 void DrawLevelHud(Level *level)
114 {
115   const char *text = TextFormat("Gold: %d", level->playerGold);
116   Font font = GetFontDefault();
117   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
118   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
119 }
120 
121 void DrawLevelReportLostWave(Level *level)
122 {
123   BeginMode3D(level->camera);
124   DrawLevelGround(level);
125   TowerDraw();
126   EnemyDraw();
127   ProjectileDraw();
128   ParticleDraw();
129   guiState.isBlocked = 0;
130   EndMode3D();
131 
132   TowerDrawHealthBars(level->camera);
133 
134   const char *text = "Wave lost";
135   int textWidth = MeasureText(text, 20);
136   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
137 
138   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
139   {
140     level->nextState = LEVEL_STATE_RESET;
141   }
142 }
143 
144 int HasLevelNextWave(Level *level)
145 {
146   for (int i = 0; i < 10; i++)
147   {
148     EnemyWave *wave = &level->waves[i];
149     if (wave->wave == level->currentWave)
150     {
151       return 1;
152     }
153   }
154   return 0;
155 }
156 
157 void DrawLevelReportWonWave(Level *level)
158 {
159   BeginMode3D(level->camera);
160   DrawLevelGround(level);
161   TowerDraw();
162   EnemyDraw();
163   ProjectileDraw();
164   ParticleDraw();
165   guiState.isBlocked = 0;
166   EndMode3D();
167 
168   TowerDrawHealthBars(level->camera);
169 
170   const char *text = "Wave won";
171   int textWidth = MeasureText(text, 20);
172   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
173 
174 
175   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
176   {
177     level->nextState = LEVEL_STATE_RESET;
178   }
179 
180   if (HasLevelNextWave(level))
181   {
182     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
183     {
184       level->nextState = LEVEL_STATE_BUILDING;
185     }
186   }
187   else {
188     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
189     {
190       level->nextState = LEVEL_STATE_WON_LEVEL;
191     }
192   }
193 }
194 
195 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
196 {
197   static ButtonState buttonStates[8] = {0};
198   int cost = GetTowerCosts(towerType);
199   const char *text = TextFormat("%s: %d", name, cost);
200   buttonStates[towerType].isSelected = level->placementMode == towerType;
201   buttonStates[towerType].isDisabled = level->playerGold < cost;
202   if (Button(text, x, y, width, height, &buttonStates[towerType]))
203   {
204     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
205   }
206 }
207 
208 float GetRandomFloat(float min, float max)
209 {
210   int random = GetRandomValue(0, 0xfffffff);
211   return ((float)random / (float)0xfffffff) * (max - min) + min;
212 }
213 
214 void DrawLevelGround(Level *level)
215 {
216   // draw checkerboard ground pattern
217   for (int x = -5; x <= 5; x += 1)
218   {
219     for (int y = -5; y <= 5; y += 1)
220     {
221       Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
222       DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
223     }
224   }
225 
226 int oldSeed = GetRandomValue(0, 0xfffffff); 227 SetRandomSeed(level->seed); 228 // increase probability for trees via duplicated entries
229 Model borderModels[64];
230 int maxRockCount = GetRandomValue(2, 6); 231 int maxTreeCount = GetRandomValue(10, 20); 232 int maxFirTreeCount = GetRandomValue(5, 10); 233 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount; 234 int grassPatchCount = GetRandomValue(5, 30);
235
236 int modelCount = 0; 237 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
238 {
239 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
240 }
241 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
242 {
243 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
244 }
245 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
246 {
247 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)]; 248 } 249 for (int i = 0; i < grassPatchCount && modelCount < 63; i++) 250 { 251 borderModels[modelCount++] = grassPatchModel[0];
252 } 253
254 // draw some objects around the border of the map 255 Vector3 up = {0, 1, 0}; 256 // a pseudo random number generator to get the same result every time 257 const float wiggle = 0.75f; 258 const int layerCount = 3;
259 for (int layer = 0; layer < layerCount; layer++) 260 { 261 int layerPos = 6 + layer; 262 for (int x = -6 + layer; x <= 6 + layer; x += 1) 263 {
264 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
265 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)}, 266 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
267 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
268 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)}, 269 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE); 270 } 271 272 for (int z = -5 + layer; z <= 5 + layer; z += 1) 273 {
274 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
275 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 276 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
277 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
278 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)}, 279 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
280 } 281 }
282 283 SetRandomSeed(oldSeed); 284 } 285 286 void DrawLevelBuildingState(Level *level) 287 { 288 BeginMode3D(level->camera); 289 DrawLevelGround(level); 290 TowerDraw(); 291 EnemyDraw(); 292 ProjectileDraw(); 293 ParticleDraw(); 294 295 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 296 float planeDistance = ray.position.y / -ray.direction.y; 297 float planeX = ray.direction.x * planeDistance + ray.position.x; 298 float planeY = ray.direction.z * planeDistance + ray.position.z; 299 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 300 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 301 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5) 302 { 303 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 304 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 305 { 306 if (TowerTryAdd(level->placementMode, mapX, mapY)) 307 { 308 level->playerGold -= GetTowerCosts(level->placementMode); 309 level->placementMode = TOWER_TYPE_NONE; 310 } 311 } 312 } 313 314 guiState.isBlocked = 0; 315 316 EndMode3D(); 317 318 TowerDrawHealthBars(level->camera); 319 320 static ButtonState buildWallButtonState = {0}; 321 static ButtonState buildGunButtonState = {0}; 322 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 323 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN; 324 325 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall"); 326 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_GUN, "Archer"); 327 328 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 329 { 330 level->nextState = LEVEL_STATE_RESET; 331 } 332 333 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 334 { 335 level->nextState = LEVEL_STATE_BATTLE; 336 } 337 338 const char *text = "Building phase"; 339 int textWidth = MeasureText(text, 20); 340 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 341 } 342 343 void InitBattleStateConditions(Level *level) 344 { 345 level->state = LEVEL_STATE_BATTLE; 346 level->nextState = LEVEL_STATE_NONE; 347 level->waveEndTimer = 0.0f; 348 for (int i = 0; i < 10; i++) 349 { 350 EnemyWave *wave = &level->waves[i]; 351 wave->spawned = 0; 352 wave->timeToSpawnNext = wave->delay; 353 } 354 } 355 356 void DrawLevelBattleState(Level *level) 357 { 358 BeginMode3D(level->camera); 359 DrawLevelGround(level); 360 TowerDraw(); 361 EnemyDraw(); 362 ProjectileDraw(); 363 ParticleDraw(); 364 guiState.isBlocked = 0; 365 EndMode3D(); 366 367 EnemyDrawHealthbars(level->camera); 368 TowerDrawHealthBars(level->camera); 369 370 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 371 { 372 level->nextState = LEVEL_STATE_RESET; 373 } 374 375 int maxCount = 0; 376 int remainingCount = 0; 377 for (int i = 0; i < 10; i++) 378 { 379 EnemyWave *wave = &level->waves[i]; 380 if (wave->wave != level->currentWave) 381 { 382 continue; 383 } 384 maxCount += wave->count; 385 remainingCount += wave->count - wave->spawned; 386 } 387 int aliveCount = EnemyCount(); 388 remainingCount += aliveCount; 389 390 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 391 int textWidth = MeasureText(text, 20); 392 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 393 } 394 395 void DrawLevel(Level *level) 396 { 397 switch (level->state) 398 { 399 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 400 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 401 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 402 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 403 default: break; 404 } 405 406 DrawLevelHud(level); 407 } 408 409 void UpdateLevel(Level *level) 410 { 411 if (level->state == LEVEL_STATE_BATTLE) 412 { 413 int activeWaves = 0; 414 for (int i = 0; i < 10; i++) 415 { 416 EnemyWave *wave = &level->waves[i]; 417 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 418 { 419 continue; 420 } 421 activeWaves++; 422 wave->timeToSpawnNext -= gameTime.deltaTime; 423 if (wave->timeToSpawnNext <= 0.0f) 424 { 425 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 426 if (enemy) 427 { 428 wave->timeToSpawnNext = wave->interval; 429 wave->spawned++; 430 } 431 } 432 } 433 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 434 level->waveEndTimer += gameTime.deltaTime; 435 if (level->waveEndTimer >= 2.0f) 436 { 437 level->nextState = LEVEL_STATE_LOST_WAVE; 438 } 439 } 440 else if (activeWaves == 0 && EnemyCount() == 0) 441 { 442 level->waveEndTimer += gameTime.deltaTime; 443 if (level->waveEndTimer >= 2.0f) 444 { 445 level->nextState = LEVEL_STATE_WON_WAVE; 446 } 447 } 448 } 449 450 PathFindingMapUpdate(); 451 EnemyUpdate(); 452 TowerUpdate(); 453 ProjectileUpdate(); 454 ParticleUpdate(); 455 456 if (level->nextState == LEVEL_STATE_RESET) 457 { 458 InitLevel(level); 459 } 460 461 if (level->nextState == LEVEL_STATE_BATTLE) 462 { 463 InitBattleStateConditions(level); 464 } 465 466 if (level->nextState == LEVEL_STATE_WON_WAVE) 467 { 468 level->currentWave++; 469 level->state = LEVEL_STATE_WON_WAVE; 470 } 471 472 if (level->nextState == LEVEL_STATE_LOST_WAVE) 473 { 474 level->state = LEVEL_STATE_LOST_WAVE; 475 } 476 477 if (level->nextState == LEVEL_STATE_BUILDING) 478 { 479 level->state = LEVEL_STATE_BUILDING; 480 } 481 482 if (level->nextState == LEVEL_STATE_WON_LEVEL) 483 { 484 // make something of this later 485 InitLevel(level); 486 } 487 488 level->nextState = LEVEL_STATE_NONE; 489 } 490 491 float nextSpawnTime = 0.0f; 492 493 void ResetGame() 494 { 495 InitLevel(currentLevel); 496 } 497 498 void InitGame() 499 { 500 TowerInit(); 501 EnemyInit(); 502 ProjectileInit(); 503 ParticleInit(); 504 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 505 506 currentLevel = levels; 507 InitLevel(currentLevel); 508 } 509 510 //# Immediate GUI functions 511 512 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 513 { 514 const float healthBarHeight = 6.0f; 515 const float healthBarOffset = 15.0f; 516 const float inset = 2.0f; 517 const float innerWidth = healthBarWidth - inset * 2; 518 const float innerHeight = healthBarHeight - inset * 2; 519 520 Vector2 screenPos = GetWorldToScreen(position, camera); 521 float centerX = screenPos.x - healthBarWidth * 0.5f; 522 float topY = screenPos.y - healthBarOffset; 523 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 524 float healthWidth = innerWidth * healthRatio; 525 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 526 } 527 528 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 529 { 530 Rectangle bounds = {x, y, width, height}; 531 int isPressed = 0; 532 int isSelected = state && state->isSelected; 533 int isDisabled = state && state->isDisabled; 534 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled) 535 { 536 Color color = isSelected ? DARKGRAY : GRAY; 537 DrawRectangle(x, y, width, height, color); 538 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 539 { 540 isPressed = 1; 541 } 542 guiState.isBlocked = 1; 543 } 544 else 545 { 546 Color color = isSelected ? WHITE : LIGHTGRAY; 547 DrawRectangle(x, y, width, height, color); 548 } 549 Font font = GetFontDefault(); 550 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 551 Color textColor = isDisabled ? GRAY : BLACK; 552 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 553 return isPressed; 554 } 555 556 //# Main game loop 557 558 void GameUpdate() 559 { 560 float dt = GetFrameTime(); 561 // cap maximum delta time to 0.1 seconds to prevent large time steps 562 if (dt > 0.1f) dt = 0.1f; 563 gameTime.time += dt; 564 gameTime.deltaTime = dt; 565 566 UpdateLevel(currentLevel); 567 } 568 569 int main(void) 570 { 571 int screenWidth, screenHeight; 572 GetPreferredSize(&screenWidth, &screenHeight); 573 InitWindow(screenWidth, screenHeight, "Tower defense"); 574 SetTargetFPS(30); 575 576 LoadAssets(); 577 InitGame(); 578 579 while (!WindowShouldClose()) 580 { 581 if (IsPaused()) { 582 // canvas is not visible in browser - do nothing 583 continue; 584 } 585 586 BeginDrawing(); 587 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF}); 588 589 GameUpdate(); 590 DrawLevel(currentLevel); 591 592 EndDrawing(); 593 } 594 595 CloseWindow(); 596 597 return 0; 598 }
  1 #ifndef TD_TUT_2_MAIN_H
  2 #define TD_TUT_2_MAIN_H
  3 
  4 #include <inttypes.h>
  5 
  6 #include "raylib.h"
  7 #include "preferred_size.h"
  8 
  9 //# Declarations
 10 
 11 #define ENEMY_MAX_PATH_COUNT 8
 12 #define ENEMY_MAX_COUNT 400
 13 #define ENEMY_TYPE_NONE 0
 14 #define ENEMY_TYPE_MINION 1
 15 
 16 #define PARTICLE_MAX_COUNT 400
 17 #define PARTICLE_TYPE_NONE 0
 18 #define PARTICLE_TYPE_EXPLOSION 1
 19 
 20 typedef struct Particle
 21 {
 22   uint8_t particleType;
 23   float spawnTime;
 24   float lifetime;
 25   Vector3 position;
 26   Vector3 velocity;
 27 } Particle;
 28 
 29 #define TOWER_MAX_COUNT 400
 30 #define TOWER_TYPE_NONE 0
 31 #define TOWER_TYPE_BASE 1
 32 #define TOWER_TYPE_GUN 2
 33 #define TOWER_TYPE_WALL 3
 34 #define TOWER_TYPE_COUNT 4
 35 
 36 typedef struct Tower
 37 {
 38   int16_t x, y;
 39   uint8_t towerType;
 40   Vector2 lastTargetPosition;
 41   float cooldown;
 42   float damage;
 43 } Tower;
 44 
 45 typedef struct GameTime
 46 {
 47   float time;
 48   float deltaTime;
 49 } GameTime;
 50 
 51 typedef struct ButtonState {
 52   char isSelected;
 53   char isDisabled;
 54 } ButtonState;
 55 
 56 typedef struct GUIState {
 57   int isBlocked;
 58 } GUIState;
 59 
 60 typedef enum LevelState
 61 {
 62   LEVEL_STATE_NONE,
 63   LEVEL_STATE_BUILDING,
 64   LEVEL_STATE_BATTLE,
 65   LEVEL_STATE_WON_WAVE,
 66   LEVEL_STATE_LOST_WAVE,
 67   LEVEL_STATE_WON_LEVEL,
 68   LEVEL_STATE_RESET,
 69 } LevelState;
 70 
 71 typedef struct EnemyWave {
 72   uint8_t enemyType;
 73   uint8_t wave;
 74   uint16_t count;
 75   float interval;
 76   float delay;
 77   Vector2 spawnPosition;
 78 
 79   uint16_t spawned;
 80   float timeToSpawnNext;
 81 } EnemyWave;
 82 
 83 typedef struct Level
 84 {
 85   int seed;
 86   LevelState state;
 87   LevelState nextState;
 88   Camera3D camera;
 89   int placementMode;
 90 
 91   int initialGold;
 92   int playerGold;
 93 
 94   EnemyWave waves[10];
 95   int currentWave;
 96   float waveEndTimer;
 97 } Level;
 98 
 99 typedef struct DeltaSrc
100 {
101   char x, y;
102 } DeltaSrc;
103 
104 typedef struct PathfindingMap
105 {
106   int width, height;
107   float scale;
108   float *distances;
109   long *towerIndex; 
110   DeltaSrc *deltaSrc;
111   float maxDistance;
112   Matrix toMapSpace;
113   Matrix toWorldSpace;
114 } PathfindingMap;
115 
116 // when we execute the pathfinding algorithm, we need to store the active nodes
117 // in a queue. Each node has a position, a distance from the start, and the
118 // position of the node that we came from.
119 typedef struct PathfindingNode
120 {
121   int16_t x, y, fromX, fromY;
122   float distance;
123 } PathfindingNode;
124 
125 typedef struct EnemyId
126 {
127   uint16_t index;
128   uint16_t generation;
129 } EnemyId;
130 
131 typedef struct EnemyClassConfig
132 {
133   float speed;
134   float health;
135   float radius;
136   float maxAcceleration;
137   float requiredContactTime;
138   float explosionDamage;
139   float explosionRange;
140   float explosionPushbackPower;
141   int goldValue;
142 } EnemyClassConfig;
143 
144 typedef struct Enemy
145 {
146   int16_t currentX, currentY;
147   int16_t nextX, nextY;
148   Vector2 simPosition;
149   Vector2 simVelocity;
150   uint16_t generation;
151   float walkedDistance;
152   float startMovingTime;
153   float damage, futureDamage;
154   float contactTime;
155   uint8_t enemyType;
156   uint8_t movePathCount;
157   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
158 } Enemy;
159 
160 // a unit that uses sprites to be drawn
161 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
162 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
163 typedef struct SpriteUnit
164 {
165   Rectangle srcRect;
166   Vector2 offset;
167   int frameCount;
168   float frameDuration;
169   Rectangle srcWeaponIdleRect;
170   Vector2 srcWeaponIdleOffset;
171   Rectangle srcWeaponCooldownRect;
172   Vector2 srcWeaponCooldownOffset;
173 } SpriteUnit;
174 
175 #define PROJECTILE_MAX_COUNT 1200
176 #define PROJECTILE_TYPE_NONE 0
177 #define PROJECTILE_TYPE_ARROW 1
178 
179 typedef struct Projectile
180 {
181   uint8_t projectileType;
182   float shootTime;
183   float arrivalTime;
184   float distance;
185   float damage;
186   Vector3 position;
187   Vector3 target;
188   Vector3 directionNormal;
189   EnemyId targetEnemy;
190 } Projectile;
191 
192 //# Function declarations
193 float TowerGetMaxHealth(Tower *tower);
194 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
195 int EnemyAddDamage(Enemy *enemy, float damage);
196 
197 //# Enemy functions
198 void EnemyInit();
199 void EnemyDraw();
200 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
201 void EnemyUpdate();
202 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
203 float EnemyGetMaxHealth(Enemy *enemy);
204 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
205 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
206 EnemyId EnemyGetId(Enemy *enemy);
207 Enemy *EnemyTryResolve(EnemyId enemyId);
208 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
209 int EnemyAddDamage(Enemy *enemy, float damage);
210 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
211 int EnemyCount();
212 void EnemyDrawHealthbars(Camera3D camera);
213 
214 //# Tower functions
215 void TowerInit();
216 Tower *TowerGetAt(int16_t x, int16_t y);
217 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
218 Tower *GetTowerByType(uint8_t towerType);
219 int GetTowerCosts(uint8_t towerType);
220 float TowerGetMaxHealth(Tower *tower);
221 void TowerDraw();
222 void TowerUpdate();
223 void TowerDrawHealthBars(Camera3D camera);
224 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
225 
226 //# Particles
227 void ParticleInit();
228 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
229 void ParticleUpdate();
230 void ParticleDraw();
231 
232 //# Projectiles
233 void ProjectileInit();
234 void ProjectileDraw();
235 void ProjectileUpdate();
236 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage);
237 
238 //# Pathfinding map
239 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
240 float PathFindingGetDistance(int mapX, int mapY);
241 Vector2 PathFindingGetGradient(Vector3 world);
242 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
243 void PathFindingMapUpdate();
244 void PathFindingMapDraw();
245 
246 //# UI
247 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
248 
249 //# Level
250 void DrawLevelGround(Level *level);
251 
252 //# variables
253 extern Level *currentLevel;
254 extern Enemy enemies[ENEMY_MAX_COUNT];
255 extern int enemyCount;
256 extern EnemyClassConfig enemyClassConfigs[];
257 
258 extern GUIState guiState;
259 extern GameTime gameTime;
260 extern Tower towers[TOWER_MAX_COUNT];
261 extern int towerCount;
262 
263 extern Texture2D palette, spriteSheet;
264 
265 #endif
  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}, 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 EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521     DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522   }
523 }
  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()
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 < towerCount; i++)
131   {
132     Tower *tower = &towers[i];
133     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134     {
135       continue;
136     }
137     int16_t mapX, mapY;
138     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139     // this would not work correctly and needs to be refined to allow towers covering multiple cells
140     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141     // one cell. For now.
142     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143     {
144       continue;
145     }
146     int index = mapY * width + mapX;
147     pathfindingMap.towerIndex[index] = i;
148   }
149 
150   // we start at the castle and add the castle to the queue
151   pathfindingMap.maxDistance = 0.0f;
152   pathfindingNodeQueueCount = 0;
153   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154   PathfindingNode *node = 0;
155   while ((node = PathFindingNodePop()))
156   {
157     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158     {
159       continue;
160     }
161     int index = node->y * width + node->x;
162     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163     {
164       continue;
165     }
166 
167     int deltaX = node->x - node->fromX;
168     int deltaY = node->y - node->fromY;
169     // even if the cell is blocked by a tower, we still may want to store the direction
170     // (though this might not be needed, IDK right now)
171     pathfindingMap.deltaSrc[index].x = (char) deltaX;
172     pathfindingMap.deltaSrc[index].y = (char) deltaY;
173 
174     // we skip nodes that are blocked by towers
175     if (pathfindingMap.towerIndex[index] >= 0)
176     {
177       node->distance += 8.0f;
178     }
179     pathfindingMap.distances[index] = node->distance;
180     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185   }
186 }
187 
188 void PathFindingMapDraw()
189 {
190   float cellSize = pathfindingMap.scale * 0.9f;
191   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192   for (int x = 0; x < pathfindingMap.width; x++)
193   {
194     for (int y = 0; y < pathfindingMap.height; y++)
195     {
196       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200       // animate the distance "wave" to show how the pathfinding algorithm expands
201       // from the castle
202       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203       {
204         color = BLACK;
205       }
206       DrawCube(position, cellSize, 0.1f, cellSize, color);
207     }
208   }
209 }
210 
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213   int16_t mapX, mapY;
214   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215   {
216     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217     return (Vector2){(float)-delta.x, (float)-delta.y};
218   }
219   // fallback to a simple gradient calculation
220   float n = PathFindingGetDistance(mapX, mapY - 1);
221   float s = PathFindingGetDistance(mapX, mapY + 1);
222   float w = PathFindingGetDistance(mapX - 1, mapY);
223   float e = PathFindingGetDistance(mapX + 1, mapY);
224   return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 void ProjectileInit()
  8 {
  9   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 10   {
 11     projectiles[i] = (Projectile){0};
 12   }
 13 }
 14 
 15 void ProjectileDraw()
 16 {
 17   for (int i = 0; i < projectileCount; i++)
 18   {
 19     Projectile projectile = projectiles[i];
 20     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 21     {
 22       continue;
 23     }
 24     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 25     if (transition >= 1.0f)
 26     {
 27       continue;
 28     }
 29     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 30     {
 31       float t = transition + transitionOffset * 0.3f;
 32       if (t > 1.0f)
 33       {
 34         break;
 35       }
 36       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 37       Color color = RED;
 38       if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 39       {
 40         // make tip red but quickly fade to brown
 41         color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 42         // fake a ballista flight path using parabola equation
 43         float parabolaT = t - 0.5f;
 44         parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 45         position.y += 0.15f * parabolaT * projectile.distance;
 46       }
 47 
 48       float size = 0.06f * (transitionOffset + 0.25f);
 49       DrawCube(position, size, size, size, color);
 50     }
 51   }
 52 }
 53 
 54 void ProjectileUpdate()
 55 {
 56   for (int i = 0; i < projectileCount; i++)
 57   {
 58     Projectile *projectile = &projectiles[i];
 59     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 60     {
 61       continue;
 62     }
 63     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 64     if (transition >= 1.0f)
 65     {
 66       projectile->projectileType = PROJECTILE_TYPE_NONE;
 67       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 68       if (enemy)
 69       {
 70         EnemyAddDamage(enemy, projectile->damage);
 71       }
 72       continue;
 73     }
 74   }
 75 }
 76 
 77 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage)
 78 {
 79   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       projectile->projectileType = projectileType;
 85       projectile->shootTime = gameTime.time;
 86       float distance = Vector3Distance(position, target);
 87       projectile->arrivalTime = gameTime.time + distance / speed;
 88       projectile->damage = damage;
 89       projectile->position = position;
 90       projectile->target = target;
 91       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
 92       projectile->distance = distance;
 93       projectile->targetEnemy = EnemyGetId(enemy);
 94       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 95       return projectile;
 96     }
 97   }
 98   return 0;
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 Tower towers[TOWER_MAX_COUNT];
  5 int towerCount = 0;
  6 
  7 Model towerModels[TOWER_TYPE_COUNT];
  8 
  9 // definition of our archer unit
 10 SpriteUnit archerUnit = {
 11     .srcRect = {0, 0, 16, 16},
 12     .offset = {7, 1},
 13     .frameCount = 1,
 14     .frameDuration = 0.0f,
 15     .srcWeaponIdleRect = {16, 0, 6, 16},
 16     .srcWeaponIdleOffset = {8, 0},
 17     .srcWeaponCooldownRect = {22, 0, 11, 16},
 18     .srcWeaponCooldownOffset = {10, 0},
 19 };
 20 
 21 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 22 {
 23   float xScale = flip ? -1.0f : 1.0f;
 24   Camera3D camera = currentLevel->camera;
 25   float size = 0.5f;
 26   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
 27   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 28   // we want the sprite to face the camera, so we need to calculate the up vector
 29   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 30   Vector3 up = {0, 1, 0};
 31   Vector3 right = Vector3CrossProduct(forward, up);
 32   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 33 
 34   Rectangle srcRect = unit.srcRect;
 35   if (unit.frameCount > 1)
 36   {
 37     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 38   }
 39   if (flip)
 40   {
 41     srcRect.x += srcRect.width;
 42     srcRect.width = -srcRect.width;
 43   }
 44   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 45 
 46   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 47   {
 48     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 49     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 50     srcRect = unit.srcWeaponCooldownRect;
 51     if (flip)
 52     {
 53       // position.x = flip * scale.x * 0.5f;
 54       srcRect.x += srcRect.width;
 55       srcRect.width = -srcRect.width;
 56       offset.x = scale.x - offset.x;
 57     }
 58     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 59   }
 60   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
 61   {
 62     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
 63     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
 64     srcRect = unit.srcWeaponIdleRect;
 65     if (flip)
 66     {
 67       // position.x = flip * scale.x * 0.5f;
 68       srcRect.x += srcRect.width;
 69       srcRect.width = -srcRect.width;
 70       offset.x = scale.x - offset.x;
 71     }
 72     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 73   }
 74 }
 75 
 76 void TowerInit()
 77 {
 78   for (int i = 0; i < TOWER_MAX_COUNT; i++)
 79   {
 80     towers[i] = (Tower){0};
 81   }
 82   towerCount = 0;
 83 
 84   towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
 85   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
 86 
 87   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
 88   {
 89     if (towerModels[i].materials)
 90     {
 91       // assign the palette texture to the material of the model (0 is not used afaik)
 92       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 93     }
 94   }
 95 }
 96 
 97 static void TowerGunUpdate(Tower *tower)
 98 {
 99   if (tower->cooldown <= 0)
100   {
101     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
102     if (enemy)
103     {
104       tower->cooldown = 0.5f;
105       // shoot the enemy; determine future position of the enemy
106       float bulletSpeed = 4.0f;
107       float bulletDamage = 3.0f;
108       Vector2 velocity = enemy->simVelocity;
109       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
110       Vector2 towerPosition = {tower->x, tower->y};
111       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
112       for (int i = 0; i < 8; i++) {
113         velocity = enemy->simVelocity;
114         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
115         float distance = Vector2Distance(towerPosition, futurePosition);
116         float eta2 = distance / bulletSpeed;
117         if (fabs(eta - eta2) < 0.01f) {
118           break;
119         }
120         eta = (eta2 + eta) * 0.5f;
121       }
122       ProjectileTryAdd(PROJECTILE_TYPE_ARROW, enemy, 
123         (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 
124         (Vector3){futurePosition.x, 0.25f, futurePosition.y},
125         bulletSpeed, bulletDamage);
126       enemy->futureDamage += bulletDamage;
127       tower->lastTargetPosition = futurePosition;
128     }
129   }
130   else
131   {
132     tower->cooldown -= gameTime.deltaTime;
133   }
134 }
135 
136 Tower *TowerGetAt(int16_t x, int16_t y)
137 {
138   for (int i = 0; i < towerCount; i++)
139   {
140     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
141     {
142       return &towers[i];
143     }
144   }
145   return 0;
146 }
147 
148 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
149 {
150   if (towerCount >= TOWER_MAX_COUNT)
151   {
152     return 0;
153   }
154 
155   Tower *tower = TowerGetAt(x, y);
156   if (tower)
157   {
158     return 0;
159   }
160 
161   tower = &towers[towerCount++];
162   tower->x = x;
163   tower->y = y;
164   tower->towerType = towerType;
165   tower->cooldown = 0.0f;
166   tower->damage = 0.0f;
167   return tower;
168 }
169 
170 Tower *GetTowerByType(uint8_t towerType)
171 {
172   for (int i = 0; i < towerCount; i++)
173   {
174     if (towers[i].towerType == towerType)
175     {
176       return &towers[i];
177     }
178   }
179   return 0;
180 }
181 
182 int GetTowerCosts(uint8_t towerType)
183 {
184   switch (towerType)
185   {
186   case TOWER_TYPE_BASE:
187     return 0;
188   case TOWER_TYPE_GUN:
189     return 6;
190   case TOWER_TYPE_WALL:
191     return 2;
192   }
193   return 0;
194 }
195 
196 float TowerGetMaxHealth(Tower *tower)
197 {
198   switch (tower->towerType)
199   {
200   case TOWER_TYPE_BASE:
201     return 10.0f;
202   case TOWER_TYPE_GUN:
203     return 3.0f;
204   case TOWER_TYPE_WALL:
205     return 5.0f;
206   }
207   return 0.0f;
208 }
209 
210 void TowerDraw()
211 {
212   for (int i = 0; i < towerCount; i++)
213   {
214     Tower tower = towers[i];
215     if (tower.towerType == TOWER_TYPE_NONE)
216     {
217       continue;
218     }
219 
220     switch (tower.towerType)
221     {
222     case TOWER_TYPE_GUN:
223       {
224         Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
225         Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
226         DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
227         DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 
228           tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
229       }
230       break;
231     default:
232       if (towerModels[tower.towerType].materials)
233       {
234         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
235       } else {
236         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
237       }
238       break;
239     }
240   }
241 }
242 
243 void TowerUpdate()
244 {
245   for (int i = 0; i < towerCount; i++)
246   {
247     Tower *tower = &towers[i];
248     switch (tower->towerType)
249     {
250     case TOWER_TYPE_GUN:
251       TowerGunUpdate(tower);
252       break;
253     }
254   }
255 }
256 
257 void TowerDrawHealthBars(Camera3D camera)
258 {
259   for (int i = 0; i < towerCount; i++)
260   {
261     Tower *tower = &towers[i];
262     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
263     {
264       continue;
265     }
266     
267     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
268     float maxHealth = TowerGetMaxHealth(tower);
269     float health = maxHealth - tower->damage;
270     float healthRatio = health / maxHealth;
271     
272     DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
273   }
274 }
  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 level now looks different each time you reset it.

When you reset the level a few times, you can see that the general look of the level can change quite a bit. Sometimes it's more grassy, sometimes it can be a thick forest.

The trick is to manipulate the counts of the models, eventually duplicating some entries to increase the probability of a certain model to be picked for drawing:

  1 Model borderModels[64];
  2 int maxRockCount = GetRandomValue(2, 6);
  3 int maxTreeCount = GetRandomValue(10, 20);
  4 int maxFirTreeCount = GetRandomValue(5, 10);
  5 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
  6 int grassPatchCount = GetRandomValue(5, 30);
  7 
  8 int modelCount = 0;
  9 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
 10 {
 11   borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
 12 }
 13 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
 14 {
 15   borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
 16 }
 17 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
 18 {
 19   borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
 20 }
 21 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
 22 {
 23   borderModels[modelCount++] = grassPatchModel[0];
 24 }

So in the beginning, we set how many models we will use for rocks, trees, fir trees, etc.. If, for example, the maxRockCount is set to 6, we will have a lot more rocks in the scene than if it is set to 2. There is also a balance between the different types of trees: The tree count is the sum of fir trees and leaf trees, so any forest will have a certain balance between the two types of trees.

Wrap up

The result is a quite natural looking environment. It's already much better looking than what we need at this point. The procedural placement here is only scratching the surface of what is possible, but I hope you get the idea of how this kind of random generation works. There is much more to explore, but for this game tutorial, I would say we are done.

In the next parts, I want to add more tower type graphics. This is quite a bit more complicated than what we've done so far:

Loading and manipulating 3d animated model files is actually quite complex. The raylib API is lacking a few features we will need here, which is why the next parts will be considerably more difficult (it also takes me much longer to prepare, but I hope I can keep the schedule). But my idea is to wrap the complicated parts into a few functions that are easy to use. It's a bit of a challenge, especially since I currently fear that this could be cause performance problems. But let's see how it goes. Until then: Happy coding!

🍪