Simple tower defense tutorial, part 7: Healthbars and first building graphics

After the refactoring in the last part, let's do something more visual again. It should still keep our overall goal in mind, so let's recap what we have so far:

Let's compare this with what we ultimately want:

Looking at the first sub point, it's clear that we can't continue with cubes. Different enemies means that we need different graphics for each enemy type. And stronger enemies means that our towers need more than one shot to kill them. To signal this to the player, we need healthbars.

This is why graphics and healthbars are the topic of this part. Let's start with the healthbars.

From world to screen

When we want to display a healthbar, we need to know where to draw it. The healthbar should be drawn above the enemy (or building) that it belongs to. The healthbar is drawn in 2D screen space, so we need to convert the 3D world position of the entity to 2D screen space coordinates.

Coordinate conversions are a common task in game development and we already have used the reverse conversion in the last parts: When we placed a building, we converted the mouse position to a ray in the 3D world space. This time, we want to convert a 3D world position to a 2D screen position.

Luckily, raylib provides a function for this: GetWorldToScreen. This function takes a 3D world position and returns the 2D screen position. The function is quite simple to use, see enemy.c, line 509:

  • 💾
  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 Level levels[] = {
 11   [0] = {
 12     .state = LEVEL_STATE_BUILDING,
 13     .initialGold = 10,
 14     .waves[0] = {
 15       .enemyType = ENEMY_TYPE_MINION,
 16       .wave = 0,
 17       .count = 10,
18 .interval = 2.5f,
19 .delay = 1.0f, 20 .spawnPosition = {0, 6}, 21 }, 22 .waves[1] = { 23 .enemyType = ENEMY_TYPE_MINION, 24 .wave = 1, 25 .count = 20,
26 .interval = 1.5f,
27 .delay = 1.0f, 28 .spawnPosition = {0, 6}, 29 }, 30 .waves[2] = { 31 .enemyType = ENEMY_TYPE_MINION, 32 .wave = 2, 33 .count = 30,
34 .interval = 1.2f,
35 .delay = 1.0f, 36 .spawnPosition = {0, 6}, 37 } 38 }, 39 }; 40 41 Level *currentLevel = levels; 42 43 //# Game 44 45 void InitLevel(Level *level) 46 { 47 TowerInit(); 48 EnemyInit(); 49 ProjectileInit(); 50 ParticleInit(); 51 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 52 53 level->placementMode = 0; 54 level->state = LEVEL_STATE_BUILDING; 55 level->nextState = LEVEL_STATE_NONE; 56 level->playerGold = level->initialGold; 57 58 Camera *camera = &level->camera; 59 camera->position = (Vector3){1.0f, 12.0f, 6.5f}; 60 camera->target = (Vector3){0.0f, 0.5f, 1.0f}; 61 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 62 camera->fovy = 45.0f; 63 camera->projection = CAMERA_PERSPECTIVE; 64 } 65 66 void DrawLevelHud(Level *level) 67 { 68 const char *text = TextFormat("Gold: %d", level->playerGold); 69 Font font = GetFontDefault(); 70 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 71 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 72 } 73 74 void DrawLevelReportLostWave(Level *level) 75 { 76 BeginMode3D(level->camera); 77 DrawGrid(10, 1.0f); 78 TowerDraw(); 79 EnemyDraw(); 80 ProjectileDraw(); 81 ParticleDraw(); 82 guiState.isBlocked = 0; 83 EndMode3D(); 84 85 const char *text = "Wave lost"; 86 int textWidth = MeasureText(text, 20); 87 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 88 89 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 90 { 91 level->nextState = LEVEL_STATE_RESET; 92 } 93 } 94 95 int HasLevelNextWave(Level *level) 96 { 97 for (int i = 0; i < 10; i++) 98 { 99 EnemyWave *wave = &level->waves[i]; 100 if (wave->wave == level->currentWave) 101 { 102 return 1; 103 } 104 } 105 return 0; 106 } 107 108 void DrawLevelReportWonWave(Level *level) 109 { 110 BeginMode3D(level->camera); 111 DrawGrid(10, 1.0f); 112 TowerDraw(); 113 EnemyDraw(); 114 ProjectileDraw(); 115 ParticleDraw(); 116 guiState.isBlocked = 0; 117 EndMode3D(); 118 119 const char *text = "Wave won"; 120 int textWidth = MeasureText(text, 20); 121 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 122 123 124 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 125 { 126 level->nextState = LEVEL_STATE_RESET; 127 } 128 129 if (HasLevelNextWave(level)) 130 { 131 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 132 { 133 level->nextState = LEVEL_STATE_BUILDING; 134 } 135 } 136 else { 137 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 138 { 139 level->nextState = LEVEL_STATE_WON_LEVEL; 140 } 141 } 142 } 143 144 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 145 { 146 static ButtonState buttonStates[8] = {0}; 147 int cost = GetTowerCosts(towerType); 148 const char *text = TextFormat("%s: %d", name, cost); 149 buttonStates[towerType].isSelected = level->placementMode == towerType; 150 buttonStates[towerType].isDisabled = level->playerGold < cost; 151 if (Button(text, x, y, width, height, &buttonStates[towerType])) 152 { 153 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 154 } 155 } 156 157 void DrawLevelBuildingState(Level *level) 158 { 159 BeginMode3D(level->camera); 160 DrawGrid(10, 1.0f); 161 TowerDraw(); 162 EnemyDraw(); 163 ProjectileDraw(); 164 ParticleDraw(); 165 166 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 167 float planeDistance = ray.position.y / -ray.direction.y; 168 float planeX = ray.direction.x * planeDistance + ray.position.x; 169 float planeY = ray.direction.z * planeDistance + ray.position.z; 170 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 171 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 172 if (level->placementMode && !guiState.isBlocked) 173 { 174 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 175 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 176 { 177 if (TowerTryAdd(level->placementMode, mapX, mapY)) 178 { 179 level->playerGold -= GetTowerCosts(level->placementMode); 180 level->placementMode = TOWER_TYPE_NONE; 181 } 182 } 183 } 184 185 guiState.isBlocked = 0; 186 187 EndMode3D(); 188 189 static ButtonState buildWallButtonState = {0}; 190 static ButtonState buildGunButtonState = {0}; 191 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 192 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN; 193 194 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall"); 195 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun"); 196 197 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 198 { 199 level->nextState = LEVEL_STATE_RESET; 200 } 201 202 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 203 { 204 level->nextState = LEVEL_STATE_BATTLE; 205 } 206 207 const char *text = "Building phase"; 208 int textWidth = MeasureText(text, 20); 209 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 210 } 211 212 void InitBattleStateConditions(Level *level) 213 { 214 level->state = LEVEL_STATE_BATTLE; 215 level->nextState = LEVEL_STATE_NONE; 216 level->waveEndTimer = 0.0f; 217 for (int i = 0; i < 10; i++) 218 { 219 EnemyWave *wave = &level->waves[i]; 220 wave->spawned = 0; 221 wave->timeToSpawnNext = wave->delay; 222 } 223 } 224 225 void DrawLevelBattleState(Level *level) 226 { 227 BeginMode3D(level->camera); 228 DrawGrid(10, 1.0f); 229 TowerDraw(); 230 EnemyDraw(); 231 ProjectileDraw(); 232 ParticleDraw(); 233 guiState.isBlocked = 0;
234 EndMode3D(); 235 236 EnemyDrawHealthbars(level->camera);
237 238 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 239 { 240 level->nextState = LEVEL_STATE_RESET; 241 } 242 243 int maxCount = 0; 244 int remainingCount = 0; 245 for (int i = 0; i < 10; i++) 246 { 247 EnemyWave *wave = &level->waves[i]; 248 if (wave->wave != level->currentWave) 249 { 250 continue; 251 } 252 maxCount += wave->count; 253 remainingCount += wave->count - wave->spawned; 254 } 255 int aliveCount = EnemyCount(); 256 remainingCount += aliveCount; 257 258 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 259 int textWidth = MeasureText(text, 20); 260 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 261 } 262 263 void DrawLevel(Level *level) 264 { 265 switch (level->state) 266 { 267 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 268 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 269 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 270 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 271 default: break; 272 } 273 274 DrawLevelHud(level); 275 } 276 277 void UpdateLevel(Level *level) 278 { 279 if (level->state == LEVEL_STATE_BATTLE) 280 { 281 int activeWaves = 0; 282 for (int i = 0; i < 10; i++) 283 { 284 EnemyWave *wave = &level->waves[i]; 285 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 286 { 287 continue; 288 } 289 activeWaves++; 290 wave->timeToSpawnNext -= gameTime.deltaTime; 291 if (wave->timeToSpawnNext <= 0.0f) 292 { 293 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 294 if (enemy) 295 { 296 wave->timeToSpawnNext = wave->interval; 297 wave->spawned++; 298 } 299 } 300 } 301 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 302 level->waveEndTimer += gameTime.deltaTime; 303 if (level->waveEndTimer >= 2.0f) 304 { 305 level->nextState = LEVEL_STATE_LOST_WAVE; 306 } 307 } 308 else if (activeWaves == 0 && EnemyCount() == 0) 309 { 310 level->waveEndTimer += gameTime.deltaTime; 311 if (level->waveEndTimer >= 2.0f) 312 { 313 level->nextState = LEVEL_STATE_WON_WAVE; 314 } 315 } 316 } 317 318 PathFindingMapUpdate(); 319 EnemyUpdate(); 320 TowerUpdate(); 321 ProjectileUpdate(); 322 ParticleUpdate(); 323 324 if (level->nextState == LEVEL_STATE_RESET) 325 { 326 InitLevel(level); 327 } 328 329 if (level->nextState == LEVEL_STATE_BATTLE) 330 { 331 InitBattleStateConditions(level); 332 } 333 334 if (level->nextState == LEVEL_STATE_WON_WAVE) 335 { 336 level->currentWave++; 337 level->state = LEVEL_STATE_WON_WAVE; 338 } 339 340 if (level->nextState == LEVEL_STATE_LOST_WAVE) 341 { 342 level->state = LEVEL_STATE_LOST_WAVE; 343 } 344 345 if (level->nextState == LEVEL_STATE_BUILDING) 346 { 347 level->state = LEVEL_STATE_BUILDING; 348 } 349 350 if (level->nextState == LEVEL_STATE_WON_LEVEL) 351 { 352 // make something of this later 353 InitLevel(level); 354 } 355 356 level->nextState = LEVEL_STATE_NONE; 357 } 358 359 float nextSpawnTime = 0.0f; 360 361 void ResetGame() 362 { 363 InitLevel(currentLevel); 364 } 365 366 void InitGame() 367 { 368 TowerInit(); 369 EnemyInit(); 370 ProjectileInit(); 371 ParticleInit(); 372 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 373 374 currentLevel = levels; 375 InitLevel(currentLevel); 376 } 377 378 //# Immediate GUI functions 379 380 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 381 { 382 Rectangle bounds = {x, y, width, height}; 383 int isPressed = 0; 384 int isSelected = state && state->isSelected; 385 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled) 386 { 387 Color color = isSelected ? DARKGRAY : GRAY; 388 DrawRectangle(x, y, width, height, color); 389 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 390 { 391 isPressed = 1; 392 } 393 guiState.isBlocked = 1; 394 } 395 else 396 { 397 Color color = isSelected ? WHITE : LIGHTGRAY; 398 DrawRectangle(x, y, width, height, color); 399 } 400 Font font = GetFontDefault(); 401 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 402 Color textColor = state->isDisabled ? GRAY : BLACK; 403 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 404 return isPressed; 405 } 406 407 //# Main game loop 408 409 void GameUpdate() 410 { 411 float dt = GetFrameTime(); 412 // cap maximum delta time to 0.1 seconds to prevent large time steps 413 if (dt > 0.1f) dt = 0.1f; 414 gameTime.time += dt; 415 gameTime.deltaTime = dt; 416 417 UpdateLevel(currentLevel); 418 } 419 420 int main(void) 421 { 422 int screenWidth, screenHeight; 423 GetPreferredSize(&screenWidth, &screenHeight); 424 InitWindow(screenWidth, screenHeight, "Tower defense"); 425 SetTargetFPS(30); 426 427 InitGame(); 428 429 while (!WindowShouldClose()) 430 { 431 if (IsPaused()) { 432 // canvas is not visible in browser - do nothing 433 continue; 434 } 435 436 BeginDrawing(); 437 ClearBackground(DARKBLUE); 438 439 GameUpdate(); 440 DrawLevel(currentLevel); 441 442 EndDrawing(); 443 } 444 445 CloseWindow(); 446 447 return 0; 448 }
  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 
 35 typedef struct Tower
 36 {
 37   int16_t x, y;
 38   uint8_t towerType;
 39   float cooldown;
 40   float damage;
 41 } Tower;
 42 
 43 typedef struct GameTime
 44 {
 45   float time;
 46   float deltaTime;
 47 } GameTime;
 48 
 49 typedef struct ButtonState {
 50   char isSelected;
 51   char isDisabled;
 52 } ButtonState;
 53 
 54 typedef struct GUIState {
 55   int isBlocked;
 56 } GUIState;
 57 
 58 typedef enum LevelState
 59 {
 60   LEVEL_STATE_NONE,
 61   LEVEL_STATE_BUILDING,
 62   LEVEL_STATE_BATTLE,
 63   LEVEL_STATE_WON_WAVE,
 64   LEVEL_STATE_LOST_WAVE,
 65   LEVEL_STATE_WON_LEVEL,
 66   LEVEL_STATE_RESET,
 67 } LevelState;
 68 
 69 typedef struct EnemyWave {
 70   uint8_t enemyType;
 71   uint8_t wave;
 72   uint16_t count;
 73   float interval;
 74   float delay;
 75   Vector2 spawnPosition;
 76 
 77   uint16_t spawned;
 78   float timeToSpawnNext;
 79 } EnemyWave;
 80 
 81 typedef struct Level
 82 {
 83   LevelState state;
 84   LevelState nextState;
 85   Camera3D camera;
 86   int placementMode;
 87 
 88   int initialGold;
 89   int playerGold;
 90 
 91   EnemyWave waves[10];
 92   int currentWave;
 93   float waveEndTimer;
 94 } Level;
 95 
 96 typedef struct DeltaSrc
 97 {
 98   char x, y;
 99 } DeltaSrc;
100 
101 typedef struct PathfindingMap
102 {
103   int width, height;
104   float scale;
105   float *distances;
106   long *towerIndex; 
107   DeltaSrc *deltaSrc;
108   float maxDistance;
109   Matrix toMapSpace;
110   Matrix toWorldSpace;
111 } PathfindingMap;
112 
113 // when we execute the pathfinding algorithm, we need to store the active nodes
114 // in a queue. Each node has a position, a distance from the start, and the
115 // position of the node that we came from.
116 typedef struct PathfindingNode
117 {
118   int16_t x, y, fromX, fromY;
119   float distance;
120 } PathfindingNode;
121 
122 typedef struct EnemyId
123 {
124   uint16_t index;
125   uint16_t generation;
126 } EnemyId;
127 
128 typedef struct EnemyClassConfig
129 {
130   float speed;
131   float health;
132   float radius;
133   float maxAcceleration;
134   float requiredContactTime;
135   float explosionDamage;
136   float explosionRange;
137   float explosionPushbackPower;
138   int goldValue;
139 } EnemyClassConfig;
140 
141 typedef struct Enemy
142 {
143   int16_t currentX, currentY;
144   int16_t nextX, nextY;
145   Vector2 simPosition;
146   Vector2 simVelocity;
147   uint16_t generation;
148   float startMovingTime;
149   float damage, futureDamage;
150   float contactTime;
151   uint8_t enemyType;
152   uint8_t movePathCount;
153   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
154 } Enemy;
155 
156 #define PROJECTILE_MAX_COUNT 1200
157 #define PROJECTILE_TYPE_NONE 0
158 #define PROJECTILE_TYPE_BULLET 1
159 
160 typedef struct Projectile
161 {
162   uint8_t projectileType;
163   float shootTime;
164   float arrivalTime;
165   float damage;
166   Vector2 position;
167   Vector2 target;
168   Vector2 directionNormal;
169   EnemyId targetEnemy;
170 } Projectile;
171 
172 //# Function declarations
173 float TowerGetMaxHealth(Tower *tower);
174 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
175 int EnemyAddDamage(Enemy *enemy, float damage);
176 
177 //# Enemy functions
178 void EnemyInit();
179 void EnemyDraw();
180 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
181 void EnemyUpdate();
182 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
183 float EnemyGetMaxHealth(Enemy *enemy);
184 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
185 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
186 EnemyId EnemyGetId(Enemy *enemy);
187 Enemy *EnemyTryResolve(EnemyId enemyId);
188 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
189 int EnemyAddDamage(Enemy *enemy, float damage);
190 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
191 int EnemyCount();
192 void EnemyDrawHealthbars(Camera3D camera);
193 194 //# Tower functions 195 void TowerInit(); 196 Tower *TowerGetAt(int16_t x, int16_t y); 197 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 198 Tower *GetTowerByType(uint8_t towerType); 199 int GetTowerCosts(uint8_t towerType); 200 float TowerGetMaxHealth(Tower *tower); 201 void TowerDraw(); 202 void TowerUpdate(); 203 204 //# Particles 205 void ParticleInit(); 206 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime); 207 void ParticleUpdate(); 208 void ParticleDraw(); 209 210 //# Projectiles 211 void ProjectileInit(); 212 void ProjectileDraw(); 213 void ProjectileUpdate(); 214 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage); 215 216 //# Pathfinding map 217 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 218 float PathFindingGetDistance(int mapX, int mapY); 219 Vector2 PathFindingGetGradient(Vector3 world); 220 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 221 void PathFindingMapUpdate(); 222 void PathFindingMapDraw(); 223 224 //# variables 225 extern Level *currentLevel; 226 extern Enemy enemies[ENEMY_MAX_COUNT]; 227 extern int enemyCount; 228 extern EnemyClassConfig enemyClassConfigs[]; 229 230 extern GUIState guiState; 231 extern GameTime gameTime; 232 extern Tower towers[TOWER_MAX_COUNT]; 233 extern int towerCount; 234 235 #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 void EnemyInit() 24 { 25 for (int i = 0; i < ENEMY_MAX_COUNT; i++) 26 { 27 enemies[i] = (Enemy){0}; 28 } 29 enemyCount = 0; 30 } 31 32 float EnemyGetCurrentMaxSpeed(Enemy *enemy) 33 { 34 return enemyClassConfigs[enemy->enemyType].speed; 35 } 36 37 float EnemyGetMaxHealth(Enemy *enemy) 38 { 39 return enemyClassConfigs[enemy->enemyType].health; 40 } 41 42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY) 43 { 44 int16_t castleX = 0; 45 int16_t castleY = 0; 46 int16_t dx = castleX - currentX; 47 int16_t dy = castleY - currentY; 48 if (dx == 0 && dy == 0) 49 { 50 *nextX = currentX; 51 *nextY = currentY; 52 return 1; 53 } 54 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY}); 55 56 if (gradient.x == 0 && gradient.y == 0) 57 { 58 *nextX = currentX; 59 *nextY = currentY; 60 return 1; 61 } 62 63 if (fabsf(gradient.x) > fabsf(gradient.y)) 64 { 65 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1); 66 *nextY = currentY; 67 return 0; 68 } 69 *nextX = currentX; 70 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1); 71 return 0; 72 } 73 74 75 // this function predicts the movement of the unit for the next deltaT seconds 76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount) 77 { 78 const float pointReachedDistance = 0.25f; 79 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance; 80 const float maxSimStepTime = 0.015625f; 81 82 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration; 83 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy); 84 int16_t nextX = enemy->nextX; 85 int16_t nextY = enemy->nextY; 86 Vector2 position = enemy->simPosition; 87 int passedCount = 0; 88 for (float t = 0.0f; t < deltaT; t += maxSimStepTime) 89 { 90 float stepTime = fminf(deltaT - t, maxSimStepTime); 91 Vector2 target = (Vector2){nextX, nextY}; 92 float speed = Vector2Length(*velocity); 93 // draw the target position for debugging 94 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED); 95 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed)); 96 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2) 97 { 98 // we reached the target position, let's move to the next waypoint 99 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY); 100 target = (Vector2){nextX, nextY}; 101 // track how many waypoints we passed 102 passedCount++; 103 } 104 105 // acceleration towards the target 106 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos)); 107 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime); 108 *velocity = Vector2Add(*velocity, acceleration); 109 110 // limit the speed to the maximum speed 111 if (speed > maxSpeed) 112 { 113 *velocity = Vector2Scale(*velocity, maxSpeed / speed); 114 } 115 116 // move the enemy 117 position = Vector2Add(position, Vector2Scale(*velocity, stepTime)); 118 } 119 120 if (waypointPassedCount) 121 { 122 (*waypointPassedCount) = passedCount; 123 } 124 125 return position; 126 } 127 128 void EnemyDraw() 129 { 130 for (int i = 0; i < enemyCount; i++) 131 { 132 Enemy enemy = enemies[i]; 133 if (enemy.enemyType == ENEMY_TYPE_NONE) 134 { 135 continue; 136 } 137 138 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0); 139 140 if (enemy.movePathCount > 0) 141 { 142 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y}; 143 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN); 144 } 145 for (int j = 1; j < enemy.movePathCount; j++) 146 { 147 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y}; 148 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y}; 149 DrawLine3D(p, q, GREEN); 150 } 151 152 switch (enemy.enemyType) 153 { 154 case ENEMY_TYPE_MINION: 155 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN); 156 break; 157 } 158 } 159 } 160 161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource) 162 { 163 // damage the tower 164 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage; 165 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange; 166 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower; 167 float explosionRange2 = explosionRange * explosionRange; 168 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage; 169 // explode the enemy 170 if (tower->damage >= TowerGetMaxHealth(tower)) 171 { 172 tower->towerType = TOWER_TYPE_NONE; 173 } 174 175 ParticleAdd(PARTICLE_TYPE_EXPLOSION, 176 explosionSource, 177 (Vector3){0, 0.1f, 0}, 1.0f); 178 179 enemy->enemyType = ENEMY_TYPE_NONE; 180 181 // push back enemies & dealing damage 182 for (int i = 0; i < enemyCount; i++) 183 { 184 Enemy *other = &enemies[i]; 185 if (other->enemyType == ENEMY_TYPE_NONE) 186 { 187 continue; 188 } 189 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition); 190 if (distanceSqr > 0 && distanceSqr < explosionRange2) 191 { 192 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition)); 193 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower)); 194 EnemyAddDamage(other, explosionDamge); 195 } 196 } 197 } 198 199 void EnemyUpdate() 200 { 201 const float castleX = 0; 202 const float castleY = 0; 203 const float maxPathDistance2 = 0.25f * 0.25f; 204 205 for (int i = 0; i < enemyCount; i++) 206 { 207 Enemy *enemy = &enemies[i]; 208 if (enemy->enemyType == ENEMY_TYPE_NONE) 209 { 210 continue; 211 } 212 213 int waypointPassedCount = 0; 214 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount); 215 enemy->startMovingTime = gameTime.time; 216 // track path of unit 217 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2) 218 { 219 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--) 220 { 221 enemy->movePath[j] = enemy->movePath[j - 1]; 222 } 223 enemy->movePath[0] = enemy->simPosition; 224 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT) 225 { 226 enemy->movePathCount = ENEMY_MAX_PATH_COUNT; 227 } 228 } 229 230 if (waypointPassedCount > 0) 231 { 232 enemy->currentX = enemy->nextX; 233 enemy->currentY = enemy->nextY; 234 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) && 235 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f) 236 { 237 // enemy reached the castle; remove it 238 enemy->enemyType = ENEMY_TYPE_NONE; 239 continue; 240 } 241 } 242 } 243 244 // handle collisions between enemies 245 for (int i = 0; i < enemyCount - 1; i++) 246 { 247 Enemy *enemyA = &enemies[i]; 248 if (enemyA->enemyType == ENEMY_TYPE_NONE) 249 { 250 continue; 251 } 252 for (int j = i + 1; j < enemyCount; j++) 253 { 254 Enemy *enemyB = &enemies[j]; 255 if (enemyB->enemyType == ENEMY_TYPE_NONE) 256 { 257 continue; 258 } 259 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition); 260 float radiusA = enemyClassConfigs[enemyA->enemyType].radius; 261 float radiusB = enemyClassConfigs[enemyB->enemyType].radius; 262 float radiusSum = radiusA + radiusB; 263 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f) 264 { 265 // collision 266 float distance = sqrtf(distanceSqr); 267 float overlap = radiusSum - distance; 268 // move the enemies apart, but softly; if we have a clog of enemies, 269 // moving them perfectly apart can cause them to jitter 270 float positionCorrection = overlap / 5.0f; 271 Vector2 direction = (Vector2){ 272 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection, 273 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection}; 274 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction); 275 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction); 276 } 277 } 278 } 279 280 // handle collisions between enemies and towers 281 for (int i = 0; i < enemyCount; i++) 282 { 283 Enemy *enemy = &enemies[i]; 284 if (enemy->enemyType == ENEMY_TYPE_NONE) 285 { 286 continue; 287 } 288 enemy->contactTime -= gameTime.deltaTime; 289 if (enemy->contactTime < 0.0f) 290 { 291 enemy->contactTime = 0.0f; 292 } 293 294 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius; 295 // linear search over towers; could be optimized by using path finding tower map, 296 // but for now, we keep it simple 297 for (int j = 0; j < towerCount; j++) 298 { 299 Tower *tower = &towers[j]; 300 if (tower->towerType == TOWER_TYPE_NONE) 301 { 302 continue; 303 } 304 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y}); 305 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1 306 if (distanceSqr > combinedRadius * combinedRadius) 307 { 308 continue; 309 } 310 // potential collision; square / circle intersection 311 float dx = tower->x - enemy->simPosition.x; 312 float dy = tower->y - enemy->simPosition.y; 313 float absDx = fabsf(dx); 314 float absDy = fabsf(dy); 315 Vector3 contactPoint = {0}; 316 if (absDx <= 0.5f && absDx <= absDy) { 317 // vertical collision; push the enemy out horizontally 318 float overlap = enemyRadius + 0.5f - absDy; 319 if (overlap < 0.0f) 320 { 321 continue; 322 } 323 float direction = dy > 0.0f ? -1.0f : 1.0f; 324 enemy->simPosition.y += direction * overlap; 325 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f}; 326 } 327 else if (absDy <= 0.5f && absDy <= absDx) 328 { 329 // horizontal collision; push the enemy out vertically 330 float overlap = enemyRadius + 0.5f - absDx; 331 if (overlap < 0.0f) 332 { 333 continue; 334 } 335 float direction = dx > 0.0f ? -1.0f : 1.0f; 336 enemy->simPosition.x += direction * overlap; 337 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y}; 338 } 339 else 340 { 341 // possible collision with a corner 342 float cornerDX = dx > 0.0f ? -0.5f : 0.5f; 343 float cornerDY = dy > 0.0f ? -0.5f : 0.5f; 344 float cornerX = tower->x + cornerDX; 345 float cornerY = tower->y + cornerDY; 346 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY}); 347 if (cornerDistanceSqr > enemyRadius * enemyRadius) 348 { 349 continue; 350 } 351 // push the enemy out along the diagonal 352 float cornerDistance = sqrtf(cornerDistanceSqr); 353 float overlap = enemyRadius - cornerDistance; 354 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX; 355 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY; 356 enemy->simPosition.x -= directionX * overlap; 357 enemy->simPosition.y -= directionY * overlap; 358 contactPoint = (Vector3){cornerX, 0.2f, cornerY}; 359 } 360 361 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f) 362 { 363 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above 364 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime) 365 { 366 EnemyTriggerExplode(enemy, tower, contactPoint); 367 } 368 } 369 } 370 } 371 } 372 373 EnemyId EnemyGetId(Enemy *enemy) 374 { 375 return (EnemyId){enemy - enemies, enemy->generation}; 376 } 377 378 Enemy *EnemyTryResolve(EnemyId enemyId) 379 { 380 if (enemyId.index >= ENEMY_MAX_COUNT) 381 { 382 return 0; 383 } 384 Enemy *enemy = &enemies[enemyId.index]; 385 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE) 386 { 387 return 0; 388 } 389 return enemy; 390 } 391 392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 393 { 394 Enemy *spawn = 0; 395 for (int i = 0; i < enemyCount; i++) 396 { 397 Enemy *enemy = &enemies[i]; 398 if (enemy->enemyType == ENEMY_TYPE_NONE) 399 { 400 spawn = enemy; 401 break; 402 } 403 } 404 405 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 406 { 407 spawn = &enemies[enemyCount++]; 408 } 409 410 if (spawn) 411 { 412 spawn->currentX = currentX; 413 spawn->currentY = currentY; 414 spawn->nextX = currentX; 415 spawn->nextY = currentY; 416 spawn->simPosition = (Vector2){currentX, currentY}; 417 spawn->simVelocity = (Vector2){0, 0}; 418 spawn->enemyType = enemyType; 419 spawn->startMovingTime = gameTime.time; 420 spawn->damage = 0.0f; 421 spawn->futureDamage = 0.0f; 422 spawn->generation++; 423 spawn->movePathCount = 0; 424 } 425 426 return spawn; 427 } 428 429 int EnemyAddDamage(Enemy *enemy, float damage) 430 { 431 enemy->damage += damage; 432 if (enemy->damage >= EnemyGetMaxHealth(enemy)) 433 { 434 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue; 435 enemy->enemyType = ENEMY_TYPE_NONE; 436 return 1; 437 } 438 439 return 0; 440 } 441 442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 443 { 444 int16_t castleX = 0; 445 int16_t castleY = 0; 446 Enemy* closest = 0; 447 int16_t closestDistance = 0; 448 float range2 = range * range; 449 for (int i = 0; i < enemyCount; i++) 450 { 451 Enemy* enemy = &enemies[i]; 452 if (enemy->enemyType == ENEMY_TYPE_NONE) 453 { 454 continue; 455 } 456 float maxHealth = EnemyGetMaxHealth(enemy); 457 if (enemy->futureDamage >= maxHealth) 458 { 459 // ignore enemies that will die soon 460 continue; 461 } 462 int16_t dx = castleX - enemy->currentX; 463 int16_t dy = castleY - enemy->currentY; 464 int16_t distance = abs(dx) + abs(dy); 465 if (!closest || distance < closestDistance) 466 { 467 float tdx = towerX - enemy->currentX; 468 float tdy = towerY - enemy->currentY; 469 float tdistance2 = tdx * tdx + tdy * tdy; 470 if (tdistance2 <= range2) 471 { 472 closest = enemy; 473 closestDistance = distance; 474 } 475 } 476 } 477 return closest; 478 } 479 480 int EnemyCount() 481 { 482 int count = 0; 483 for (int i = 0; i < enemyCount; i++) 484 { 485 if (enemies[i].enemyType != ENEMY_TYPE_NONE) 486 { 487 count++; 488 } 489 } 490 return count; 491 }
492 493 void EnemyDrawHealthbars(Camera3D camera) 494 { 495 const float healthBarWidth = 40.0f; 496 const float healthBarHeight = 6.0f; 497 const float healthBarOffset = 15.0f; 498 const float inset = 2.0f; 499 const float innerWidth = healthBarWidth - inset * 2; 500 const float innerHeight = healthBarHeight - inset * 2; 501 for (int i = 0; i < enemyCount; i++) 502 { 503 Enemy *enemy = &enemies[i]; 504 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f) 505 { 506 continue; 507 } 508 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y}; 509 Vector2 screenPos = GetWorldToScreen(position, camera); 510 float maxHealth = EnemyGetMaxHealth(enemy); 511 float health = maxHealth - enemy->damage; 512 float healthRatio = health / maxHealth; 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, GREEN); 518 } 519 }
  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     Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
 30     float x = position.x;
 31     float y = position.y;
 32     float dx = projectile.directionNormal.x;
 33     float dy = projectile.directionNormal.y;
 34     for (float d = 1.0f; d > 0.0f; d -= 0.25f)
 35     {
 36       x -= dx * 0.1f;
 37       y -= dy * 0.1f;
 38       float size = 0.1f * d;
 39       DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
 40     }
 41   }
 42 }
 43 
 44 void ProjectileUpdate()
 45 {
 46   for (int i = 0; i < projectileCount; i++)
 47   {
 48     Projectile *projectile = &projectiles[i];
 49     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 50     {
 51       continue;
 52     }
 53     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 54     if (transition >= 1.0f)
 55     {
 56       projectile->projectileType = PROJECTILE_TYPE_NONE;
 57       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 58       if (enemy)
 59       {
 60         EnemyAddDamage(enemy, projectile->damage);
 61       }
 62       continue;
 63     }
 64   }
 65 }
 66 
 67 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
 68 {
 69   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 70   {
 71     Projectile *projectile = &projectiles[i];
 72     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 73     {
 74       projectile->projectileType = projectileType;
 75       projectile->shootTime = gameTime.time;
 76       projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
 77       projectile->damage = damage;
 78       projectile->position = position;
 79       projectile->target = target;
 80       projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
 81       projectile->targetEnemy = EnemyGetId(enemy);
 82       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 83       return projectile;
 84     }
 85   }
 86   return 0;
 87 }
  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 void TowerInit()
  8 {
  9   for (int i = 0; i < TOWER_MAX_COUNT; i++)
 10   {
 11     towers[i] = (Tower){0};
 12   }
 13   towerCount = 0;
 14 }
 15 
 16 static void TowerGunUpdate(Tower *tower)
 17 {
 18   if (tower->cooldown <= 0)
 19   {
 20     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
 21     if (enemy)
 22     {
23 tower->cooldown = 0.5f;
24 // shoot the enemy; determine future position of the enemy 25 float bulletSpeed = 1.0f; 26 float bulletDamage = 3.0f; 27 Vector2 velocity = enemy->simVelocity; 28 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 29 Vector2 towerPosition = {tower->x, tower->y}; 30 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 31 for (int i = 0; i < 8; i++) { 32 velocity = enemy->simVelocity; 33 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 34 float distance = Vector2Distance(towerPosition, futurePosition); 35 float eta2 = distance / bulletSpeed; 36 if (fabs(eta - eta2) < 0.01f) { 37 break; 38 } 39 eta = (eta2 + eta) * 0.5f; 40 } 41 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition, 42 bulletSpeed, bulletDamage); 43 enemy->futureDamage += bulletDamage; 44 } 45 } 46 else 47 { 48 tower->cooldown -= gameTime.deltaTime; 49 } 50 } 51 52 Tower *TowerGetAt(int16_t x, int16_t y) 53 { 54 for (int i = 0; i < towerCount; i++) 55 { 56 if (towers[i].x == x && towers[i].y == y) 57 { 58 return &towers[i]; 59 } 60 } 61 return 0; 62 } 63 64 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 65 { 66 if (towerCount >= TOWER_MAX_COUNT) 67 { 68 return 0; 69 } 70 71 Tower *tower = TowerGetAt(x, y); 72 if (tower) 73 { 74 return 0; 75 } 76 77 tower = &towers[towerCount++]; 78 tower->x = x; 79 tower->y = y; 80 tower->towerType = towerType; 81 tower->cooldown = 0.0f; 82 tower->damage = 0.0f; 83 return tower; 84 } 85 86 Tower *GetTowerByType(uint8_t towerType) 87 { 88 for (int i = 0; i < towerCount; i++) 89 { 90 if (towers[i].towerType == towerType) 91 { 92 return &towers[i]; 93 } 94 } 95 return 0; 96 } 97 98 int GetTowerCosts(uint8_t towerType) 99 { 100 switch (towerType) 101 { 102 case TOWER_TYPE_BASE: 103 return 0; 104 case TOWER_TYPE_GUN: 105 return 6; 106 case TOWER_TYPE_WALL: 107 return 2; 108 } 109 return 0; 110 } 111 112 float TowerGetMaxHealth(Tower *tower) 113 { 114 switch (tower->towerType) 115 { 116 case TOWER_TYPE_BASE: 117 return 10.0f; 118 case TOWER_TYPE_GUN: 119 return 3.0f; 120 case TOWER_TYPE_WALL: 121 return 5.0f; 122 } 123 return 0.0f; 124 } 125 126 void TowerDraw() 127 { 128 for (int i = 0; i < towerCount; i++) 129 { 130 Tower tower = towers[i]; 131 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 132 switch (tower.towerType) 133 { 134 case TOWER_TYPE_BASE: 135 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 136 break; 137 case TOWER_TYPE_GUN: 138 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 139 break; 140 case TOWER_TYPE_WALL: 141 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 142 break; 143 } 144 } 145 } 146 147 void TowerUpdate() 148 { 149 for (int i = 0; i < towerCount; i++) 150 { 151 Tower *tower = &towers[i]; 152 switch (tower->towerType) 153 { 154 case TOWER_TYPE_GUN: 155 TowerGunUpdate(tower); 156 break; 157 } 158 } 159 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif

When placing a tower and starting the wave, once the tower hits an enemy, the enemy's healthbar appears above the enemy:

Healthbars of damaged enemies being shown above them.

To highlight once more the code that does this, here is the relevant part:

  1 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
  2 Vector2 screenPos = GetWorldToScreen(position, camera);
  3 float maxHealth = EnemyGetMaxHealth(enemy);
  4 float health = maxHealth - enemy->damage;
  5 float healthRatio = health / maxHealth;
  6 float centerX = screenPos.x - healthBarWidth * 0.5f;
  7 float topY = screenPos.y - healthBarOffset;
  8 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
  9 float healthWidth = innerWidth * healthRatio;
 10 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, GREEN);

This is pretty straight forward: We get the screen position of the enemy, calculate the health ratio and draw two rectangles: One black rectangle as the background and one green rectangle as the health bar itself. The green rectangle is scaled according to the health ratio.

Note: It is important that we use the correct camera and position of the object we want to draw the overlay for. A typical problem for this kind of task is that the UI is not synchronized with the game world's position. E.g. if we move the camera between drawing the game world and the UI, the healthbar would be drawn at the wrong position. Getting the order of drawing and updating the game world and camera right is crucial.

Now we want to add a healthbar for the buildings as well. We could copy paste the code above and adapt it for the buildings, but we could also extract the logic for rendering the healthbar into a separate function. This way, we can reuse the code for both enemies and buildings, though we might need to adapt it slightly for each case. Let's do this:

  • 💾
  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 Level levels[] = {
 11   [0] = {
 12     .state = LEVEL_STATE_BUILDING,
13 .initialGold = 20,
14 .waves[0] = { 15 .enemyType = ENEMY_TYPE_MINION, 16 .wave = 0, 17 .count = 10, 18 .interval = 2.5f, 19 .delay = 1.0f, 20 .spawnPosition = {0, 6}, 21 }, 22 .waves[1] = { 23 .enemyType = ENEMY_TYPE_MINION, 24 .wave = 1, 25 .count = 20, 26 .interval = 1.5f, 27 .delay = 1.0f, 28 .spawnPosition = {0, 6}, 29 }, 30 .waves[2] = { 31 .enemyType = ENEMY_TYPE_MINION, 32 .wave = 2, 33 .count = 30, 34 .interval = 1.2f, 35 .delay = 1.0f, 36 .spawnPosition = {0, 6}, 37 } 38 }, 39 }; 40 41 Level *currentLevel = levels; 42 43 //# Game 44 45 void InitLevel(Level *level) 46 { 47 TowerInit(); 48 EnemyInit(); 49 ProjectileInit(); 50 ParticleInit(); 51 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 52 53 level->placementMode = 0; 54 level->state = LEVEL_STATE_BUILDING; 55 level->nextState = LEVEL_STATE_NONE; 56 level->playerGold = level->initialGold; 57 58 Camera *camera = &level->camera; 59 camera->position = (Vector3){1.0f, 12.0f, 6.5f}; 60 camera->target = (Vector3){0.0f, 0.5f, 1.0f}; 61 camera->up = (Vector3){0.0f, 1.0f, 0.0f}; 62 camera->fovy = 45.0f; 63 camera->projection = CAMERA_PERSPECTIVE; 64 } 65 66 void DrawLevelHud(Level *level) 67 { 68 const char *text = TextFormat("Gold: %d", level->playerGold); 69 Font font = GetFontDefault(); 70 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 71 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 72 } 73 74 void DrawLevelReportLostWave(Level *level) 75 { 76 BeginMode3D(level->camera); 77 DrawGrid(10, 1.0f); 78 TowerDraw(); 79 EnemyDraw(); 80 ProjectileDraw(); 81 ParticleDraw(); 82 guiState.isBlocked = 0;
83 EndMode3D(); 84 85 TowerDrawHealthBars(level->camera);
86 87 const char *text = "Wave lost"; 88 int textWidth = MeasureText(text, 20); 89 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 90 91 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 92 { 93 level->nextState = LEVEL_STATE_RESET; 94 } 95 } 96 97 int HasLevelNextWave(Level *level) 98 { 99 for (int i = 0; i < 10; i++) 100 { 101 EnemyWave *wave = &level->waves[i]; 102 if (wave->wave == level->currentWave) 103 { 104 return 1; 105 } 106 } 107 return 0; 108 } 109 110 void DrawLevelReportWonWave(Level *level) 111 { 112 BeginMode3D(level->camera); 113 DrawGrid(10, 1.0f); 114 TowerDraw(); 115 EnemyDraw(); 116 ProjectileDraw(); 117 ParticleDraw(); 118 guiState.isBlocked = 0;
119 EndMode3D(); 120 121 TowerDrawHealthBars(level->camera);
122 123 const char *text = "Wave won"; 124 int textWidth = MeasureText(text, 20); 125 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 126 127 128 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 129 { 130 level->nextState = LEVEL_STATE_RESET; 131 } 132 133 if (HasLevelNextWave(level)) 134 { 135 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 136 { 137 level->nextState = LEVEL_STATE_BUILDING; 138 } 139 } 140 else { 141 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 142 { 143 level->nextState = LEVEL_STATE_WON_LEVEL; 144 } 145 } 146 } 147 148 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 149 { 150 static ButtonState buttonStates[8] = {0}; 151 int cost = GetTowerCosts(towerType); 152 const char *text = TextFormat("%s: %d", name, cost); 153 buttonStates[towerType].isSelected = level->placementMode == towerType; 154 buttonStates[towerType].isDisabled = level->playerGold < cost; 155 if (Button(text, x, y, width, height, &buttonStates[towerType])) 156 { 157 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 158 } 159 } 160 161 void DrawLevelBuildingState(Level *level) 162 { 163 BeginMode3D(level->camera); 164 DrawGrid(10, 1.0f); 165 TowerDraw(); 166 EnemyDraw(); 167 ProjectileDraw(); 168 ParticleDraw(); 169 170 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 171 float planeDistance = ray.position.y / -ray.direction.y; 172 float planeX = ray.direction.x * planeDistance + ray.position.x; 173 float planeY = ray.direction.z * planeDistance + ray.position.z; 174 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 175 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 176 if (level->placementMode && !guiState.isBlocked) 177 { 178 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 179 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 180 { 181 if (TowerTryAdd(level->placementMode, mapX, mapY)) 182 { 183 level->playerGold -= GetTowerCosts(level->placementMode); 184 level->placementMode = TOWER_TYPE_NONE; 185 } 186 } 187 } 188 189 guiState.isBlocked = 0; 190
191 EndMode3D(); 192 193 TowerDrawHealthBars(level->camera);
194 195 static ButtonState buildWallButtonState = {0}; 196 static ButtonState buildGunButtonState = {0}; 197 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 198 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN; 199 200 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall"); 201 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun"); 202 203 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 204 { 205 level->nextState = LEVEL_STATE_RESET; 206 } 207 208 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 209 { 210 level->nextState = LEVEL_STATE_BATTLE; 211 } 212 213 const char *text = "Building phase"; 214 int textWidth = MeasureText(text, 20); 215 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 216 } 217 218 void InitBattleStateConditions(Level *level) 219 { 220 level->state = LEVEL_STATE_BATTLE; 221 level->nextState = LEVEL_STATE_NONE; 222 level->waveEndTimer = 0.0f; 223 for (int i = 0; i < 10; i++) 224 { 225 EnemyWave *wave = &level->waves[i]; 226 wave->spawned = 0; 227 wave->timeToSpawnNext = wave->delay; 228 } 229 } 230 231 void DrawLevelBattleState(Level *level) 232 { 233 BeginMode3D(level->camera); 234 DrawGrid(10, 1.0f); 235 TowerDraw(); 236 EnemyDraw(); 237 ProjectileDraw(); 238 ParticleDraw(); 239 guiState.isBlocked = 0; 240 EndMode3D(); 241
242 EnemyDrawHealthbars(level->camera); 243 TowerDrawHealthBars(level->camera);
244 245 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 246 { 247 level->nextState = LEVEL_STATE_RESET; 248 } 249 250 int maxCount = 0; 251 int remainingCount = 0; 252 for (int i = 0; i < 10; i++) 253 { 254 EnemyWave *wave = &level->waves[i]; 255 if (wave->wave != level->currentWave) 256 { 257 continue; 258 } 259 maxCount += wave->count; 260 remainingCount += wave->count - wave->spawned; 261 } 262 int aliveCount = EnemyCount(); 263 remainingCount += aliveCount; 264 265 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 266 int textWidth = MeasureText(text, 20); 267 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 268 } 269 270 void DrawLevel(Level *level) 271 { 272 switch (level->state) 273 { 274 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 275 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 276 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 277 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 278 default: break; 279 } 280 281 DrawLevelHud(level); 282 } 283 284 void UpdateLevel(Level *level) 285 { 286 if (level->state == LEVEL_STATE_BATTLE) 287 { 288 int activeWaves = 0; 289 for (int i = 0; i < 10; i++) 290 { 291 EnemyWave *wave = &level->waves[i]; 292 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 293 { 294 continue; 295 } 296 activeWaves++; 297 wave->timeToSpawnNext -= gameTime.deltaTime; 298 if (wave->timeToSpawnNext <= 0.0f) 299 { 300 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 301 if (enemy) 302 { 303 wave->timeToSpawnNext = wave->interval; 304 wave->spawned++; 305 } 306 } 307 } 308 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 309 level->waveEndTimer += gameTime.deltaTime; 310 if (level->waveEndTimer >= 2.0f) 311 { 312 level->nextState = LEVEL_STATE_LOST_WAVE; 313 } 314 } 315 else if (activeWaves == 0 && EnemyCount() == 0) 316 { 317 level->waveEndTimer += gameTime.deltaTime; 318 if (level->waveEndTimer >= 2.0f) 319 { 320 level->nextState = LEVEL_STATE_WON_WAVE; 321 } 322 } 323 } 324 325 PathFindingMapUpdate(); 326 EnemyUpdate(); 327 TowerUpdate(); 328 ProjectileUpdate(); 329 ParticleUpdate(); 330 331 if (level->nextState == LEVEL_STATE_RESET) 332 { 333 InitLevel(level); 334 } 335 336 if (level->nextState == LEVEL_STATE_BATTLE) 337 { 338 InitBattleStateConditions(level); 339 } 340 341 if (level->nextState == LEVEL_STATE_WON_WAVE) 342 { 343 level->currentWave++; 344 level->state = LEVEL_STATE_WON_WAVE; 345 } 346 347 if (level->nextState == LEVEL_STATE_LOST_WAVE) 348 { 349 level->state = LEVEL_STATE_LOST_WAVE; 350 } 351 352 if (level->nextState == LEVEL_STATE_BUILDING) 353 { 354 level->state = LEVEL_STATE_BUILDING; 355 } 356 357 if (level->nextState == LEVEL_STATE_WON_LEVEL) 358 { 359 // make something of this later 360 InitLevel(level); 361 } 362 363 level->nextState = LEVEL_STATE_NONE; 364 } 365 366 float nextSpawnTime = 0.0f; 367 368 void ResetGame() 369 { 370 InitLevel(currentLevel); 371 } 372 373 void InitGame() 374 { 375 TowerInit(); 376 EnemyInit(); 377 ProjectileInit(); 378 ParticleInit(); 379 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 380 381 currentLevel = levels; 382 InitLevel(currentLevel); 383 } 384
385 //# Immediate GUI functions 386 387 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor) 388 { 389 const float healthBarWidth = 40.0f; 390 const float healthBarHeight = 6.0f; 391 const float healthBarOffset = 15.0f; 392 const float inset = 2.0f; 393 const float innerWidth = healthBarWidth - inset * 2; 394 const float innerHeight = healthBarHeight - inset * 2; 395 396 Vector2 screenPos = GetWorldToScreen(position, camera); 397 float centerX = screenPos.x - healthBarWidth * 0.5f; 398 float topY = screenPos.y - healthBarOffset; 399 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 400 float healthWidth = innerWidth * healthRatio; 401 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 402 }
403 404 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 405 { 406 Rectangle bounds = {x, y, width, height}; 407 int isPressed = 0; 408 int isSelected = state && state->isSelected; 409 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled) 410 { 411 Color color = isSelected ? DARKGRAY : GRAY; 412 DrawRectangle(x, y, width, height, color); 413 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 414 { 415 isPressed = 1; 416 } 417 guiState.isBlocked = 1; 418 } 419 else 420 { 421 Color color = isSelected ? WHITE : LIGHTGRAY; 422 DrawRectangle(x, y, width, height, color); 423 } 424 Font font = GetFontDefault(); 425 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 426 Color textColor = state->isDisabled ? GRAY : BLACK; 427 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 428 return isPressed; 429 } 430 431 //# Main game loop 432 433 void GameUpdate() 434 { 435 float dt = GetFrameTime(); 436 // cap maximum delta time to 0.1 seconds to prevent large time steps 437 if (dt > 0.1f) dt = 0.1f; 438 gameTime.time += dt; 439 gameTime.deltaTime = dt; 440 441 UpdateLevel(currentLevel); 442 } 443 444 int main(void) 445 { 446 int screenWidth, screenHeight; 447 GetPreferredSize(&screenWidth, &screenHeight); 448 InitWindow(screenWidth, screenHeight, "Tower defense"); 449 SetTargetFPS(30); 450 451 InitGame(); 452 453 while (!WindowShouldClose()) 454 { 455 if (IsPaused()) { 456 // canvas is not visible in browser - do nothing 457 continue; 458 } 459 460 BeginDrawing(); 461 ClearBackground(DARKBLUE); 462 463 GameUpdate(); 464 DrawLevel(currentLevel); 465 466 EndDrawing(); 467 } 468 469 CloseWindow(); 470 471 return 0; 472 }
  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 
 35 typedef struct Tower
 36 {
 37   int16_t x, y;
 38   uint8_t towerType;
 39   float cooldown;
 40   float damage;
 41 } Tower;
 42 
 43 typedef struct GameTime
 44 {
 45   float time;
 46   float deltaTime;
 47 } GameTime;
 48 
 49 typedef struct ButtonState {
 50   char isSelected;
 51   char isDisabled;
 52 } ButtonState;
 53 
 54 typedef struct GUIState {
 55   int isBlocked;
 56 } GUIState;
 57 
 58 typedef enum LevelState
 59 {
 60   LEVEL_STATE_NONE,
 61   LEVEL_STATE_BUILDING,
 62   LEVEL_STATE_BATTLE,
 63   LEVEL_STATE_WON_WAVE,
 64   LEVEL_STATE_LOST_WAVE,
 65   LEVEL_STATE_WON_LEVEL,
 66   LEVEL_STATE_RESET,
 67 } LevelState;
 68 
 69 typedef struct EnemyWave {
 70   uint8_t enemyType;
 71   uint8_t wave;
 72   uint16_t count;
 73   float interval;
 74   float delay;
 75   Vector2 spawnPosition;
 76 
 77   uint16_t spawned;
 78   float timeToSpawnNext;
 79 } EnemyWave;
 80 
 81 typedef struct Level
 82 {
 83   LevelState state;
 84   LevelState nextState;
 85   Camera3D camera;
 86   int placementMode;
 87 
 88   int initialGold;
 89   int playerGold;
 90 
 91   EnemyWave waves[10];
 92   int currentWave;
 93   float waveEndTimer;
 94 } Level;
 95 
 96 typedef struct DeltaSrc
 97 {
 98   char x, y;
 99 } DeltaSrc;
100 
101 typedef struct PathfindingMap
102 {
103   int width, height;
104   float scale;
105   float *distances;
106   long *towerIndex; 
107   DeltaSrc *deltaSrc;
108   float maxDistance;
109   Matrix toMapSpace;
110   Matrix toWorldSpace;
111 } PathfindingMap;
112 
113 // when we execute the pathfinding algorithm, we need to store the active nodes
114 // in a queue. Each node has a position, a distance from the start, and the
115 // position of the node that we came from.
116 typedef struct PathfindingNode
117 {
118   int16_t x, y, fromX, fromY;
119   float distance;
120 } PathfindingNode;
121 
122 typedef struct EnemyId
123 {
124   uint16_t index;
125   uint16_t generation;
126 } EnemyId;
127 
128 typedef struct EnemyClassConfig
129 {
130   float speed;
131   float health;
132   float radius;
133   float maxAcceleration;
134   float requiredContactTime;
135   float explosionDamage;
136   float explosionRange;
137   float explosionPushbackPower;
138   int goldValue;
139 } EnemyClassConfig;
140 
141 typedef struct Enemy
142 {
143   int16_t currentX, currentY;
144   int16_t nextX, nextY;
145   Vector2 simPosition;
146   Vector2 simVelocity;
147   uint16_t generation;
148   float startMovingTime;
149   float damage, futureDamage;
150   float contactTime;
151   uint8_t enemyType;
152   uint8_t movePathCount;
153   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
154 } Enemy;
155 
156 #define PROJECTILE_MAX_COUNT 1200
157 #define PROJECTILE_TYPE_NONE 0
158 #define PROJECTILE_TYPE_BULLET 1
159 
160 typedef struct Projectile
161 {
162   uint8_t projectileType;
163   float shootTime;
164   float arrivalTime;
165   float damage;
166   Vector2 position;
167   Vector2 target;
168   Vector2 directionNormal;
169   EnemyId targetEnemy;
170 } Projectile;
171 
172 //# Function declarations
173 float TowerGetMaxHealth(Tower *tower);
174 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
175 int EnemyAddDamage(Enemy *enemy, float damage);
176 
177 //# Enemy functions
178 void EnemyInit();
179 void EnemyDraw();
180 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
181 void EnemyUpdate();
182 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
183 float EnemyGetMaxHealth(Enemy *enemy);
184 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
185 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
186 EnemyId EnemyGetId(Enemy *enemy);
187 Enemy *EnemyTryResolve(EnemyId enemyId);
188 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
189 int EnemyAddDamage(Enemy *enemy, float damage);
190 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
191 int EnemyCount();
192 void EnemyDrawHealthbars(Camera3D camera);
193 
194 //# Tower functions
195 void TowerInit();
196 Tower *TowerGetAt(int16_t x, int16_t y);
197 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
198 Tower *GetTowerByType(uint8_t towerType);
199 int GetTowerCosts(uint8_t towerType);
200 float TowerGetMaxHealth(Tower *tower);
201 void TowerDraw();
202 void TowerUpdate();
203 void TowerDrawHealthBars(Camera3D camera);
204 205 //# Particles 206 void ParticleInit(); 207 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime); 208 void ParticleUpdate(); 209 void ParticleDraw(); 210 211 //# Projectiles 212 void ProjectileInit(); 213 void ProjectileDraw(); 214 void ProjectileUpdate(); 215 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage); 216 217 //# Pathfinding map 218 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 219 float PathFindingGetDistance(int mapX, int mapY); 220 Vector2 PathFindingGetGradient(Vector3 world); 221 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 222 void PathFindingMapUpdate();
223 void PathFindingMapDraw(); 224 225 //# UI 226 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor);
227 228 //# variables 229 extern Level *currentLevel; 230 extern Enemy enemies[ENEMY_MAX_COUNT]; 231 extern int enemyCount; 232 extern EnemyClassConfig enemyClassConfigs[]; 233 234 extern GUIState guiState; 235 extern GameTime gameTime; 236 extern Tower towers[TOWER_MAX_COUNT]; 237 extern int towerCount; 238 239 #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 void EnemyInit()
 24 {
 25   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 26   {
 27     enemies[i] = (Enemy){0};
 28   }
 29   enemyCount = 0;
 30 }
 31 
 32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 33 {
 34   return enemyClassConfigs[enemy->enemyType].speed;
 35 }
 36 
 37 float EnemyGetMaxHealth(Enemy *enemy)
 38 {
 39   return enemyClassConfigs[enemy->enemyType].health;
 40 }
 41 
 42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 43 {
 44   int16_t castleX = 0;
 45   int16_t castleY = 0;
 46   int16_t dx = castleX - currentX;
 47   int16_t dy = castleY - currentY;
 48   if (dx == 0 && dy == 0)
 49   {
 50     *nextX = currentX;
 51     *nextY = currentY;
 52     return 1;
 53   }
 54   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 55 
 56   if (gradient.x == 0 && gradient.y == 0)
 57   {
 58     *nextX = currentX;
 59     *nextY = currentY;
 60     return 1;
 61   }
 62 
 63   if (fabsf(gradient.x) > fabsf(gradient.y))
 64   {
 65     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 66     *nextY = currentY;
 67     return 0;
 68   }
 69   *nextX = currentX;
 70   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 71   return 0;
 72 }
 73 
 74 
 75 // this function predicts the movement of the unit for the next deltaT seconds
 76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 77 {
 78   const float pointReachedDistance = 0.25f;
 79   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 80   const float maxSimStepTime = 0.015625f;
 81   
 82   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 83   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 84   int16_t nextX = enemy->nextX;
 85   int16_t nextY = enemy->nextY;
 86   Vector2 position = enemy->simPosition;
 87   int passedCount = 0;
 88   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 89   {
 90     float stepTime = fminf(deltaT - t, maxSimStepTime);
 91     Vector2 target = (Vector2){nextX, nextY};
 92     float speed = Vector2Length(*velocity);
 93     // draw the target position for debugging
 94     DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
 95     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
 96     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
 97     {
 98       // we reached the target position, let's move to the next waypoint
 99       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100       target = (Vector2){nextX, nextY};
101       // track how many waypoints we passed
102       passedCount++;
103     }
104     
105     // acceleration towards the target
106     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108     *velocity = Vector2Add(*velocity, acceleration);
109 
110     // limit the speed to the maximum speed
111     if (speed > maxSpeed)
112     {
113       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114     }
115 
116     // move the enemy
117     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118   }
119 
120   if (waypointPassedCount)
121   {
122     (*waypointPassedCount) = passedCount;
123   }
124 
125   return position;
126 }
127 
128 void EnemyDraw()
129 {
130   for (int i = 0; i < enemyCount; i++)
131   {
132     Enemy enemy = enemies[i];
133     if (enemy.enemyType == ENEMY_TYPE_NONE)
134     {
135       continue;
136     }
137 
138     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139     
140     if (enemy.movePathCount > 0)
141     {
142       Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143       DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144     }
145     for (int j = 1; j < enemy.movePathCount; j++)
146     {
147       Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148       Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149       DrawLine3D(p, q, GREEN);
150     }
151 
152     switch (enemy.enemyType)
153     {
154     case ENEMY_TYPE_MINION:
155       DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156       break;
157     }
158   }
159 }
160 
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163   // damage the tower
164   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167   float explosionRange2 = explosionRange * explosionRange;
168   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169   // explode the enemy
170   if (tower->damage >= TowerGetMaxHealth(tower))
171   {
172     tower->towerType = TOWER_TYPE_NONE;
173   }
174 
175   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
176     explosionSource, 
177     (Vector3){0, 0.1f, 0}, 1.0f);
178 
179   enemy->enemyType = ENEMY_TYPE_NONE;
180 
181   // push back enemies & dealing damage
182   for (int i = 0; i < enemyCount; i++)
183   {
184     Enemy *other = &enemies[i];
185     if (other->enemyType == ENEMY_TYPE_NONE)
186     {
187       continue;
188     }
189     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190     if (distanceSqr > 0 && distanceSqr < explosionRange2)
191     {
192       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194       EnemyAddDamage(other, explosionDamge);
195     }
196   }
197 }
198 
199 void EnemyUpdate()
200 {
201   const float castleX = 0;
202   const float castleY = 0;
203   const float maxPathDistance2 = 0.25f * 0.25f;
204   
205   for (int i = 0; i < enemyCount; i++)
206   {
207     Enemy *enemy = &enemies[i];
208     if (enemy->enemyType == ENEMY_TYPE_NONE)
209     {
210       continue;
211     }
212 
213     int waypointPassedCount = 0;
214     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215     enemy->startMovingTime = gameTime.time;
216     // track path of unit
217     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218     {
219       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220       {
221         enemy->movePath[j] = enemy->movePath[j - 1];
222       }
223       enemy->movePath[0] = enemy->simPosition;
224       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225       {
226         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227       }
228     }
229 
230     if (waypointPassedCount > 0)
231     {
232       enemy->currentX = enemy->nextX;
233       enemy->currentY = enemy->nextY;
234       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236       {
237         // enemy reached the castle; remove it
238         enemy->enemyType = ENEMY_TYPE_NONE;
239         continue;
240       }
241     }
242   }
243 
244   // handle collisions between enemies
245   for (int i = 0; i < enemyCount - 1; i++)
246   {
247     Enemy *enemyA = &enemies[i];
248     if (enemyA->enemyType == ENEMY_TYPE_NONE)
249     {
250       continue;
251     }
252     for (int j = i + 1; j < enemyCount; j++)
253     {
254       Enemy *enemyB = &enemies[j];
255       if (enemyB->enemyType == ENEMY_TYPE_NONE)
256       {
257         continue;
258       }
259       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262       float radiusSum = radiusA + radiusB;
263       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264       {
265         // collision
266         float distance = sqrtf(distanceSqr);
267         float overlap = radiusSum - distance;
268         // move the enemies apart, but softly; if we have a clog of enemies,
269         // moving them perfectly apart can cause them to jitter
270         float positionCorrection = overlap / 5.0f;
271         Vector2 direction = (Vector2){
272             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276       }
277     }
278   }
279 
280   // handle collisions between enemies and towers
281   for (int i = 0; i < enemyCount; i++)
282   {
283     Enemy *enemy = &enemies[i];
284     if (enemy->enemyType == ENEMY_TYPE_NONE)
285     {
286       continue;
287     }
288     enemy->contactTime -= gameTime.deltaTime;
289     if (enemy->contactTime < 0.0f)
290     {
291       enemy->contactTime = 0.0f;
292     }
293 
294     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295     // linear search over towers; could be optimized by using path finding tower map,
296     // but for now, we keep it simple
297     for (int j = 0; j < towerCount; j++)
298     {
299       Tower *tower = &towers[j];
300       if (tower->towerType == TOWER_TYPE_NONE)
301       {
302         continue;
303       }
304       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306       if (distanceSqr > combinedRadius * combinedRadius)
307       {
308         continue;
309       }
310       // potential collision; square / circle intersection
311       float dx = tower->x - enemy->simPosition.x;
312       float dy = tower->y - enemy->simPosition.y;
313       float absDx = fabsf(dx);
314       float absDy = fabsf(dy);
315       Vector3 contactPoint = {0};
316       if (absDx <= 0.5f && absDx <= absDy) {
317         // vertical collision; push the enemy out horizontally
318         float overlap = enemyRadius + 0.5f - absDy;
319         if (overlap < 0.0f)
320         {
321           continue;
322         }
323         float direction = dy > 0.0f ? -1.0f : 1.0f;
324         enemy->simPosition.y += direction * overlap;
325 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
326 } 327 else if (absDy <= 0.5f && absDy <= absDx) 328 { 329 // horizontal collision; push the enemy out vertically 330 float overlap = enemyRadius + 0.5f - absDx; 331 if (overlap < 0.0f) 332 { 333 continue; 334 } 335 float direction = dx > 0.0f ? -1.0f : 1.0f; 336 enemy->simPosition.x += direction * overlap; 337 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y}; 338 } 339 else 340 { 341 // possible collision with a corner 342 float cornerDX = dx > 0.0f ? -0.5f : 0.5f; 343 float cornerDY = dy > 0.0f ? -0.5f : 0.5f; 344 float cornerX = tower->x + cornerDX; 345 float cornerY = tower->y + cornerDY; 346 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY}); 347 if (cornerDistanceSqr > enemyRadius * enemyRadius) 348 { 349 continue; 350 } 351 // push the enemy out along the diagonal 352 float cornerDistance = sqrtf(cornerDistanceSqr); 353 float overlap = enemyRadius - cornerDistance; 354 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX; 355 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY; 356 enemy->simPosition.x -= directionX * overlap; 357 enemy->simPosition.y -= directionY * overlap; 358 contactPoint = (Vector3){cornerX, 0.2f, cornerY}; 359 } 360 361 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f) 362 { 363 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above 364 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime) 365 { 366 EnemyTriggerExplode(enemy, tower, contactPoint); 367 } 368 } 369 } 370 } 371 } 372 373 EnemyId EnemyGetId(Enemy *enemy) 374 { 375 return (EnemyId){enemy - enemies, enemy->generation}; 376 } 377 378 Enemy *EnemyTryResolve(EnemyId enemyId) 379 { 380 if (enemyId.index >= ENEMY_MAX_COUNT) 381 { 382 return 0; 383 } 384 Enemy *enemy = &enemies[enemyId.index]; 385 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE) 386 { 387 return 0; 388 } 389 return enemy; 390 } 391 392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 393 { 394 Enemy *spawn = 0; 395 for (int i = 0; i < enemyCount; i++) 396 { 397 Enemy *enemy = &enemies[i]; 398 if (enemy->enemyType == ENEMY_TYPE_NONE) 399 { 400 spawn = enemy; 401 break; 402 } 403 } 404 405 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 406 { 407 spawn = &enemies[enemyCount++]; 408 } 409 410 if (spawn) 411 { 412 spawn->currentX = currentX; 413 spawn->currentY = currentY; 414 spawn->nextX = currentX; 415 spawn->nextY = currentY; 416 spawn->simPosition = (Vector2){currentX, currentY}; 417 spawn->simVelocity = (Vector2){0, 0}; 418 spawn->enemyType = enemyType; 419 spawn->startMovingTime = gameTime.time; 420 spawn->damage = 0.0f; 421 spawn->futureDamage = 0.0f; 422 spawn->generation++; 423 spawn->movePathCount = 0; 424 } 425 426 return spawn; 427 } 428 429 int EnemyAddDamage(Enemy *enemy, float damage) 430 { 431 enemy->damage += damage; 432 if (enemy->damage >= EnemyGetMaxHealth(enemy)) 433 { 434 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue; 435 enemy->enemyType = ENEMY_TYPE_NONE; 436 return 1; 437 } 438 439 return 0; 440 } 441 442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 443 { 444 int16_t castleX = 0; 445 int16_t castleY = 0; 446 Enemy* closest = 0; 447 int16_t closestDistance = 0; 448 float range2 = range * range; 449 for (int i = 0; i < enemyCount; i++) 450 { 451 Enemy* enemy = &enemies[i]; 452 if (enemy->enemyType == ENEMY_TYPE_NONE) 453 { 454 continue; 455 } 456 float maxHealth = EnemyGetMaxHealth(enemy); 457 if (enemy->futureDamage >= maxHealth) 458 { 459 // ignore enemies that will die soon 460 continue; 461 } 462 int16_t dx = castleX - enemy->currentX; 463 int16_t dy = castleY - enemy->currentY; 464 int16_t distance = abs(dx) + abs(dy); 465 if (!closest || distance < closestDistance) 466 { 467 float tdx = towerX - enemy->currentX; 468 float tdy = towerY - enemy->currentY; 469 float tdistance2 = tdx * tdx + tdy * tdy; 470 if (tdistance2 <= range2) 471 { 472 closest = enemy; 473 closestDistance = distance; 474 } 475 } 476 } 477 return closest; 478 } 479 480 int EnemyCount() 481 { 482 int count = 0; 483 for (int i = 0; i < enemyCount; i++) 484 { 485 if (enemies[i].enemyType != ENEMY_TYPE_NONE) 486 { 487 count++;
488 } 489 } 490 return count; 491 } 492 493 void EnemyDrawHealthbars(Camera3D camera) 494 { 495 for (int i = 0; i < enemyCount; i++) 496 { 497 Enemy *enemy = &enemies[i]; 498 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f) 499 { 500 continue;
501 } 502 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y}; 503 float maxHealth = EnemyGetMaxHealth(enemy); 504 float health = maxHealth - enemy->damage; 505 float healthRatio = health / maxHealth; 506 507 DrawHealthBar(camera, position, healthRatio, GREEN); 508 } 509 }
  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     Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
 30     float x = position.x;
 31     float y = position.y;
 32     float dx = projectile.directionNormal.x;
 33     float dy = projectile.directionNormal.y;
 34     for (float d = 1.0f; d > 0.0f; d -= 0.25f)
 35     {
 36       x -= dx * 0.1f;
 37       y -= dy * 0.1f;
 38       float size = 0.1f * d;
 39       DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
 40     }
 41   }
 42 }
 43 
 44 void ProjectileUpdate()
 45 {
 46   for (int i = 0; i < projectileCount; i++)
 47   {
 48     Projectile *projectile = &projectiles[i];
 49     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 50     {
 51       continue;
 52     }
 53     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 54     if (transition >= 1.0f)
 55     {
 56       projectile->projectileType = PROJECTILE_TYPE_NONE;
 57       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 58       if (enemy)
 59       {
 60         EnemyAddDamage(enemy, projectile->damage);
 61       }
 62       continue;
 63     }
 64   }
 65 }
 66 
 67 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
 68 {
 69   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 70   {
 71     Projectile *projectile = &projectiles[i];
 72     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 73     {
 74       projectile->projectileType = projectileType;
 75       projectile->shootTime = gameTime.time;
 76       projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
 77       projectile->damage = damage;
 78       projectile->position = position;
 79       projectile->target = target;
 80       projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
 81       projectile->targetEnemy = EnemyGetId(enemy);
 82       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 83       return projectile;
 84     }
 85   }
 86   return 0;
 87 }
  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 void TowerInit()
  8 {
  9   for (int i = 0; i < TOWER_MAX_COUNT; i++)
 10   {
 11     towers[i] = (Tower){0};
 12   }
 13   towerCount = 0;
 14 }
 15 
 16 static void TowerGunUpdate(Tower *tower)
 17 {
 18   if (tower->cooldown <= 0)
 19   {
 20     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
 21     if (enemy)
 22     {
 23       tower->cooldown = 0.5f;
 24       // shoot the enemy; determine future position of the enemy
 25       float bulletSpeed = 1.0f;
 26       float bulletDamage = 3.0f;
 27       Vector2 velocity = enemy->simVelocity;
 28       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
 29       Vector2 towerPosition = {tower->x, tower->y};
 30       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
 31       for (int i = 0; i < 8; i++) {
 32         velocity = enemy->simVelocity;
 33         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
 34         float distance = Vector2Distance(towerPosition, futurePosition);
 35         float eta2 = distance / bulletSpeed;
 36         if (fabs(eta - eta2) < 0.01f) {
 37           break;
 38         }
 39         eta = (eta2 + eta) * 0.5f;
 40       }
 41       ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition, 
 42         bulletSpeed, bulletDamage);
 43       enemy->futureDamage += bulletDamage;
 44     }
 45   }
 46   else
 47   {
 48     tower->cooldown -= gameTime.deltaTime;
 49   }
 50 }
 51 
 52 Tower *TowerGetAt(int16_t x, int16_t y)
 53 {
 54   for (int i = 0; i < towerCount; i++)
 55   {
 56     if (towers[i].x == x && towers[i].y == y)
 57     {
 58       return &towers[i];
 59     }
 60   }
 61   return 0;
 62 }
 63 
 64 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
 65 {
 66   if (towerCount >= TOWER_MAX_COUNT)
 67   {
 68     return 0;
 69   }
 70 
 71   Tower *tower = TowerGetAt(x, y);
 72   if (tower)
 73   {
 74     return 0;
 75   }
 76 
 77   tower = &towers[towerCount++];
 78   tower->x = x;
 79   tower->y = y;
 80   tower->towerType = towerType;
 81   tower->cooldown = 0.0f;
 82   tower->damage = 0.0f;
 83   return tower;
 84 }
 85 
 86 Tower *GetTowerByType(uint8_t towerType)
 87 {
 88   for (int i = 0; i < towerCount; i++)
 89   {
 90     if (towers[i].towerType == towerType)
 91     {
 92       return &towers[i];
 93     }
 94   }
 95   return 0;
 96 }
 97 
 98 int GetTowerCosts(uint8_t towerType)
 99 {
100   switch (towerType)
101   {
102   case TOWER_TYPE_BASE:
103     return 0;
104   case TOWER_TYPE_GUN:
105     return 6;
106   case TOWER_TYPE_WALL:
107     return 2;
108   }
109   return 0;
110 }
111 
112 float TowerGetMaxHealth(Tower *tower)
113 {
114   switch (tower->towerType)
115   {
116   case TOWER_TYPE_BASE:
117     return 10.0f;
118   case TOWER_TYPE_GUN:
119     return 3.0f;
120   case TOWER_TYPE_WALL:
121     return 5.0f;
122   }
123   return 0.0f;
124 }
125 
126 void TowerDraw()
127 {
128   for (int i = 0; i < towerCount; i++)
129   {
130     Tower tower = towers[i];
131     DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
132     switch (tower.towerType)
133     {
134     case TOWER_TYPE_BASE:
135       DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
136       break;
137     case TOWER_TYPE_GUN:
138       DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
139       break;
140     case TOWER_TYPE_WALL:
141       DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
142       break;
143     }
144   }
145 }
146 
147 void TowerUpdate()
148 {
149   for (int i = 0; i < towerCount; i++)
150   {
151     Tower *tower = &towers[i];
152     switch (tower->towerType)
153     {
154     case TOWER_TYPE_GUN:
155       TowerGunUpdate(tower);
156       break;
157     }
158   }
159 }
160 
161 void TowerDrawHealthBars(Camera3D camera) 162 { 163 for (int i = 0; i < towerCount; i++) 164 { 165 Tower *tower = &towers[i]; 166 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 167 { 168 continue; 169 } 170 171 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 172 float maxHealth = TowerGetMaxHealth(tower); 173 float health = maxHealth - tower->damage; 174 float healthRatio = health / maxHealth; 175 176 DrawHealthBar(camera, position, healthRatio, GREEN); 177 } 178 }
  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
This test revealed a bug where the explosions of enemies appeared in the wrong place. Turns out the contact point calculation was wrong. It was introduced on the post of the 6th December. The bugfix can be found in the enemy.c file, line 325. These kind of errors happen quite easily (at least to me) - identifying and fixing them is one of the most prominent aspects when working on software.

Here's how it looks like when you put it up to a test:

Healthbars of damaged buildings being shown above them.

This wasn't difficult at all! Seeing how damaged your buildings are provides a lot of insights, especially since it shows that the damage remains even after the wave is over:

A damaged base after the wave is over.

... which leads up to a question: Can the player repair buildings? How and when? I am not a game designer and I know that an important aspect of this profession is to think about these kind of questions before implementing the game. But one can't think of everything in advance and very often you only realize something when actually seeing it in action. I find this part of game development to be one of the most interesting ones: Once you start to see your game in action, you get a lot of ideas on how to improve it. You "just" have to start this journey. It's really the most important thing you can do: Start doing it!

Graphics

Speaking of seeing stuff in action: We need to replace the enemy and tower cubes with some graphics for the purpose of being able to distinguish between different enemy types and towers. Anything simple will do for now, so let's just use some billboard sprites for the enemies and simple 3d meshes for the towers.

In this tutorial, I won't go into detail how to create 3D models or sprites and will focus on how to integrate them into the game. You can use the graphics I provide (they are contained in the ZIP files below), search for assets on the internet or create your own.

What type of graphic style and quality you choose is up to you. You can get an asset pack from sites like Kenney.nl or OpenGameArt or you can create your own. For prototype game development, simpler graphics are usually better, as they are faster to create and don't distract from gameplay evaluation. A consistent style and sensible use of colors is still important, though.

Chosing a premade colorpalette can help a great deal to use colors that fit together. The Lospec Palette List is a great resource for this. My favorite 32 color palette is the Dawnbringer 32 palette.

I love graphical details, so I usually spend more time on this than I should, but for me it is also important to have fun while developing (remember, free time project, not work!). I am not a professional artist, but I have come up with some ways to create simple graphics that still have a certain charm and that I can create in quite a short time.

Here's an overview of the 3d models that I will use:

3D models for the game.
Wireframe of the 3D models.

As you can see, the models are quite simple and look fairly low poly, though the tesselation is still high due to not using textures but only a gradient texture, which looks like this:

128x128 Gradient texture for the 3D models.

While this technique may look a little strange at first, it has a few advantages:

To illustrate the last point, here's in real time how I change the color of the tree model:

Color variations of the tree model.

And if you're curious how the modelling looks like, here's a video on how I model a simple low poly rock in Blender (also real time):

Apparently, this isn't the first time I've done this, so this one went quite fast. It takes more time when creating something new, but in general, all objects I created for this tutorial were done within a few hours.

As for the sprites, I am using a texture atlas with sprites for the enemies and I use the same color palette as for the 3D models (albeit limited to the 32 colors, the color palette is btw. the Dawnbringer 32 palette).

Here's a part of the texture atlas that I worked out in Aseprite:

Spritesheet for the units.

The sprites are each around 16x16, so fairly small. That makes it easier and fast to create.

All the models and sprites are available in the ZIP files that you can download from the respective parts of this tutorial. Feel free to use them for your own projects: I release them under the CC0 license, so you can use them as you like.

Replacing the cubes

Let's start with placing actual walls. We load the 3D and draw it in place of the cube we used before:

  • 💾
  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 Level levels[] = {
 11   [0] = {
 12     .state = LEVEL_STATE_BUILDING,
 13     .initialGold = 20,
 14     .waves[0] = {
 15       .enemyType = ENEMY_TYPE_MINION,
 16       .wave = 0,
 17       .count = 10,
 18       .interval = 2.5f,
 19       .delay = 1.0f,
 20       .spawnPosition = {0, 6},
 21     },
 22     .waves[1] = {
 23       .enemyType = ENEMY_TYPE_MINION,
 24       .wave = 1,
 25       .count = 20,
 26       .interval = 1.5f,
 27       .delay = 1.0f,
 28       .spawnPosition = {0, 6},
 29     },
 30     .waves[2] = {
 31       .enemyType = ENEMY_TYPE_MINION,
 32       .wave = 2,
 33       .count = 30,
 34       .interval = 1.2f,
 35       .delay = 1.0f,
 36       .spawnPosition = {0, 6},
 37     }
 38   },
 39 };
 40 
 41 Level *currentLevel = levels;
 42 
 43 //# Game
 44 
 45 void InitLevel(Level *level)
 46 {
 47   TowerInit();
 48   EnemyInit();
 49   ProjectileInit();
 50   ParticleInit();
 51   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
 52 
 53   level->placementMode = 0;
 54   level->state = LEVEL_STATE_BUILDING;
 55   level->nextState = LEVEL_STATE_NONE;
 56   level->playerGold = level->initialGold;
 57 
 58   Camera *camera = &level->camera;
 59   camera->position = (Vector3){1.0f, 12.0f, 6.5f};
 60   camera->target = (Vector3){0.0f, 0.5f, 1.0f};
 61   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
 62   camera->fovy = 45.0f;
 63   camera->projection = CAMERA_PERSPECTIVE;
 64 }
 65 
 66 void DrawLevelHud(Level *level)
 67 {
 68   const char *text = TextFormat("Gold: %d", level->playerGold);
 69   Font font = GetFontDefault();
 70   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
 71   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
 72 }
 73 
 74 void DrawLevelReportLostWave(Level *level)
 75 {
 76   BeginMode3D(level->camera);
 77   DrawGrid(10, 1.0f);
 78   TowerDraw();
 79   EnemyDraw();
 80   ProjectileDraw();
 81   ParticleDraw();
 82   guiState.isBlocked = 0;
 83   EndMode3D();
 84 
 85   TowerDrawHealthBars(level->camera);
 86 
 87   const char *text = "Wave lost";
 88   int textWidth = MeasureText(text, 20);
 89   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
 90 
 91   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
 92   {
 93     level->nextState = LEVEL_STATE_RESET;
 94   }
 95 }
 96 
 97 int HasLevelNextWave(Level *level)
 98 {
 99   for (int i = 0; i < 10; i++)
100   {
101     EnemyWave *wave = &level->waves[i];
102     if (wave->wave == level->currentWave)
103     {
104       return 1;
105     }
106   }
107   return 0;
108 }
109 
110 void DrawLevelReportWonWave(Level *level)
111 {
112   BeginMode3D(level->camera);
113   DrawGrid(10, 1.0f);
114   TowerDraw();
115   EnemyDraw();
116   ProjectileDraw();
117   ParticleDraw();
118   guiState.isBlocked = 0;
119   EndMode3D();
120 
121   TowerDrawHealthBars(level->camera);
122 
123   const char *text = "Wave won";
124   int textWidth = MeasureText(text, 20);
125   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
126 
127 
128   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
129   {
130     level->nextState = LEVEL_STATE_RESET;
131   }
132 
133   if (HasLevelNextWave(level))
134   {
135     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
136     {
137       level->nextState = LEVEL_STATE_BUILDING;
138     }
139   }
140   else {
141     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
142     {
143       level->nextState = LEVEL_STATE_WON_LEVEL;
144     }
145   }
146 }
147 
148 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
149 {
150   static ButtonState buttonStates[8] = {0};
151   int cost = GetTowerCosts(towerType);
152   const char *text = TextFormat("%s: %d", name, cost);
153   buttonStates[towerType].isSelected = level->placementMode == towerType;
154   buttonStates[towerType].isDisabled = level->playerGold < cost;
155   if (Button(text, x, y, width, height, &buttonStates[towerType]))
156   {
157     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
158   }
159 }
160 
161 void DrawLevelBuildingState(Level *level)
162 {
163   BeginMode3D(level->camera);
164   DrawGrid(10, 1.0f);
165   TowerDraw();
166   EnemyDraw();
167   ProjectileDraw();
168   ParticleDraw();
169 
170   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
171   float planeDistance = ray.position.y / -ray.direction.y;
172   float planeX = ray.direction.x * planeDistance + ray.position.x;
173   float planeY = ray.direction.z * planeDistance + ray.position.z;
174   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
175   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
176   if (level->placementMode && !guiState.isBlocked)
177   {
178     DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
179     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
180     {
181       if (TowerTryAdd(level->placementMode, mapX, mapY))
182       {
183         level->playerGold -= GetTowerCosts(level->placementMode);
184         level->placementMode = TOWER_TYPE_NONE;
185       }
186     }
187   }
188 
189   guiState.isBlocked = 0;
190 
191   EndMode3D();
192 
193   TowerDrawHealthBars(level->camera);
194 
195   static ButtonState buildWallButtonState = {0};
196   static ButtonState buildGunButtonState = {0};
197   buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
198   buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
199 
200   DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
201   DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
202 
203   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
204   {
205     level->nextState = LEVEL_STATE_RESET;
206   }
207   
208   if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
209   {
210     level->nextState = LEVEL_STATE_BATTLE;
211   }
212 
213   const char *text = "Building phase";
214   int textWidth = MeasureText(text, 20);
215   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
216 }
217 
218 void InitBattleStateConditions(Level *level)
219 {
220   level->state = LEVEL_STATE_BATTLE;
221   level->nextState = LEVEL_STATE_NONE;
222   level->waveEndTimer = 0.0f;
223   for (int i = 0; i < 10; i++)
224   {
225     EnemyWave *wave = &level->waves[i];
226     wave->spawned = 0;
227     wave->timeToSpawnNext = wave->delay;
228   }
229 }
230 
231 void DrawLevelBattleState(Level *level)
232 {
233   BeginMode3D(level->camera);
234   DrawGrid(10, 1.0f);
235   TowerDraw();
236   EnemyDraw();
237   ProjectileDraw();
238   ParticleDraw();
239   guiState.isBlocked = 0;
240   EndMode3D();
241 
242   EnemyDrawHealthbars(level->camera);
243   TowerDrawHealthBars(level->camera);
244 
245   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
246   {
247     level->nextState = LEVEL_STATE_RESET;
248   }
249 
250   int maxCount = 0;
251   int remainingCount = 0;
252   for (int i = 0; i < 10; i++)
253   {
254     EnemyWave *wave = &level->waves[i];
255     if (wave->wave != level->currentWave)
256     {
257       continue;
258     }
259     maxCount += wave->count;
260     remainingCount += wave->count - wave->spawned;
261   }
262   int aliveCount = EnemyCount();
263   remainingCount += aliveCount;
264 
265   const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
266   int textWidth = MeasureText(text, 20);
267   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
268 }
269 
270 void DrawLevel(Level *level)
271 {
272   switch (level->state)
273   {
274     case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
275     case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
276     case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
277     case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
278     default: break;
279   }
280 
281   DrawLevelHud(level);
282 }
283 
284 void UpdateLevel(Level *level)
285 {
286   if (level->state == LEVEL_STATE_BATTLE)
287   {
288     int activeWaves = 0;
289     for (int i = 0; i < 10; i++)
290     {
291       EnemyWave *wave = &level->waves[i];
292       if (wave->spawned >= wave->count || wave->wave != level->currentWave)
293       {
294         continue;
295       }
296       activeWaves++;
297       wave->timeToSpawnNext -= gameTime.deltaTime;
298       if (wave->timeToSpawnNext <= 0.0f)
299       {
300         Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
301         if (enemy)
302         {
303           wave->timeToSpawnNext = wave->interval;
304           wave->spawned++;
305         }
306       }
307     }
308     if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
309       level->waveEndTimer += gameTime.deltaTime;
310       if (level->waveEndTimer >= 2.0f)
311       {
312         level->nextState = LEVEL_STATE_LOST_WAVE;
313       }
314     }
315     else if (activeWaves == 0 && EnemyCount() == 0)
316     {
317       level->waveEndTimer += gameTime.deltaTime;
318       if (level->waveEndTimer >= 2.0f)
319       {
320         level->nextState = LEVEL_STATE_WON_WAVE;
321       }
322     }
323   }
324 
325   PathFindingMapUpdate();
326   EnemyUpdate();
327   TowerUpdate();
328   ProjectileUpdate();
329   ParticleUpdate();
330 
331   if (level->nextState == LEVEL_STATE_RESET)
332   {
333     InitLevel(level);
334   }
335   
336   if (level->nextState == LEVEL_STATE_BATTLE)
337   {
338     InitBattleStateConditions(level);
339   }
340   
341   if (level->nextState == LEVEL_STATE_WON_WAVE)
342   {
343     level->currentWave++;
344     level->state = LEVEL_STATE_WON_WAVE;
345   }
346   
347   if (level->nextState == LEVEL_STATE_LOST_WAVE)
348   {
349     level->state = LEVEL_STATE_LOST_WAVE;
350   }
351 
352   if (level->nextState == LEVEL_STATE_BUILDING)
353   {
354     level->state = LEVEL_STATE_BUILDING;
355   }
356 
357   if (level->nextState == LEVEL_STATE_WON_LEVEL)
358   {
359     // make something of this later
360     InitLevel(level);
361   }
362 
363   level->nextState = LEVEL_STATE_NONE;
364 }
365 
366 float nextSpawnTime = 0.0f;
367 
368 void ResetGame()
369 {
370   InitLevel(currentLevel);
371 }
372 
373 void InitGame()
374 {
375   TowerInit();
376   EnemyInit();
377   ProjectileInit();
378   ParticleInit();
379   PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
380 
381   currentLevel = levels;
382   InitLevel(currentLevel);
383 }
384 
385 //# Immediate GUI functions
386 
387 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor)
388 {
389   const float healthBarWidth = 40.0f;
390   const float healthBarHeight = 6.0f;
391   const float healthBarOffset = 15.0f;
392   const float inset = 2.0f;
393   const float innerWidth = healthBarWidth - inset * 2;
394   const float innerHeight = healthBarHeight - inset * 2;
395 
396   Vector2 screenPos = GetWorldToScreen(position, camera);
397   float centerX = screenPos.x - healthBarWidth * 0.5f;
398   float topY = screenPos.y - healthBarOffset;
399   DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
400   float healthWidth = innerWidth * healthRatio;
401   DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
402 }
403 
404 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
405 {
406   Rectangle bounds = {x, y, width, height};
407   int isPressed = 0;
408   int isSelected = state && state->isSelected;
409   if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
410   {
411     Color color = isSelected ? DARKGRAY : GRAY;
412     DrawRectangle(x, y, width, height, color);
413     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
414     {
415       isPressed = 1;
416     }
417     guiState.isBlocked = 1;
418   }
419   else
420   {
421     Color color = isSelected ? WHITE : LIGHTGRAY;
422     DrawRectangle(x, y, width, height, color);
423   }
424   Font font = GetFontDefault();
425   Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
426   Color textColor = state->isDisabled ? GRAY : BLACK;
427   DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
428   return isPressed;
429 }
430 
431 //# Main game loop
432 
433 void GameUpdate()
434 {
435   float dt = GetFrameTime();
436   // cap maximum delta time to 0.1 seconds to prevent large time steps
437   if (dt > 0.1f) dt = 0.1f;
438   gameTime.time += dt;
439   gameTime.deltaTime = dt;
440 
441   UpdateLevel(currentLevel);
442 }
443 
444 int main(void)
445 {
446   int screenWidth, screenHeight;
447   GetPreferredSize(&screenWidth, &screenHeight);
448   InitWindow(screenWidth, screenHeight, "Tower defense");
449   SetTargetFPS(30);
450 
451   InitGame();
452 
453   while (!WindowShouldClose())
454   {
455     if (IsPaused()) {
456       // canvas is not visible in browser - do nothing
457       continue;
458     }
459 
460     BeginDrawing();
461     ClearBackground(DARKBLUE);
462 
463     GameUpdate();
464     DrawLevel(currentLevel);
465 
466     EndDrawing();
467   }
468 
469   CloseWindow();
470 
471   return 0;
472 }
  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 float cooldown; 41 float damage; 42 } Tower; 43 44 typedef struct GameTime 45 { 46 float time; 47 float deltaTime; 48 } GameTime; 49 50 typedef struct ButtonState { 51 char isSelected; 52 char isDisabled; 53 } ButtonState; 54 55 typedef struct GUIState { 56 int isBlocked; 57 } GUIState; 58 59 typedef enum LevelState 60 { 61 LEVEL_STATE_NONE, 62 LEVEL_STATE_BUILDING, 63 LEVEL_STATE_BATTLE, 64 LEVEL_STATE_WON_WAVE, 65 LEVEL_STATE_LOST_WAVE, 66 LEVEL_STATE_WON_LEVEL, 67 LEVEL_STATE_RESET, 68 } LevelState; 69 70 typedef struct EnemyWave { 71 uint8_t enemyType; 72 uint8_t wave; 73 uint16_t count; 74 float interval; 75 float delay; 76 Vector2 spawnPosition; 77 78 uint16_t spawned; 79 float timeToSpawnNext; 80 } EnemyWave; 81 82 typedef struct Level 83 { 84 LevelState state; 85 LevelState nextState; 86 Camera3D camera; 87 int placementMode; 88 89 int initialGold; 90 int playerGold; 91 92 EnemyWave waves[10]; 93 int currentWave; 94 float waveEndTimer; 95 } Level; 96 97 typedef struct DeltaSrc 98 { 99 char x, y; 100 } DeltaSrc; 101 102 typedef struct PathfindingMap 103 { 104 int width, height; 105 float scale; 106 float *distances; 107 long *towerIndex; 108 DeltaSrc *deltaSrc; 109 float maxDistance; 110 Matrix toMapSpace; 111 Matrix toWorldSpace; 112 } PathfindingMap; 113 114 // when we execute the pathfinding algorithm, we need to store the active nodes 115 // in a queue. Each node has a position, a distance from the start, and the 116 // position of the node that we came from. 117 typedef struct PathfindingNode 118 { 119 int16_t x, y, fromX, fromY; 120 float distance; 121 } PathfindingNode; 122 123 typedef struct EnemyId 124 { 125 uint16_t index; 126 uint16_t generation; 127 } EnemyId; 128 129 typedef struct EnemyClassConfig 130 { 131 float speed; 132 float health; 133 float radius; 134 float maxAcceleration; 135 float requiredContactTime; 136 float explosionDamage; 137 float explosionRange; 138 float explosionPushbackPower; 139 int goldValue; 140 } EnemyClassConfig; 141 142 typedef struct Enemy 143 { 144 int16_t currentX, currentY; 145 int16_t nextX, nextY; 146 Vector2 simPosition; 147 Vector2 simVelocity; 148 uint16_t generation; 149 float startMovingTime; 150 float damage, futureDamage; 151 float contactTime; 152 uint8_t enemyType; 153 uint8_t movePathCount; 154 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 155 } Enemy; 156 157 #define PROJECTILE_MAX_COUNT 1200 158 #define PROJECTILE_TYPE_NONE 0 159 #define PROJECTILE_TYPE_BULLET 1 160 161 typedef struct Projectile 162 { 163 uint8_t projectileType; 164 float shootTime; 165 float arrivalTime; 166 float damage; 167 Vector2 position; 168 Vector2 target; 169 Vector2 directionNormal; 170 EnemyId targetEnemy; 171 } Projectile; 172 173 //# Function declarations 174 float TowerGetMaxHealth(Tower *tower); 175 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 176 int EnemyAddDamage(Enemy *enemy, float damage); 177 178 //# Enemy functions 179 void EnemyInit(); 180 void EnemyDraw(); 181 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 182 void EnemyUpdate(); 183 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 184 float EnemyGetMaxHealth(Enemy *enemy); 185 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 186 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 187 EnemyId EnemyGetId(Enemy *enemy); 188 Enemy *EnemyTryResolve(EnemyId enemyId); 189 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 190 int EnemyAddDamage(Enemy *enemy, float damage); 191 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 192 int EnemyCount(); 193 void EnemyDrawHealthbars(Camera3D camera); 194 195 //# Tower functions 196 void TowerInit(); 197 Tower *TowerGetAt(int16_t x, int16_t y); 198 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 199 Tower *GetTowerByType(uint8_t towerType); 200 int GetTowerCosts(uint8_t towerType); 201 float TowerGetMaxHealth(Tower *tower); 202 void TowerDraw(); 203 void TowerUpdate(); 204 void TowerDrawHealthBars(Camera3D camera); 205 206 //# Particles 207 void ParticleInit(); 208 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime); 209 void ParticleUpdate(); 210 void ParticleDraw(); 211 212 //# Projectiles 213 void ProjectileInit(); 214 void ProjectileDraw(); 215 void ProjectileUpdate(); 216 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage); 217 218 //# Pathfinding map 219 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 220 float PathFindingGetDistance(int mapX, int mapY); 221 Vector2 PathFindingGetGradient(Vector3 world); 222 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 223 void PathFindingMapUpdate(); 224 void PathFindingMapDraw(); 225 226 //# UI 227 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor); 228 229 //# variables 230 extern Level *currentLevel; 231 extern Enemy enemies[ENEMY_MAX_COUNT]; 232 extern int enemyCount; 233 extern EnemyClassConfig enemyClassConfigs[]; 234 235 extern GUIState guiState; 236 extern GameTime gameTime; 237 extern Tower towers[TOWER_MAX_COUNT]; 238 extern int towerCount; 239 240 #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 void EnemyInit()
 24 {
 25   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 26   {
 27     enemies[i] = (Enemy){0};
 28   }
 29   enemyCount = 0;
 30 }
 31 
 32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 33 {
 34   return enemyClassConfigs[enemy->enemyType].speed;
 35 }
 36 
 37 float EnemyGetMaxHealth(Enemy *enemy)
 38 {
 39   return enemyClassConfigs[enemy->enemyType].health;
 40 }
 41 
 42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 43 {
 44   int16_t castleX = 0;
 45   int16_t castleY = 0;
 46   int16_t dx = castleX - currentX;
 47   int16_t dy = castleY - currentY;
 48   if (dx == 0 && dy == 0)
 49   {
 50     *nextX = currentX;
 51     *nextY = currentY;
 52     return 1;
 53   }
 54   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 55 
 56   if (gradient.x == 0 && gradient.y == 0)
 57   {
 58     *nextX = currentX;
 59     *nextY = currentY;
 60     return 1;
 61   }
 62 
 63   if (fabsf(gradient.x) > fabsf(gradient.y))
 64   {
 65     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 66     *nextY = currentY;
 67     return 0;
 68   }
 69   *nextX = currentX;
 70   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 71   return 0;
 72 }
 73 
 74 
 75 // this function predicts the movement of the unit for the next deltaT seconds
 76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 77 {
 78   const float pointReachedDistance = 0.25f;
 79   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 80   const float maxSimStepTime = 0.015625f;
 81   
 82   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 83   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 84   int16_t nextX = enemy->nextX;
 85   int16_t nextY = enemy->nextY;
 86   Vector2 position = enemy->simPosition;
 87   int passedCount = 0;
 88   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 89   {
 90     float stepTime = fminf(deltaT - t, maxSimStepTime);
 91     Vector2 target = (Vector2){nextX, nextY};
 92     float speed = Vector2Length(*velocity);
 93     // draw the target position for debugging
 94     DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
 95     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
 96     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
 97     {
 98       // we reached the target position, let's move to the next waypoint
 99       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100       target = (Vector2){nextX, nextY};
101       // track how many waypoints we passed
102       passedCount++;
103     }
104     
105     // acceleration towards the target
106     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108     *velocity = Vector2Add(*velocity, acceleration);
109 
110     // limit the speed to the maximum speed
111     if (speed > maxSpeed)
112     {
113       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114     }
115 
116     // move the enemy
117     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118   }
119 
120   if (waypointPassedCount)
121   {
122     (*waypointPassedCount) = passedCount;
123   }
124 
125   return position;
126 }
127 
128 void EnemyDraw()
129 {
130   for (int i = 0; i < enemyCount; i++)
131   {
132     Enemy enemy = enemies[i];
133     if (enemy.enemyType == ENEMY_TYPE_NONE)
134     {
135       continue;
136     }
137 
138     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139     
140     if (enemy.movePathCount > 0)
141     {
142       Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143       DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144     }
145     for (int j = 1; j < enemy.movePathCount; j++)
146     {
147       Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148       Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149       DrawLine3D(p, q, GREEN);
150     }
151 
152     switch (enemy.enemyType)
153     {
154     case ENEMY_TYPE_MINION:
155       DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156       break;
157     }
158   }
159 }
160 
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163   // damage the tower
164   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167   float explosionRange2 = explosionRange * explosionRange;
168   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169   // explode the enemy
170   if (tower->damage >= TowerGetMaxHealth(tower))
171   {
172     tower->towerType = TOWER_TYPE_NONE;
173   }
174 
175   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
176     explosionSource, 
177     (Vector3){0, 0.1f, 0}, 1.0f);
178 
179   enemy->enemyType = ENEMY_TYPE_NONE;
180 
181   // push back enemies & dealing damage
182   for (int i = 0; i < enemyCount; i++)
183   {
184     Enemy *other = &enemies[i];
185     if (other->enemyType == ENEMY_TYPE_NONE)
186     {
187       continue;
188     }
189     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190     if (distanceSqr > 0 && distanceSqr < explosionRange2)
191     {
192       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194       EnemyAddDamage(other, explosionDamge);
195     }
196   }
197 }
198 
199 void EnemyUpdate()
200 {
201   const float castleX = 0;
202   const float castleY = 0;
203   const float maxPathDistance2 = 0.25f * 0.25f;
204   
205   for (int i = 0; i < enemyCount; i++)
206   {
207     Enemy *enemy = &enemies[i];
208     if (enemy->enemyType == ENEMY_TYPE_NONE)
209     {
210       continue;
211     }
212 
213     int waypointPassedCount = 0;
214     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215     enemy->startMovingTime = gameTime.time;
216     // track path of unit
217     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218     {
219       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220       {
221         enemy->movePath[j] = enemy->movePath[j - 1];
222       }
223       enemy->movePath[0] = enemy->simPosition;
224       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225       {
226         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227       }
228     }
229 
230     if (waypointPassedCount > 0)
231     {
232       enemy->currentX = enemy->nextX;
233       enemy->currentY = enemy->nextY;
234       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236       {
237         // enemy reached the castle; remove it
238         enemy->enemyType = ENEMY_TYPE_NONE;
239         continue;
240       }
241     }
242   }
243 
244   // handle collisions between enemies
245   for (int i = 0; i < enemyCount - 1; i++)
246   {
247     Enemy *enemyA = &enemies[i];
248     if (enemyA->enemyType == ENEMY_TYPE_NONE)
249     {
250       continue;
251     }
252     for (int j = i + 1; j < enemyCount; j++)
253     {
254       Enemy *enemyB = &enemies[j];
255       if (enemyB->enemyType == ENEMY_TYPE_NONE)
256       {
257         continue;
258       }
259       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262       float radiusSum = radiusA + radiusB;
263       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264       {
265         // collision
266         float distance = sqrtf(distanceSqr);
267         float overlap = radiusSum - distance;
268         // move the enemies apart, but softly; if we have a clog of enemies,
269         // moving them perfectly apart can cause them to jitter
270         float positionCorrection = overlap / 5.0f;
271         Vector2 direction = (Vector2){
272             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276       }
277     }
278   }
279 
280   // handle collisions between enemies and towers
281   for (int i = 0; i < enemyCount; i++)
282   {
283     Enemy *enemy = &enemies[i];
284     if (enemy->enemyType == ENEMY_TYPE_NONE)
285     {
286       continue;
287     }
288     enemy->contactTime -= gameTime.deltaTime;
289     if (enemy->contactTime < 0.0f)
290     {
291       enemy->contactTime = 0.0f;
292     }
293 
294     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295     // linear search over towers; could be optimized by using path finding tower map,
296     // but for now, we keep it simple
297     for (int j = 0; j < towerCount; j++)
298     {
299       Tower *tower = &towers[j];
300       if (tower->towerType == TOWER_TYPE_NONE)
301       {
302         continue;
303       }
304       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306       if (distanceSqr > combinedRadius * combinedRadius)
307       {
308         continue;
309       }
310       // potential collision; square / circle intersection
311       float dx = tower->x - enemy->simPosition.x;
312       float dy = tower->y - enemy->simPosition.y;
313       float absDx = fabsf(dx);
314       float absDy = fabsf(dy);
315       Vector3 contactPoint = {0};
316       if (absDx <= 0.5f && absDx <= absDy) {
317         // vertical collision; push the enemy out horizontally
318         float overlap = enemyRadius + 0.5f - absDy;
319         if (overlap < 0.0f)
320         {
321           continue;
322         }
323         float direction = dy > 0.0f ? -1.0f : 1.0f;
324         enemy->simPosition.y += direction * overlap;
325         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
326       }
327       else if (absDy <= 0.5f && absDy <= absDx)
328       {
329         // horizontal collision; push the enemy out vertically
330         float overlap = enemyRadius + 0.5f - absDx;
331         if (overlap < 0.0f)
332         {
333           continue;
334         }
335         float direction = dx > 0.0f ? -1.0f : 1.0f;
336         enemy->simPosition.x += direction * overlap;
337         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
338       }
339       else
340       {
341         // possible collision with a corner
342         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
343         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
344         float cornerX = tower->x + cornerDX;
345         float cornerY = tower->y + cornerDY;
346         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
347         if (cornerDistanceSqr > enemyRadius * enemyRadius)
348         {
349           continue;
350         }
351         // push the enemy out along the diagonal
352         float cornerDistance = sqrtf(cornerDistanceSqr);
353         float overlap = enemyRadius - cornerDistance;
354         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
355         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
356         enemy->simPosition.x -= directionX * overlap;
357         enemy->simPosition.y -= directionY * overlap;
358         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
359       }
360 
361       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
362       {
363         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
364         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
365         {
366           EnemyTriggerExplode(enemy, tower, contactPoint);
367         }
368       }
369     }
370   }
371 }
372 
373 EnemyId EnemyGetId(Enemy *enemy)
374 {
375   return (EnemyId){enemy - enemies, enemy->generation};
376 }
377 
378 Enemy *EnemyTryResolve(EnemyId enemyId)
379 {
380   if (enemyId.index >= ENEMY_MAX_COUNT)
381   {
382     return 0;
383   }
384   Enemy *enemy = &enemies[enemyId.index];
385   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
386   {
387     return 0;
388   }
389   return enemy;
390 }
391 
392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
393 {
394   Enemy *spawn = 0;
395   for (int i = 0; i < enemyCount; i++)
396   {
397     Enemy *enemy = &enemies[i];
398     if (enemy->enemyType == ENEMY_TYPE_NONE)
399     {
400       spawn = enemy;
401       break;
402     }
403   }
404 
405   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
406   {
407     spawn = &enemies[enemyCount++];
408   }
409 
410   if (spawn)
411   {
412     spawn->currentX = currentX;
413     spawn->currentY = currentY;
414     spawn->nextX = currentX;
415     spawn->nextY = currentY;
416     spawn->simPosition = (Vector2){currentX, currentY};
417     spawn->simVelocity = (Vector2){0, 0};
418     spawn->enemyType = enemyType;
419     spawn->startMovingTime = gameTime.time;
420     spawn->damage = 0.0f;
421     spawn->futureDamage = 0.0f;
422     spawn->generation++;
423     spawn->movePathCount = 0;
424   }
425 
426   return spawn;
427 }
428 
429 int EnemyAddDamage(Enemy *enemy, float damage)
430 {
431   enemy->damage += damage;
432   if (enemy->damage >= EnemyGetMaxHealth(enemy))
433   {
434     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
435     enemy->enemyType = ENEMY_TYPE_NONE;
436     return 1;
437   }
438 
439   return 0;
440 }
441 
442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
443 {
444   int16_t castleX = 0;
445   int16_t castleY = 0;
446   Enemy* closest = 0;
447   int16_t closestDistance = 0;
448   float range2 = range * range;
449   for (int i = 0; i < enemyCount; i++)
450   {
451     Enemy* enemy = &enemies[i];
452     if (enemy->enemyType == ENEMY_TYPE_NONE)
453     {
454       continue;
455     }
456     float maxHealth = EnemyGetMaxHealth(enemy);
457     if (enemy->futureDamage >= maxHealth)
458     {
459       // ignore enemies that will die soon
460       continue;
461     }
462     int16_t dx = castleX - enemy->currentX;
463     int16_t dy = castleY - enemy->currentY;
464     int16_t distance = abs(dx) + abs(dy);
465     if (!closest || distance < closestDistance)
466     {
467       float tdx = towerX - enemy->currentX;
468       float tdy = towerY - enemy->currentY;
469       float tdistance2 = tdx * tdx + tdy * tdy;
470       if (tdistance2 <= range2)
471       {
472         closest = enemy;
473         closestDistance = distance;
474       }
475     }
476   }
477   return closest;
478 }
479 
480 int EnemyCount()
481 {
482   int count = 0;
483   for (int i = 0; i < enemyCount; i++)
484   {
485     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
486     {
487       count++;
488     }
489   }
490   return count;
491 }
492 
493 void EnemyDrawHealthbars(Camera3D camera)
494 {
495   for (int i = 0; i < enemyCount; i++)
496   {
497     Enemy *enemy = &enemies[i];
498     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
499     {
500       continue;
501     }
502     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
503     float maxHealth = EnemyGetMaxHealth(enemy);
504     float health = maxHealth - enemy->damage;
505     float healthRatio = health / maxHealth;
506     
507     DrawHealthBar(camera, position, healthRatio, GREEN);
508   }
509 }
  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     Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
 30     float x = position.x;
 31     float y = position.y;
 32     float dx = projectile.directionNormal.x;
 33     float dy = projectile.directionNormal.y;
 34     for (float d = 1.0f; d > 0.0f; d -= 0.25f)
 35     {
 36       x -= dx * 0.1f;
 37       y -= dy * 0.1f;
 38       float size = 0.1f * d;
 39       DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
 40     }
 41   }
 42 }
 43 
 44 void ProjectileUpdate()
 45 {
 46   for (int i = 0; i < projectileCount; i++)
 47   {
 48     Projectile *projectile = &projectiles[i];
 49     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 50     {
 51       continue;
 52     }
 53     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 54     if (transition >= 1.0f)
 55     {
 56       projectile->projectileType = PROJECTILE_TYPE_NONE;
 57       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 58       if (enemy)
 59       {
 60         EnemyAddDamage(enemy, projectile->damage);
 61       }
 62       continue;
 63     }
 64   }
 65 }
 66 
 67 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
 68 {
 69   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 70   {
 71     Projectile *projectile = &projectiles[i];
 72     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 73     {
 74       projectile->projectileType = projectileType;
 75       projectile->shootTime = gameTime.time;
 76       projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
 77       projectile->damage = damage;
 78       projectile->position = position;
 79       projectile->target = target;
 80       projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
 81       projectile->targetEnemy = EnemyGetId(enemy);
 82       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 83       return projectile;
 84     }
 85   }
 86   return 0;
 87 }
  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 Texture2D palette; 9
10 void TowerInit() 11 { 12 for (int i = 0; i < TOWER_MAX_COUNT; i++) 13 { 14 towers[i] = (Tower){0}; 15 }
16 towerCount = 0; 17 18 // we'll use a palette texture to colorize the all buildings and environment art 19 palette = LoadTexture("data/palette.png"); 20 // The texture uses gradients on very small space, so we'll enable bilinear filtering 21 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 22 23 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb"); 24 25 for (int i = 0; i < TOWER_TYPE_COUNT; i++) 26 { 27 if (towerModels[i].materials) 28 { 29 // assign the palette texture to the material of the model (0 is not used afaik) 30 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 31 } 32 }
33 } 34 35 static void TowerGunUpdate(Tower *tower) 36 { 37 if (tower->cooldown <= 0) 38 { 39 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 40 if (enemy) 41 { 42 tower->cooldown = 0.5f; 43 // shoot the enemy; determine future position of the enemy 44 float bulletSpeed = 1.0f; 45 float bulletDamage = 3.0f; 46 Vector2 velocity = enemy->simVelocity; 47 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 48 Vector2 towerPosition = {tower->x, tower->y}; 49 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 50 for (int i = 0; i < 8; i++) { 51 velocity = enemy->simVelocity; 52 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 53 float distance = Vector2Distance(towerPosition, futurePosition); 54 float eta2 = distance / bulletSpeed; 55 if (fabs(eta - eta2) < 0.01f) { 56 break; 57 } 58 eta = (eta2 + eta) * 0.5f; 59 } 60 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition, 61 bulletSpeed, bulletDamage); 62 enemy->futureDamage += bulletDamage; 63 } 64 } 65 else 66 { 67 tower->cooldown -= gameTime.deltaTime; 68 } 69 } 70 71 Tower *TowerGetAt(int16_t x, int16_t y) 72 { 73 for (int i = 0; i < towerCount; i++) 74 {
75 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
76 { 77 return &towers[i]; 78 } 79 } 80 return 0; 81 } 82 83 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 84 { 85 if (towerCount >= TOWER_MAX_COUNT) 86 { 87 return 0; 88 } 89 90 Tower *tower = TowerGetAt(x, y); 91 if (tower) 92 { 93 return 0; 94 } 95 96 tower = &towers[towerCount++]; 97 tower->x = x; 98 tower->y = y; 99 tower->towerType = towerType; 100 tower->cooldown = 0.0f; 101 tower->damage = 0.0f; 102 return tower; 103 } 104 105 Tower *GetTowerByType(uint8_t towerType) 106 { 107 for (int i = 0; i < towerCount; i++) 108 { 109 if (towers[i].towerType == towerType) 110 { 111 return &towers[i]; 112 } 113 } 114 return 0; 115 } 116 117 int GetTowerCosts(uint8_t towerType) 118 { 119 switch (towerType) 120 { 121 case TOWER_TYPE_BASE: 122 return 0; 123 case TOWER_TYPE_GUN: 124 return 6; 125 case TOWER_TYPE_WALL: 126 return 2; 127 } 128 return 0; 129 } 130 131 float TowerGetMaxHealth(Tower *tower) 132 { 133 switch (tower->towerType) 134 { 135 case TOWER_TYPE_BASE: 136 return 10.0f; 137 case TOWER_TYPE_GUN: 138 return 3.0f; 139 case TOWER_TYPE_WALL: 140 return 5.0f; 141 } 142 return 0.0f; 143 } 144 145 void TowerDraw() 146 { 147 for (int i = 0; i < towerCount; i++) 148 { 149 Tower tower = towers[i];
150 if (tower.towerType == TOWER_TYPE_NONE) 151 { 152 continue; 153 } 154
155 switch (tower.towerType) 156 { 157 case TOWER_TYPE_BASE: 158 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 159 break; 160 case TOWER_TYPE_GUN: 161 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 162 break;
163 default: 164 if (towerModels[tower.towerType].materials) 165 { 166 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 167 } else { 168 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 169 }
170 break; 171 } 172 } 173 } 174 175 void TowerUpdate() 176 { 177 for (int i = 0; i < towerCount; i++) 178 { 179 Tower *tower = &towers[i]; 180 switch (tower->towerType) 181 { 182 case TOWER_TYPE_GUN: 183 TowerGunUpdate(tower); 184 break; 185 } 186 } 187 } 188 189 void TowerDrawHealthBars(Camera3D camera) 190 { 191 for (int i = 0; i < towerCount; i++) 192 { 193 Tower *tower = &towers[i]; 194 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 195 { 196 continue; 197 } 198 199 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 200 float maxHealth = TowerGetMaxHealth(tower); 201 float health = maxHealth - tower->damage; 202 float healthRatio = health / maxHealth; 203 204 DrawHealthBar(camera, position, healthRatio, GREEN); 205 } 206 }
  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

Place some walls to see the graphics:

There is now only a model for the walls; everything else is untouched:

Walls are now 3D models.

Typically, prototyping assets are simple and not very detailed, but my weakness is that I tend to look at things from an artistic perspective and prefer to have things looking a little bit more polished.

So the setting is now good old medieval or fantasy; So the enemies should be some kind of orcs or goblins and the shooter ... how about adding archers to the towers, also as sprites? This means we need to combine 3D models with 2D sprites. Let's experiment a little

  • 💾
  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 Level levels[] = {
 11   [0] = {
 12     .state = LEVEL_STATE_BUILDING,
 13     .initialGold = 20,
 14     .waves[0] = {
 15       .enemyType = ENEMY_TYPE_MINION,
 16       .wave = 0,
 17       .count = 10,
 18       .interval = 2.5f,
 19       .delay = 1.0f,
 20       .spawnPosition = {0, 6},
 21     },
 22     .waves[1] = {
 23       .enemyType = ENEMY_TYPE_MINION,
 24       .wave = 1,
 25       .count = 20,
 26       .interval = 1.5f,
 27       .delay = 1.0f,
 28       .spawnPosition = {0, 6},
 29     },
 30     .waves[2] = {
 31       .enemyType = ENEMY_TYPE_MINION,
 32       .wave = 2,
 33       .count = 30,
 34       .interval = 1.2f,
 35       .delay = 1.0f,
 36       .spawnPosition = {0, 6},
 37     }
 38   },
 39 };
 40 
 41 Level *currentLevel = levels;
 42 
 43 //# Game
 44 
 45 void InitLevel(Level *level)
 46 {
 47   TowerInit();
 48   EnemyInit();
 49   ProjectileInit();
 50   ParticleInit();
 51   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
 52 
 53   level->placementMode = 0;
 54   level->state = LEVEL_STATE_BUILDING;
 55   level->nextState = LEVEL_STATE_NONE;
 56   level->playerGold = level->initialGold;
 57 
 58   Camera *camera = &level->camera;
 59   camera->position = (Vector3){1.0f, 12.0f, 6.5f};
 60   camera->target = (Vector3){0.0f, 0.5f, 1.0f};
 61   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
 62   camera->fovy = 45.0f;
 63   camera->projection = CAMERA_PERSPECTIVE;
 64 }
 65 
 66 void DrawLevelHud(Level *level)
 67 {
 68   const char *text = TextFormat("Gold: %d", level->playerGold);
 69   Font font = GetFontDefault();
 70   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
 71   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
 72 }
 73 
 74 void DrawLevelReportLostWave(Level *level)
 75 {
 76   BeginMode3D(level->camera);
 77   DrawGrid(10, 1.0f);
 78   TowerDraw();
 79   EnemyDraw();
 80   ProjectileDraw();
 81   ParticleDraw();
 82   guiState.isBlocked = 0;
 83   EndMode3D();
 84 
 85   TowerDrawHealthBars(level->camera);
 86 
 87   const char *text = "Wave lost";
 88   int textWidth = MeasureText(text, 20);
 89   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
 90 
 91   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
 92   {
 93     level->nextState = LEVEL_STATE_RESET;
 94   }
 95 }
 96 
 97 int HasLevelNextWave(Level *level)
 98 {
 99   for (int i = 0; i < 10; i++)
100   {
101     EnemyWave *wave = &level->waves[i];
102     if (wave->wave == level->currentWave)
103     {
104       return 1;
105     }
106   }
107   return 0;
108 }
109 
110 void DrawLevelReportWonWave(Level *level)
111 {
112   BeginMode3D(level->camera);
113   DrawGrid(10, 1.0f);
114   TowerDraw();
115   EnemyDraw();
116   ProjectileDraw();
117   ParticleDraw();
118   guiState.isBlocked = 0;
119   EndMode3D();
120 
121   TowerDrawHealthBars(level->camera);
122 
123   const char *text = "Wave won";
124   int textWidth = MeasureText(text, 20);
125   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
126 
127 
128   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
129   {
130     level->nextState = LEVEL_STATE_RESET;
131   }
132 
133   if (HasLevelNextWave(level))
134   {
135     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
136     {
137       level->nextState = LEVEL_STATE_BUILDING;
138     }
139   }
140   else {
141     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
142     {
143       level->nextState = LEVEL_STATE_WON_LEVEL;
144     }
145   }
146 }
147 
148 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
149 {
150   static ButtonState buttonStates[8] = {0};
151   int cost = GetTowerCosts(towerType);
152   const char *text = TextFormat("%s: %d", name, cost);
153   buttonStates[towerType].isSelected = level->placementMode == towerType;
154   buttonStates[towerType].isDisabled = level->playerGold < cost;
155   if (Button(text, x, y, width, height, &buttonStates[towerType]))
156   {
157     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
158   }
159 }
160 
161 void DrawLevelBuildingState(Level *level)
162 {
163   BeginMode3D(level->camera);
164   DrawGrid(10, 1.0f);
165   TowerDraw();
166   EnemyDraw();
167   ProjectileDraw();
168   ParticleDraw();
169 
170   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
171   float planeDistance = ray.position.y / -ray.direction.y;
172   float planeX = ray.direction.x * planeDistance + ray.position.x;
173   float planeY = ray.direction.z * planeDistance + ray.position.z;
174   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
175   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
176   if (level->placementMode && !guiState.isBlocked)
177   {
178     DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
179     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
180     {
181       if (TowerTryAdd(level->placementMode, mapX, mapY))
182       {
183         level->playerGold -= GetTowerCosts(level->placementMode);
184         level->placementMode = TOWER_TYPE_NONE;
185       }
186     }
187   }
188 
189   guiState.isBlocked = 0;
190 
191   EndMode3D();
192 
193   TowerDrawHealthBars(level->camera);
194 
195   static ButtonState buildWallButtonState = {0};
196   static ButtonState buildGunButtonState = {0};
197   buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
198   buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
199 
200   DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
201   DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
202 
203   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
204   {
205     level->nextState = LEVEL_STATE_RESET;
206   }
207   
208   if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
209   {
210     level->nextState = LEVEL_STATE_BATTLE;
211   }
212 
213   const char *text = "Building phase";
214   int textWidth = MeasureText(text, 20);
215   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
216 }
217 
218 void InitBattleStateConditions(Level *level)
219 {
220   level->state = LEVEL_STATE_BATTLE;
221   level->nextState = LEVEL_STATE_NONE;
222   level->waveEndTimer = 0.0f;
223   for (int i = 0; i < 10; i++)
224   {
225     EnemyWave *wave = &level->waves[i];
226     wave->spawned = 0;
227     wave->timeToSpawnNext = wave->delay;
228   }
229 }
230 
231 void DrawLevelBattleState(Level *level)
232 {
233   BeginMode3D(level->camera);
234   DrawGrid(10, 1.0f);
235   TowerDraw();
236   EnemyDraw();
237   ProjectileDraw();
238   ParticleDraw();
239   guiState.isBlocked = 0;
240   EndMode3D();
241 
242   EnemyDrawHealthbars(level->camera);
243   TowerDrawHealthBars(level->camera);
244 
245   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
246   {
247     level->nextState = LEVEL_STATE_RESET;
248   }
249 
250   int maxCount = 0;
251   int remainingCount = 0;
252   for (int i = 0; i < 10; i++)
253   {
254     EnemyWave *wave = &level->waves[i];
255     if (wave->wave != level->currentWave)
256     {
257       continue;
258     }
259     maxCount += wave->count;
260     remainingCount += wave->count - wave->spawned;
261   }
262   int aliveCount = EnemyCount();
263   remainingCount += aliveCount;
264 
265   const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
266   int textWidth = MeasureText(text, 20);
267   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
268 }
269 
270 void DrawLevel(Level *level)
271 {
272   switch (level->state)
273   {
274     case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
275     case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
276     case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
277     case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
278     default: break;
279   }
280 
281   DrawLevelHud(level);
282 }
283 
284 void UpdateLevel(Level *level)
285 {
286   if (level->state == LEVEL_STATE_BATTLE)
287   {
288     int activeWaves = 0;
289     for (int i = 0; i < 10; i++)
290     {
291       EnemyWave *wave = &level->waves[i];
292       if (wave->spawned >= wave->count || wave->wave != level->currentWave)
293       {
294         continue;
295       }
296       activeWaves++;
297       wave->timeToSpawnNext -= gameTime.deltaTime;
298       if (wave->timeToSpawnNext <= 0.0f)
299       {
300         Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
301         if (enemy)
302         {
303           wave->timeToSpawnNext = wave->interval;
304           wave->spawned++;
305         }
306       }
307     }
308     if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
309       level->waveEndTimer += gameTime.deltaTime;
310       if (level->waveEndTimer >= 2.0f)
311       {
312         level->nextState = LEVEL_STATE_LOST_WAVE;
313       }
314     }
315     else if (activeWaves == 0 && EnemyCount() == 0)
316     {
317       level->waveEndTimer += gameTime.deltaTime;
318       if (level->waveEndTimer >= 2.0f)
319       {
320         level->nextState = LEVEL_STATE_WON_WAVE;
321       }
322     }
323   }
324 
325   PathFindingMapUpdate();
326   EnemyUpdate();
327   TowerUpdate();
328   ProjectileUpdate();
329   ParticleUpdate();
330 
331   if (level->nextState == LEVEL_STATE_RESET)
332   {
333     InitLevel(level);
334   }
335   
336   if (level->nextState == LEVEL_STATE_BATTLE)
337   {
338     InitBattleStateConditions(level);
339   }
340   
341   if (level->nextState == LEVEL_STATE_WON_WAVE)
342   {
343     level->currentWave++;
344     level->state = LEVEL_STATE_WON_WAVE;
345   }
346   
347   if (level->nextState == LEVEL_STATE_LOST_WAVE)
348   {
349     level->state = LEVEL_STATE_LOST_WAVE;
350   }
351 
352   if (level->nextState == LEVEL_STATE_BUILDING)
353   {
354     level->state = LEVEL_STATE_BUILDING;
355   }
356 
357   if (level->nextState == LEVEL_STATE_WON_LEVEL)
358   {
359     // make something of this later
360     InitLevel(level);
361   }
362 
363   level->nextState = LEVEL_STATE_NONE;
364 }
365 
366 float nextSpawnTime = 0.0f;
367 
368 void ResetGame()
369 {
370   InitLevel(currentLevel);
371 }
372 
373 void InitGame()
374 {
375   TowerInit();
376   EnemyInit();
377   ProjectileInit();
378   ParticleInit();
379   PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
380 
381   currentLevel = levels;
382   InitLevel(currentLevel);
383 }
384 
385 //# Immediate GUI functions
386 
387 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor)
388 {
389   const float healthBarWidth = 40.0f;
390   const float healthBarHeight = 6.0f;
391   const float healthBarOffset = 15.0f;
392   const float inset = 2.0f;
393   const float innerWidth = healthBarWidth - inset * 2;
394   const float innerHeight = healthBarHeight - inset * 2;
395 
396   Vector2 screenPos = GetWorldToScreen(position, camera);
397   float centerX = screenPos.x - healthBarWidth * 0.5f;
398   float topY = screenPos.y - healthBarOffset;
399   DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
400   float healthWidth = innerWidth * healthRatio;
401   DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
402 }
403 
404 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
405 {
406   Rectangle bounds = {x, y, width, height};
407   int isPressed = 0;
408   int isSelected = state && state->isSelected;
409   if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
410   {
411     Color color = isSelected ? DARKGRAY : GRAY;
412     DrawRectangle(x, y, width, height, color);
413     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
414     {
415       isPressed = 1;
416     }
417     guiState.isBlocked = 1;
418   }
419   else
420   {
421     Color color = isSelected ? WHITE : LIGHTGRAY;
422     DrawRectangle(x, y, width, height, color);
423   }
424   Font font = GetFontDefault();
425   Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
426   Color textColor = state->isDisabled ? GRAY : BLACK;
427   DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
428   return isPressed;
429 }
430 
431 //# Main game loop
432 
433 void GameUpdate()
434 {
435   float dt = GetFrameTime();
436   // cap maximum delta time to 0.1 seconds to prevent large time steps
437   if (dt > 0.1f) dt = 0.1f;
438   gameTime.time += dt;
439   gameTime.deltaTime = dt;
440 
441   UpdateLevel(currentLevel);
442 }
443 
444 int main(void)
445 {
446   int screenWidth, screenHeight;
447   GetPreferredSize(&screenWidth, &screenHeight);
448   InitWindow(screenWidth, screenHeight, "Tower defense");
449   SetTargetFPS(30);
450 
451   InitGame();
452 
453   while (!WindowShouldClose())
454   {
455     if (IsPaused()) {
456       // canvas is not visible in browser - do nothing
457       continue;
458     }
459 
460     BeginDrawing();
461     ClearBackground(DARKBLUE);
462 
463     GameUpdate();
464     DrawLevel(currentLevel);
465 
466     EndDrawing();
467   }
468 
469   CloseWindow();
470 
471   return 0;
472 }
  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   float cooldown;
 41   float damage;
 42 } Tower;
 43 
 44 typedef struct GameTime
 45 {
 46   float time;
 47   float deltaTime;
 48 } GameTime;
 49 
 50 typedef struct ButtonState {
 51   char isSelected;
 52   char isDisabled;
 53 } ButtonState;
 54 
 55 typedef struct GUIState {
 56   int isBlocked;
 57 } GUIState;
 58 
 59 typedef enum LevelState
 60 {
 61   LEVEL_STATE_NONE,
 62   LEVEL_STATE_BUILDING,
 63   LEVEL_STATE_BATTLE,
 64   LEVEL_STATE_WON_WAVE,
 65   LEVEL_STATE_LOST_WAVE,
 66   LEVEL_STATE_WON_LEVEL,
 67   LEVEL_STATE_RESET,
 68 } LevelState;
 69 
 70 typedef struct EnemyWave {
 71   uint8_t enemyType;
 72   uint8_t wave;
 73   uint16_t count;
 74   float interval;
 75   float delay;
 76   Vector2 spawnPosition;
 77 
 78   uint16_t spawned;
 79   float timeToSpawnNext;
 80 } EnemyWave;
 81 
 82 typedef struct Level
 83 {
 84   LevelState state;
 85   LevelState nextState;
 86   Camera3D camera;
 87   int placementMode;
 88 
 89   int initialGold;
 90   int playerGold;
 91 
 92   EnemyWave waves[10];
 93   int currentWave;
 94   float waveEndTimer;
 95 } Level;
 96 
 97 typedef struct DeltaSrc
 98 {
 99   char x, y;
100 } DeltaSrc;
101 
102 typedef struct PathfindingMap
103 {
104   int width, height;
105   float scale;
106   float *distances;
107   long *towerIndex; 
108   DeltaSrc *deltaSrc;
109   float maxDistance;
110   Matrix toMapSpace;
111   Matrix toWorldSpace;
112 } PathfindingMap;
113 
114 // when we execute the pathfinding algorithm, we need to store the active nodes
115 // in a queue. Each node has a position, a distance from the start, and the
116 // position of the node that we came from.
117 typedef struct PathfindingNode
118 {
119   int16_t x, y, fromX, fromY;
120   float distance;
121 } PathfindingNode;
122 
123 typedef struct EnemyId
124 {
125   uint16_t index;
126   uint16_t generation;
127 } EnemyId;
128 
129 typedef struct EnemyClassConfig
130 {
131   float speed;
132   float health;
133   float radius;
134   float maxAcceleration;
135   float requiredContactTime;
136   float explosionDamage;
137   float explosionRange;
138   float explosionPushbackPower;
139   int goldValue;
140 } EnemyClassConfig;
141 
142 typedef struct Enemy
143 {
144   int16_t currentX, currentY;
145   int16_t nextX, nextY;
146   Vector2 simPosition;
147   Vector2 simVelocity;
148   uint16_t generation;
149   float startMovingTime;
150   float damage, futureDamage;
151   float contactTime;
152   uint8_t enemyType;
153   uint8_t movePathCount;
154   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
155 } Enemy;
156 
157 #define PROJECTILE_MAX_COUNT 1200
158 #define PROJECTILE_TYPE_NONE 0
159 #define PROJECTILE_TYPE_BULLET 1
160 
161 typedef struct Projectile
162 {
163   uint8_t projectileType;
164   float shootTime;
165   float arrivalTime;
166   float damage;
167   Vector2 position;
168   Vector2 target;
169   Vector2 directionNormal;
170   EnemyId targetEnemy;
171 } Projectile;
172 
173 //# Function declarations
174 float TowerGetMaxHealth(Tower *tower);
175 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
176 int EnemyAddDamage(Enemy *enemy, float damage);
177 
178 //# Enemy functions
179 void EnemyInit();
180 void EnemyDraw();
181 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
182 void EnemyUpdate();
183 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
184 float EnemyGetMaxHealth(Enemy *enemy);
185 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
186 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
187 EnemyId EnemyGetId(Enemy *enemy);
188 Enemy *EnemyTryResolve(EnemyId enemyId);
189 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
190 int EnemyAddDamage(Enemy *enemy, float damage);
191 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
192 int EnemyCount();
193 void EnemyDrawHealthbars(Camera3D camera);
194 
195 //# Tower functions
196 void TowerInit();
197 Tower *TowerGetAt(int16_t x, int16_t y);
198 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
199 Tower *GetTowerByType(uint8_t towerType);
200 int GetTowerCosts(uint8_t towerType);
201 float TowerGetMaxHealth(Tower *tower);
202 void TowerDraw();
203 void TowerUpdate();
204 void TowerDrawHealthBars(Camera3D camera);
205 
206 //# Particles
207 void ParticleInit();
208 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
209 void ParticleUpdate();
210 void ParticleDraw();
211 
212 //# Projectiles
213 void ProjectileInit();
214 void ProjectileDraw();
215 void ProjectileUpdate();
216 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage);
217 
218 //# Pathfinding map
219 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
220 float PathFindingGetDistance(int mapX, int mapY);
221 Vector2 PathFindingGetGradient(Vector3 world);
222 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
223 void PathFindingMapUpdate();
224 void PathFindingMapDraw();
225 
226 //# UI
227 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor);
228 
229 //# variables
230 extern Level *currentLevel;
231 extern Enemy enemies[ENEMY_MAX_COUNT];
232 extern int enemyCount;
233 extern EnemyClassConfig enemyClassConfigs[];
234 
235 extern GUIState guiState;
236 extern GameTime gameTime;
237 extern Tower towers[TOWER_MAX_COUNT];
238 extern int towerCount;
239 
240 #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 void EnemyInit()
 24 {
 25   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 26   {
 27     enemies[i] = (Enemy){0};
 28   }
 29   enemyCount = 0;
 30 }
 31 
 32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 33 {
 34   return enemyClassConfigs[enemy->enemyType].speed;
 35 }
 36 
 37 float EnemyGetMaxHealth(Enemy *enemy)
 38 {
 39   return enemyClassConfigs[enemy->enemyType].health;
 40 }
 41 
 42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 43 {
 44   int16_t castleX = 0;
 45   int16_t castleY = 0;
 46   int16_t dx = castleX - currentX;
 47   int16_t dy = castleY - currentY;
 48   if (dx == 0 && dy == 0)
 49   {
 50     *nextX = currentX;
 51     *nextY = currentY;
 52     return 1;
 53   }
 54   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 55 
 56   if (gradient.x == 0 && gradient.y == 0)
 57   {
 58     *nextX = currentX;
 59     *nextY = currentY;
 60     return 1;
 61   }
 62 
 63   if (fabsf(gradient.x) > fabsf(gradient.y))
 64   {
 65     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 66     *nextY = currentY;
 67     return 0;
 68   }
 69   *nextX = currentX;
 70   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 71   return 0;
 72 }
 73 
 74 
 75 // this function predicts the movement of the unit for the next deltaT seconds
 76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 77 {
 78   const float pointReachedDistance = 0.25f;
 79   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 80   const float maxSimStepTime = 0.015625f;
 81   
 82   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 83   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 84   int16_t nextX = enemy->nextX;
 85   int16_t nextY = enemy->nextY;
 86   Vector2 position = enemy->simPosition;
 87   int passedCount = 0;
 88   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 89   {
 90     float stepTime = fminf(deltaT - t, maxSimStepTime);
 91     Vector2 target = (Vector2){nextX, nextY};
 92     float speed = Vector2Length(*velocity);
 93     // draw the target position for debugging
 94     DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
 95     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
 96     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
 97     {
 98       // we reached the target position, let's move to the next waypoint
 99       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100       target = (Vector2){nextX, nextY};
101       // track how many waypoints we passed
102       passedCount++;
103     }
104     
105     // acceleration towards the target
106     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108     *velocity = Vector2Add(*velocity, acceleration);
109 
110     // limit the speed to the maximum speed
111     if (speed > maxSpeed)
112     {
113       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114     }
115 
116     // move the enemy
117     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118   }
119 
120   if (waypointPassedCount)
121   {
122     (*waypointPassedCount) = passedCount;
123   }
124 
125   return position;
126 }
127 
128 void EnemyDraw()
129 {
130   for (int i = 0; i < enemyCount; i++)
131   {
132     Enemy enemy = enemies[i];
133     if (enemy.enemyType == ENEMY_TYPE_NONE)
134     {
135       continue;
136     }
137 
138     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139     
140     if (enemy.movePathCount > 0)
141     {
142       Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143       DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144     }
145     for (int j = 1; j < enemy.movePathCount; j++)
146     {
147       Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148       Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149       DrawLine3D(p, q, GREEN);
150     }
151 
152     switch (enemy.enemyType)
153     {
154     case ENEMY_TYPE_MINION:
155       DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156       break;
157     }
158   }
159 }
160 
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163   // damage the tower
164   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167   float explosionRange2 = explosionRange * explosionRange;
168   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169   // explode the enemy
170   if (tower->damage >= TowerGetMaxHealth(tower))
171   {
172     tower->towerType = TOWER_TYPE_NONE;
173   }
174 
175   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
176     explosionSource, 
177     (Vector3){0, 0.1f, 0}, 1.0f);
178 
179   enemy->enemyType = ENEMY_TYPE_NONE;
180 
181   // push back enemies & dealing damage
182   for (int i = 0; i < enemyCount; i++)
183   {
184     Enemy *other = &enemies[i];
185     if (other->enemyType == ENEMY_TYPE_NONE)
186     {
187       continue;
188     }
189     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190     if (distanceSqr > 0 && distanceSqr < explosionRange2)
191     {
192       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194       EnemyAddDamage(other, explosionDamge);
195     }
196   }
197 }
198 
199 void EnemyUpdate()
200 {
201   const float castleX = 0;
202   const float castleY = 0;
203   const float maxPathDistance2 = 0.25f * 0.25f;
204   
205   for (int i = 0; i < enemyCount; i++)
206   {
207     Enemy *enemy = &enemies[i];
208     if (enemy->enemyType == ENEMY_TYPE_NONE)
209     {
210       continue;
211     }
212 
213     int waypointPassedCount = 0;
214     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215     enemy->startMovingTime = gameTime.time;
216     // track path of unit
217     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218     {
219       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220       {
221         enemy->movePath[j] = enemy->movePath[j - 1];
222       }
223       enemy->movePath[0] = enemy->simPosition;
224       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225       {
226         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227       }
228     }
229 
230     if (waypointPassedCount > 0)
231     {
232       enemy->currentX = enemy->nextX;
233       enemy->currentY = enemy->nextY;
234       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236       {
237         // enemy reached the castle; remove it
238         enemy->enemyType = ENEMY_TYPE_NONE;
239         continue;
240       }
241     }
242   }
243 
244   // handle collisions between enemies
245   for (int i = 0; i < enemyCount - 1; i++)
246   {
247     Enemy *enemyA = &enemies[i];
248     if (enemyA->enemyType == ENEMY_TYPE_NONE)
249     {
250       continue;
251     }
252     for (int j = i + 1; j < enemyCount; j++)
253     {
254       Enemy *enemyB = &enemies[j];
255       if (enemyB->enemyType == ENEMY_TYPE_NONE)
256       {
257         continue;
258       }
259       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262       float radiusSum = radiusA + radiusB;
263       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264       {
265         // collision
266         float distance = sqrtf(distanceSqr);
267         float overlap = radiusSum - distance;
268         // move the enemies apart, but softly; if we have a clog of enemies,
269         // moving them perfectly apart can cause them to jitter
270         float positionCorrection = overlap / 5.0f;
271         Vector2 direction = (Vector2){
272             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276       }
277     }
278   }
279 
280   // handle collisions between enemies and towers
281   for (int i = 0; i < enemyCount; i++)
282   {
283     Enemy *enemy = &enemies[i];
284     if (enemy->enemyType == ENEMY_TYPE_NONE)
285     {
286       continue;
287     }
288     enemy->contactTime -= gameTime.deltaTime;
289     if (enemy->contactTime < 0.0f)
290     {
291       enemy->contactTime = 0.0f;
292     }
293 
294     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295     // linear search over towers; could be optimized by using path finding tower map,
296     // but for now, we keep it simple
297     for (int j = 0; j < towerCount; j++)
298     {
299       Tower *tower = &towers[j];
300       if (tower->towerType == TOWER_TYPE_NONE)
301       {
302         continue;
303       }
304       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306       if (distanceSqr > combinedRadius * combinedRadius)
307       {
308         continue;
309       }
310       // potential collision; square / circle intersection
311       float dx = tower->x - enemy->simPosition.x;
312       float dy = tower->y - enemy->simPosition.y;
313       float absDx = fabsf(dx);
314       float absDy = fabsf(dy);
315       Vector3 contactPoint = {0};
316       if (absDx <= 0.5f && absDx <= absDy) {
317         // vertical collision; push the enemy out horizontally
318         float overlap = enemyRadius + 0.5f - absDy;
319         if (overlap < 0.0f)
320         {
321           continue;
322         }
323         float direction = dy > 0.0f ? -1.0f : 1.0f;
324         enemy->simPosition.y += direction * overlap;
325         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
326       }
327       else if (absDy <= 0.5f && absDy <= absDx)
328       {
329         // horizontal collision; push the enemy out vertically
330         float overlap = enemyRadius + 0.5f - absDx;
331         if (overlap < 0.0f)
332         {
333           continue;
334         }
335         float direction = dx > 0.0f ? -1.0f : 1.0f;
336         enemy->simPosition.x += direction * overlap;
337         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
338       }
339       else
340       {
341         // possible collision with a corner
342         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
343         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
344         float cornerX = tower->x + cornerDX;
345         float cornerY = tower->y + cornerDY;
346         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
347         if (cornerDistanceSqr > enemyRadius * enemyRadius)
348         {
349           continue;
350         }
351         // push the enemy out along the diagonal
352         float cornerDistance = sqrtf(cornerDistanceSqr);
353         float overlap = enemyRadius - cornerDistance;
354         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
355         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
356         enemy->simPosition.x -= directionX * overlap;
357         enemy->simPosition.y -= directionY * overlap;
358         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
359       }
360 
361       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
362       {
363         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
364         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
365         {
366           EnemyTriggerExplode(enemy, tower, contactPoint);
367         }
368       }
369     }
370   }
371 }
372 
373 EnemyId EnemyGetId(Enemy *enemy)
374 {
375   return (EnemyId){enemy - enemies, enemy->generation};
376 }
377 
378 Enemy *EnemyTryResolve(EnemyId enemyId)
379 {
380   if (enemyId.index >= ENEMY_MAX_COUNT)
381   {
382     return 0;
383   }
384   Enemy *enemy = &enemies[enemyId.index];
385   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
386   {
387     return 0;
388   }
389   return enemy;
390 }
391 
392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
393 {
394   Enemy *spawn = 0;
395   for (int i = 0; i < enemyCount; i++)
396   {
397     Enemy *enemy = &enemies[i];
398     if (enemy->enemyType == ENEMY_TYPE_NONE)
399     {
400       spawn = enemy;
401       break;
402     }
403   }
404 
405   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
406   {
407     spawn = &enemies[enemyCount++];
408   }
409 
410   if (spawn)
411   {
412     spawn->currentX = currentX;
413     spawn->currentY = currentY;
414     spawn->nextX = currentX;
415     spawn->nextY = currentY;
416     spawn->simPosition = (Vector2){currentX, currentY};
417     spawn->simVelocity = (Vector2){0, 0};
418     spawn->enemyType = enemyType;
419     spawn->startMovingTime = gameTime.time;
420     spawn->damage = 0.0f;
421     spawn->futureDamage = 0.0f;
422     spawn->generation++;
423     spawn->movePathCount = 0;
424   }
425 
426   return spawn;
427 }
428 
429 int EnemyAddDamage(Enemy *enemy, float damage)
430 {
431   enemy->damage += damage;
432   if (enemy->damage >= EnemyGetMaxHealth(enemy))
433   {
434     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
435     enemy->enemyType = ENEMY_TYPE_NONE;
436     return 1;
437   }
438 
439   return 0;
440 }
441 
442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
443 {
444   int16_t castleX = 0;
445   int16_t castleY = 0;
446   Enemy* closest = 0;
447   int16_t closestDistance = 0;
448   float range2 = range * range;
449   for (int i = 0; i < enemyCount; i++)
450   {
451     Enemy* enemy = &enemies[i];
452     if (enemy->enemyType == ENEMY_TYPE_NONE)
453     {
454       continue;
455     }
456     float maxHealth = EnemyGetMaxHealth(enemy);
457     if (enemy->futureDamage >= maxHealth)
458     {
459       // ignore enemies that will die soon
460       continue;
461     }
462     int16_t dx = castleX - enemy->currentX;
463     int16_t dy = castleY - enemy->currentY;
464     int16_t distance = abs(dx) + abs(dy);
465     if (!closest || distance < closestDistance)
466     {
467       float tdx = towerX - enemy->currentX;
468       float tdy = towerY - enemy->currentY;
469       float tdistance2 = tdx * tdx + tdy * tdy;
470       if (tdistance2 <= range2)
471       {
472         closest = enemy;
473         closestDistance = distance;
474       }
475     }
476   }
477   return closest;
478 }
479 
480 int EnemyCount()
481 {
482   int count = 0;
483   for (int i = 0; i < enemyCount; i++)
484   {
485     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
486     {
487       count++;
488     }
489   }
490   return count;
491 }
492 
493 void EnemyDrawHealthbars(Camera3D camera)
494 {
495   for (int i = 0; i < enemyCount; i++)
496   {
497     Enemy *enemy = &enemies[i];
498     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
499     {
500       continue;
501     }
502     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
503     float maxHealth = EnemyGetMaxHealth(enemy);
504     float health = maxHealth - enemy->damage;
505     float healthRatio = health / maxHealth;
506     
507     DrawHealthBar(camera, position, healthRatio, GREEN);
508   }
509 }
  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     Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
 30     float x = position.x;
 31     float y = position.y;
 32     float dx = projectile.directionNormal.x;
 33     float dy = projectile.directionNormal.y;
 34     for (float d = 1.0f; d > 0.0f; d -= 0.25f)
 35     {
 36       x -= dx * 0.1f;
 37       y -= dy * 0.1f;
 38       float size = 0.1f * d;
 39       DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
 40     }
 41   }
 42 }
 43 
 44 void ProjectileUpdate()
 45 {
 46   for (int i = 0; i < projectileCount; i++)
 47   {
 48     Projectile *projectile = &projectiles[i];
 49     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 50     {
 51       continue;
 52     }
 53     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 54     if (transition >= 1.0f)
 55     {
 56       projectile->projectileType = PROJECTILE_TYPE_NONE;
 57       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 58       if (enemy)
 59       {
 60         EnemyAddDamage(enemy, projectile->damage);
 61       }
 62       continue;
 63     }
 64   }
 65 }
 66 
 67 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
 68 {
 69   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 70   {
 71     Projectile *projectile = &projectiles[i];
 72     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 73     {
 74       projectile->projectileType = projectileType;
 75       projectile->shootTime = gameTime.time;
 76       projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
 77       projectile->damage = damage;
 78       projectile->position = position;
 79       projectile->target = target;
 80       projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
 81       projectile->targetEnemy = EnemyGetId(enemy);
 82       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 83       return projectile;
 84     }
 85   }
 86   return 0;
 87 }
  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 Texture2D palette, spriteSheet; 9 10 // a unit that uses sprites to be drawn 11 typedef struct SpriteUnit 12 { 13 Rectangle srcRect; 14 Vector2 offset; 15 } SpriteUnit; 16 17 // definition of our archer unit 18 SpriteUnit archerUnit = { 19 .srcRect = {0, 0, 16, 16}, 20 .offset = {7, 1}, 21 }; 22 23 void DrawSpriteUnit(SpriteUnit unit, Vector3 position) 24 { 25 Camera3D camera = currentLevel->camera; 26 float size = 0.5f; 27 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size }; 28 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size }; 29 // we want the sprite to face the camera, so we need to calculate the up vector 30 Vector3 forward = Vector3Subtract(camera.target, camera.position); 31 Vector3 up = {0, 1, 0}; 32 Vector3 right = Vector3CrossProduct(forward, up); 33 up = Vector3Normalize(Vector3CrossProduct(right, forward)); 34 DrawBillboardPro(camera, spriteSheet, unit.srcRect, position, up, scale, offset, 0, WHITE); 35 }
36 37 void TowerInit() 38 { 39 for (int i = 0; i < TOWER_MAX_COUNT; i++) 40 { 41 towers[i] = (Tower){0}; 42 }
43 towerCount = 0; 44 45 // load a sprite sheet that contains all units 46 spriteSheet = LoadTexture("data/spritesheet.png");
47 48 // we'll use a palette texture to colorize the all buildings and environment art 49 palette = LoadTexture("data/palette.png"); 50 // The texture uses gradients on very small space, so we'll enable bilinear filtering 51 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 52 53 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb"); 54 55 for (int i = 0; i < TOWER_TYPE_COUNT; i++) 56 { 57 if (towerModels[i].materials) 58 { 59 // assign the palette texture to the material of the model (0 is not used afaik) 60 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 61 } 62 } 63 } 64 65 static void TowerGunUpdate(Tower *tower) 66 { 67 if (tower->cooldown <= 0) 68 { 69 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 70 if (enemy) 71 { 72 tower->cooldown = 0.5f; 73 // shoot the enemy; determine future position of the enemy 74 float bulletSpeed = 1.0f; 75 float bulletDamage = 3.0f; 76 Vector2 velocity = enemy->simVelocity; 77 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 78 Vector2 towerPosition = {tower->x, tower->y}; 79 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 80 for (int i = 0; i < 8; i++) { 81 velocity = enemy->simVelocity; 82 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 83 float distance = Vector2Distance(towerPosition, futurePosition); 84 float eta2 = distance / bulletSpeed; 85 if (fabs(eta - eta2) < 0.01f) { 86 break; 87 } 88 eta = (eta2 + eta) * 0.5f; 89 } 90 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition, 91 bulletSpeed, bulletDamage); 92 enemy->futureDamage += bulletDamage; 93 } 94 } 95 else 96 { 97 tower->cooldown -= gameTime.deltaTime; 98 } 99 } 100 101 Tower *TowerGetAt(int16_t x, int16_t y) 102 { 103 for (int i = 0; i < towerCount; i++) 104 { 105 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE) 106 { 107 return &towers[i]; 108 } 109 } 110 return 0; 111 } 112 113 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 114 { 115 if (towerCount >= TOWER_MAX_COUNT) 116 { 117 return 0; 118 } 119 120 Tower *tower = TowerGetAt(x, y); 121 if (tower) 122 { 123 return 0; 124 } 125 126 tower = &towers[towerCount++]; 127 tower->x = x; 128 tower->y = y; 129 tower->towerType = towerType; 130 tower->cooldown = 0.0f; 131 tower->damage = 0.0f; 132 return tower; 133 } 134 135 Tower *GetTowerByType(uint8_t towerType) 136 { 137 for (int i = 0; i < towerCount; i++) 138 { 139 if (towers[i].towerType == towerType) 140 { 141 return &towers[i]; 142 } 143 } 144 return 0; 145 } 146 147 int GetTowerCosts(uint8_t towerType) 148 { 149 switch (towerType) 150 { 151 case TOWER_TYPE_BASE: 152 return 0; 153 case TOWER_TYPE_GUN: 154 return 6; 155 case TOWER_TYPE_WALL: 156 return 2; 157 } 158 return 0; 159 } 160 161 float TowerGetMaxHealth(Tower *tower) 162 { 163 switch (tower->towerType) 164 { 165 case TOWER_TYPE_BASE: 166 return 10.0f; 167 case TOWER_TYPE_GUN: 168 return 3.0f; 169 case TOWER_TYPE_WALL: 170 return 5.0f; 171 } 172 return 0.0f; 173 } 174 175 void TowerDraw() 176 { 177 for (int i = 0; i < towerCount; i++) 178 { 179 Tower tower = towers[i]; 180 if (tower.towerType == TOWER_TYPE_NONE) 181 { 182 continue; 183 } 184 185 switch (tower.towerType) 186 { 187 case TOWER_TYPE_BASE: 188 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 189 break; 190 case TOWER_TYPE_GUN:
191 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 192 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y});
193 break; 194 default: 195 if (towerModels[tower.towerType].materials) 196 { 197 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 198 } else { 199 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 200 } 201 break; 202 } 203 } 204 } 205 206 void TowerUpdate() 207 { 208 for (int i = 0; i < towerCount; i++) 209 { 210 Tower *tower = &towers[i]; 211 switch (tower->towerType) 212 { 213 case TOWER_TYPE_GUN: 214 TowerGunUpdate(tower); 215 break; 216 } 217 } 218 } 219 220 void TowerDrawHealthBars(Camera3D camera) 221 { 222 for (int i = 0; i < towerCount; i++) 223 { 224 Tower *tower = &towers[i]; 225 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 226 { 227 continue; 228 } 229 230 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 231 float maxHealth = TowerGetMaxHealth(tower); 232 float health = maxHealth - tower->damage; 233 float healthRatio = health / maxHealth; 234 235 DrawHealthBar(camera, position, healthRatio, GREEN); 236 } 237 }
  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

Place a gun to see a tower with the sprite on top:

So we have now a tower on which a ... person is standing. It's not an archer yet, but looking at the result...

Towers now have archers on them.

... maybe we should try an orthogonal view for the game. This way, the sprites should mix better with the 3D models. Let's try this:

  • 💾
  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 Level levels[] = {
 11   [0] = {
 12     .state = LEVEL_STATE_BUILDING,
 13     .initialGold = 20,
 14     .waves[0] = {
 15       .enemyType = ENEMY_TYPE_MINION,
 16       .wave = 0,
 17       .count = 10,
 18       .interval = 2.5f,
 19       .delay = 1.0f,
 20       .spawnPosition = {0, 6},
 21     },
 22     .waves[1] = {
 23       .enemyType = ENEMY_TYPE_MINION,
 24       .wave = 1,
 25       .count = 20,
 26       .interval = 1.5f,
 27       .delay = 1.0f,
 28       .spawnPosition = {0, 6},
 29     },
 30     .waves[2] = {
 31       .enemyType = ENEMY_TYPE_MINION,
 32       .wave = 2,
 33       .count = 30,
 34       .interval = 1.2f,
 35       .delay = 1.0f,
 36       .spawnPosition = {0, 6},
 37     }
 38   },
 39 };
 40 
 41 Level *currentLevel = levels;
 42 
 43 //# Game
 44 
 45 void InitLevel(Level *level)
 46 {
 47   TowerInit();
 48   EnemyInit();
 49   ProjectileInit();
 50   ParticleInit();
 51   TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
 52 
 53   level->placementMode = 0;
 54   level->state = LEVEL_STATE_BUILDING;
 55   level->nextState = LEVEL_STATE_NONE;
 56   level->playerGold = level->initialGold;
 57 
 58   Camera *camera = &level->camera;
59 camera->position = (Vector3){4.0f, 8.0f, 8.0f}; 60 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
61 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
62 camera->fovy = 10.0f; 63 camera->projection = CAMERA_ORTHOGRAPHIC;
64 } 65 66 void DrawLevelHud(Level *level) 67 { 68 const char *text = TextFormat("Gold: %d", level->playerGold); 69 Font font = GetFontDefault(); 70 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK); 71 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW); 72 } 73 74 void DrawLevelReportLostWave(Level *level) 75 { 76 BeginMode3D(level->camera); 77 DrawGrid(10, 1.0f); 78 TowerDraw(); 79 EnemyDraw(); 80 ProjectileDraw(); 81 ParticleDraw(); 82 guiState.isBlocked = 0; 83 EndMode3D(); 84 85 TowerDrawHealthBars(level->camera); 86 87 const char *text = "Wave lost"; 88 int textWidth = MeasureText(text, 20); 89 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 90 91 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 92 { 93 level->nextState = LEVEL_STATE_RESET; 94 } 95 } 96 97 int HasLevelNextWave(Level *level) 98 { 99 for (int i = 0; i < 10; i++) 100 { 101 EnemyWave *wave = &level->waves[i]; 102 if (wave->wave == level->currentWave) 103 { 104 return 1; 105 } 106 } 107 return 0; 108 } 109 110 void DrawLevelReportWonWave(Level *level) 111 { 112 BeginMode3D(level->camera); 113 DrawGrid(10, 1.0f); 114 TowerDraw(); 115 EnemyDraw(); 116 ProjectileDraw(); 117 ParticleDraw(); 118 guiState.isBlocked = 0; 119 EndMode3D(); 120 121 TowerDrawHealthBars(level->camera); 122 123 const char *text = "Wave won"; 124 int textWidth = MeasureText(text, 20); 125 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 126 127 128 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 129 { 130 level->nextState = LEVEL_STATE_RESET; 131 } 132 133 if (HasLevelNextWave(level)) 134 { 135 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 136 { 137 level->nextState = LEVEL_STATE_BUILDING; 138 } 139 } 140 else { 141 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0)) 142 { 143 level->nextState = LEVEL_STATE_WON_LEVEL; 144 } 145 } 146 } 147 148 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name) 149 { 150 static ButtonState buttonStates[8] = {0}; 151 int cost = GetTowerCosts(towerType); 152 const char *text = TextFormat("%s: %d", name, cost); 153 buttonStates[towerType].isSelected = level->placementMode == towerType; 154 buttonStates[towerType].isDisabled = level->playerGold < cost; 155 if (Button(text, x, y, width, height, &buttonStates[towerType])) 156 { 157 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType; 158 } 159 } 160 161 void DrawLevelBuildingState(Level *level) 162 { 163 BeginMode3D(level->camera); 164 DrawGrid(10, 1.0f); 165 TowerDraw(); 166 EnemyDraw(); 167 ProjectileDraw(); 168 ParticleDraw(); 169 170 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera); 171 float planeDistance = ray.position.y / -ray.direction.y; 172 float planeX = ray.direction.x * planeDistance + ray.position.x; 173 float planeY = ray.direction.z * planeDistance + ray.position.z; 174 int16_t mapX = (int16_t)floorf(planeX + 0.5f); 175 int16_t mapY = (int16_t)floorf(planeY + 0.5f); 176 if (level->placementMode && !guiState.isBlocked) 177 { 178 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED); 179 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 180 { 181 if (TowerTryAdd(level->placementMode, mapX, mapY)) 182 { 183 level->playerGold -= GetTowerCosts(level->placementMode); 184 level->placementMode = TOWER_TYPE_NONE; 185 } 186 } 187 } 188 189 guiState.isBlocked = 0; 190 191 EndMode3D(); 192 193 TowerDrawHealthBars(level->camera); 194 195 static ButtonState buildWallButtonState = {0}; 196 static ButtonState buildGunButtonState = {0}; 197 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL; 198 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN; 199 200 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall"); 201 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun"); 202 203 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 204 { 205 level->nextState = LEVEL_STATE_RESET; 206 } 207 208 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0)) 209 { 210 level->nextState = LEVEL_STATE_BATTLE; 211 } 212 213 const char *text = "Building phase"; 214 int textWidth = MeasureText(text, 20); 215 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 216 } 217 218 void InitBattleStateConditions(Level *level) 219 { 220 level->state = LEVEL_STATE_BATTLE; 221 level->nextState = LEVEL_STATE_NONE; 222 level->waveEndTimer = 0.0f; 223 for (int i = 0; i < 10; i++) 224 { 225 EnemyWave *wave = &level->waves[i]; 226 wave->spawned = 0; 227 wave->timeToSpawnNext = wave->delay; 228 } 229 } 230 231 void DrawLevelBattleState(Level *level) 232 { 233 BeginMode3D(level->camera); 234 DrawGrid(10, 1.0f); 235 TowerDraw(); 236 EnemyDraw(); 237 ProjectileDraw(); 238 ParticleDraw(); 239 guiState.isBlocked = 0; 240 EndMode3D(); 241 242 EnemyDrawHealthbars(level->camera); 243 TowerDrawHealthBars(level->camera); 244 245 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0)) 246 { 247 level->nextState = LEVEL_STATE_RESET; 248 } 249 250 int maxCount = 0; 251 int remainingCount = 0; 252 for (int i = 0; i < 10; i++) 253 { 254 EnemyWave *wave = &level->waves[i]; 255 if (wave->wave != level->currentWave) 256 { 257 continue; 258 } 259 maxCount += wave->count; 260 remainingCount += wave->count - wave->spawned; 261 } 262 int aliveCount = EnemyCount(); 263 remainingCount += aliveCount; 264 265 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount); 266 int textWidth = MeasureText(text, 20); 267 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE); 268 } 269 270 void DrawLevel(Level *level) 271 { 272 switch (level->state) 273 { 274 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break; 275 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break; 276 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break; 277 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break; 278 default: break; 279 } 280 281 DrawLevelHud(level); 282 } 283 284 void UpdateLevel(Level *level) 285 { 286 if (level->state == LEVEL_STATE_BATTLE) 287 { 288 int activeWaves = 0; 289 for (int i = 0; i < 10; i++) 290 { 291 EnemyWave *wave = &level->waves[i]; 292 if (wave->spawned >= wave->count || wave->wave != level->currentWave) 293 { 294 continue; 295 } 296 activeWaves++; 297 wave->timeToSpawnNext -= gameTime.deltaTime; 298 if (wave->timeToSpawnNext <= 0.0f) 299 { 300 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y); 301 if (enemy) 302 { 303 wave->timeToSpawnNext = wave->interval; 304 wave->spawned++; 305 } 306 } 307 } 308 if (GetTowerByType(TOWER_TYPE_BASE) == 0) { 309 level->waveEndTimer += gameTime.deltaTime; 310 if (level->waveEndTimer >= 2.0f) 311 { 312 level->nextState = LEVEL_STATE_LOST_WAVE; 313 } 314 } 315 else if (activeWaves == 0 && EnemyCount() == 0) 316 { 317 level->waveEndTimer += gameTime.deltaTime; 318 if (level->waveEndTimer >= 2.0f) 319 { 320 level->nextState = LEVEL_STATE_WON_WAVE; 321 } 322 } 323 } 324 325 PathFindingMapUpdate(); 326 EnemyUpdate(); 327 TowerUpdate(); 328 ProjectileUpdate(); 329 ParticleUpdate(); 330 331 if (level->nextState == LEVEL_STATE_RESET) 332 { 333 InitLevel(level); 334 } 335 336 if (level->nextState == LEVEL_STATE_BATTLE) 337 { 338 InitBattleStateConditions(level); 339 } 340 341 if (level->nextState == LEVEL_STATE_WON_WAVE) 342 { 343 level->currentWave++; 344 level->state = LEVEL_STATE_WON_WAVE; 345 } 346 347 if (level->nextState == LEVEL_STATE_LOST_WAVE) 348 { 349 level->state = LEVEL_STATE_LOST_WAVE; 350 } 351 352 if (level->nextState == LEVEL_STATE_BUILDING) 353 { 354 level->state = LEVEL_STATE_BUILDING; 355 } 356 357 if (level->nextState == LEVEL_STATE_WON_LEVEL) 358 { 359 // make something of this later 360 InitLevel(level); 361 } 362 363 level->nextState = LEVEL_STATE_NONE; 364 } 365 366 float nextSpawnTime = 0.0f; 367 368 void ResetGame() 369 { 370 InitLevel(currentLevel); 371 } 372 373 void InitGame() 374 { 375 TowerInit(); 376 EnemyInit(); 377 ProjectileInit(); 378 ParticleInit(); 379 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f); 380 381 currentLevel = levels; 382 InitLevel(currentLevel); 383 } 384 385 //# Immediate GUI functions 386 387 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor) 388 { 389 const float healthBarWidth = 40.0f; 390 const float healthBarHeight = 6.0f; 391 const float healthBarOffset = 15.0f; 392 const float inset = 2.0f; 393 const float innerWidth = healthBarWidth - inset * 2; 394 const float innerHeight = healthBarHeight - inset * 2; 395 396 Vector2 screenPos = GetWorldToScreen(position, camera); 397 float centerX = screenPos.x - healthBarWidth * 0.5f; 398 float topY = screenPos.y - healthBarOffset; 399 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK); 400 float healthWidth = innerWidth * healthRatio; 401 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor); 402 } 403 404 int Button(const char *text, int x, int y, int width, int height, ButtonState *state) 405 { 406 Rectangle bounds = {x, y, width, height}; 407 int isPressed = 0; 408 int isSelected = state && state->isSelected; 409 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled) 410 { 411 Color color = isSelected ? DARKGRAY : GRAY; 412 DrawRectangle(x, y, width, height, color); 413 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) 414 { 415 isPressed = 1; 416 } 417 guiState.isBlocked = 1; 418 } 419 else 420 { 421 Color color = isSelected ? WHITE : LIGHTGRAY; 422 DrawRectangle(x, y, width, height, color); 423 } 424 Font font = GetFontDefault(); 425 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1); 426 Color textColor = state->isDisabled ? GRAY : BLACK; 427 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor); 428 return isPressed; 429 } 430 431 //# Main game loop 432 433 void GameUpdate() 434 { 435 float dt = GetFrameTime(); 436 // cap maximum delta time to 0.1 seconds to prevent large time steps 437 if (dt > 0.1f) dt = 0.1f; 438 gameTime.time += dt; 439 gameTime.deltaTime = dt; 440 441 UpdateLevel(currentLevel); 442 } 443 444 int main(void) 445 { 446 int screenWidth, screenHeight; 447 GetPreferredSize(&screenWidth, &screenHeight); 448 InitWindow(screenWidth, screenHeight, "Tower defense"); 449 SetTargetFPS(30); 450 451 InitGame(); 452 453 while (!WindowShouldClose()) 454 { 455 if (IsPaused()) { 456 // canvas is not visible in browser - do nothing 457 continue; 458 } 459 460 BeginDrawing(); 461 ClearBackground(DARKBLUE); 462 463 GameUpdate(); 464 DrawLevel(currentLevel); 465 466 EndDrawing(); 467 } 468 469 CloseWindow(); 470 471 return 0; 472 }
  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   float cooldown;
 41   float damage;
 42 } Tower;
 43 
 44 typedef struct GameTime
 45 {
 46   float time;
 47   float deltaTime;
 48 } GameTime;
 49 
 50 typedef struct ButtonState {
 51   char isSelected;
 52   char isDisabled;
 53 } ButtonState;
 54 
 55 typedef struct GUIState {
 56   int isBlocked;
 57 } GUIState;
 58 
 59 typedef enum LevelState
 60 {
 61   LEVEL_STATE_NONE,
 62   LEVEL_STATE_BUILDING,
 63   LEVEL_STATE_BATTLE,
 64   LEVEL_STATE_WON_WAVE,
 65   LEVEL_STATE_LOST_WAVE,
 66   LEVEL_STATE_WON_LEVEL,
 67   LEVEL_STATE_RESET,
 68 } LevelState;
 69 
 70 typedef struct EnemyWave {
 71   uint8_t enemyType;
 72   uint8_t wave;
 73   uint16_t count;
 74   float interval;
 75   float delay;
 76   Vector2 spawnPosition;
 77 
 78   uint16_t spawned;
 79   float timeToSpawnNext;
 80 } EnemyWave;
 81 
 82 typedef struct Level
 83 {
 84   LevelState state;
 85   LevelState nextState;
 86   Camera3D camera;
 87   int placementMode;
 88 
 89   int initialGold;
 90   int playerGold;
 91 
 92   EnemyWave waves[10];
 93   int currentWave;
 94   float waveEndTimer;
 95 } Level;
 96 
 97 typedef struct DeltaSrc
 98 {
 99   char x, y;
100 } DeltaSrc;
101 
102 typedef struct PathfindingMap
103 {
104   int width, height;
105   float scale;
106   float *distances;
107   long *towerIndex; 
108   DeltaSrc *deltaSrc;
109   float maxDistance;
110   Matrix toMapSpace;
111   Matrix toWorldSpace;
112 } PathfindingMap;
113 
114 // when we execute the pathfinding algorithm, we need to store the active nodes
115 // in a queue. Each node has a position, a distance from the start, and the
116 // position of the node that we came from.
117 typedef struct PathfindingNode
118 {
119   int16_t x, y, fromX, fromY;
120   float distance;
121 } PathfindingNode;
122 
123 typedef struct EnemyId
124 {
125   uint16_t index;
126   uint16_t generation;
127 } EnemyId;
128 
129 typedef struct EnemyClassConfig
130 {
131   float speed;
132   float health;
133   float radius;
134   float maxAcceleration;
135   float requiredContactTime;
136   float explosionDamage;
137   float explosionRange;
138   float explosionPushbackPower;
139   int goldValue;
140 } EnemyClassConfig;
141 
142 typedef struct Enemy
143 {
144   int16_t currentX, currentY;
145   int16_t nextX, nextY;
146   Vector2 simPosition;
147   Vector2 simVelocity;
148   uint16_t generation;
149   float startMovingTime;
150   float damage, futureDamage;
151   float contactTime;
152   uint8_t enemyType;
153   uint8_t movePathCount;
154   Vector2 movePath[ENEMY_MAX_PATH_COUNT];
155 } Enemy;
156 
157 #define PROJECTILE_MAX_COUNT 1200
158 #define PROJECTILE_TYPE_NONE 0
159 #define PROJECTILE_TYPE_BULLET 1
160 
161 typedef struct Projectile
162 {
163   uint8_t projectileType;
164   float shootTime;
165   float arrivalTime;
166   float damage;
167   Vector2 position;
168   Vector2 target;
169   Vector2 directionNormal;
170   EnemyId targetEnemy;
171 } Projectile;
172 
173 //# Function declarations
174 float TowerGetMaxHealth(Tower *tower);
175 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
176 int EnemyAddDamage(Enemy *enemy, float damage);
177 
178 //# Enemy functions
179 void EnemyInit();
180 void EnemyDraw();
181 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
182 void EnemyUpdate();
183 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
184 float EnemyGetMaxHealth(Enemy *enemy);
185 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
186 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
187 EnemyId EnemyGetId(Enemy *enemy);
188 Enemy *EnemyTryResolve(EnemyId enemyId);
189 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
190 int EnemyAddDamage(Enemy *enemy, float damage);
191 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
192 int EnemyCount();
193 void EnemyDrawHealthbars(Camera3D camera);
194 
195 //# Tower functions
196 void TowerInit();
197 Tower *TowerGetAt(int16_t x, int16_t y);
198 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
199 Tower *GetTowerByType(uint8_t towerType);
200 int GetTowerCosts(uint8_t towerType);
201 float TowerGetMaxHealth(Tower *tower);
202 void TowerDraw();
203 void TowerUpdate();
204 void TowerDrawHealthBars(Camera3D camera);
205 
206 //# Particles
207 void ParticleInit();
208 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
209 void ParticleUpdate();
210 void ParticleDraw();
211 
212 //# Projectiles
213 void ProjectileInit();
214 void ProjectileDraw();
215 void ProjectileUpdate();
216 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage);
217 
218 //# Pathfinding map
219 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
220 float PathFindingGetDistance(int mapX, int mapY);
221 Vector2 PathFindingGetGradient(Vector3 world);
222 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
223 void PathFindingMapUpdate();
224 void PathFindingMapDraw();
225 
226 //# UI
227 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor);
228 
229 //# variables
230 extern Level *currentLevel;
231 extern Enemy enemies[ENEMY_MAX_COUNT];
232 extern int enemyCount;
233 extern EnemyClassConfig enemyClassConfigs[];
234 
235 extern GUIState guiState;
236 extern GameTime gameTime;
237 extern Tower towers[TOWER_MAX_COUNT];
238 extern int towerCount;
239 
240 #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 void EnemyInit()
 24 {
 25   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 26   {
 27     enemies[i] = (Enemy){0};
 28   }
 29   enemyCount = 0;
 30 }
 31 
 32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 33 {
 34   return enemyClassConfigs[enemy->enemyType].speed;
 35 }
 36 
 37 float EnemyGetMaxHealth(Enemy *enemy)
 38 {
 39   return enemyClassConfigs[enemy->enemyType].health;
 40 }
 41 
 42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 43 {
 44   int16_t castleX = 0;
 45   int16_t castleY = 0;
 46   int16_t dx = castleX - currentX;
 47   int16_t dy = castleY - currentY;
 48   if (dx == 0 && dy == 0)
 49   {
 50     *nextX = currentX;
 51     *nextY = currentY;
 52     return 1;
 53   }
 54   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 55 
 56   if (gradient.x == 0 && gradient.y == 0)
 57   {
 58     *nextX = currentX;
 59     *nextY = currentY;
 60     return 1;
 61   }
 62 
 63   if (fabsf(gradient.x) > fabsf(gradient.y))
 64   {
 65     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 66     *nextY = currentY;
 67     return 0;
 68   }
 69   *nextX = currentX;
 70   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 71   return 0;
 72 }
 73 
 74 
 75 // this function predicts the movement of the unit for the next deltaT seconds
 76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 77 {
 78   const float pointReachedDistance = 0.25f;
 79   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 80   const float maxSimStepTime = 0.015625f;
 81   
 82   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 83   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 84   int16_t nextX = enemy->nextX;
 85   int16_t nextY = enemy->nextY;
 86   Vector2 position = enemy->simPosition;
 87   int passedCount = 0;
 88   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 89   {
 90     float stepTime = fminf(deltaT - t, maxSimStepTime);
 91     Vector2 target = (Vector2){nextX, nextY};
 92     float speed = Vector2Length(*velocity);
 93     // draw the target position for debugging
 94     DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
 95     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
 96     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
 97     {
 98       // we reached the target position, let's move to the next waypoint
 99       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100       target = (Vector2){nextX, nextY};
101       // track how many waypoints we passed
102       passedCount++;
103     }
104     
105     // acceleration towards the target
106     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108     *velocity = Vector2Add(*velocity, acceleration);
109 
110     // limit the speed to the maximum speed
111     if (speed > maxSpeed)
112     {
113       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114     }
115 
116     // move the enemy
117     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118   }
119 
120   if (waypointPassedCount)
121   {
122     (*waypointPassedCount) = passedCount;
123   }
124 
125   return position;
126 }
127 
128 void EnemyDraw()
129 {
130   for (int i = 0; i < enemyCount; i++)
131   {
132     Enemy enemy = enemies[i];
133     if (enemy.enemyType == ENEMY_TYPE_NONE)
134     {
135       continue;
136     }
137 
138     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139     
140     if (enemy.movePathCount > 0)
141     {
142       Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143       DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144     }
145     for (int j = 1; j < enemy.movePathCount; j++)
146     {
147       Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148       Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149       DrawLine3D(p, q, GREEN);
150     }
151 
152     switch (enemy.enemyType)
153     {
154     case ENEMY_TYPE_MINION:
155       DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156       break;
157     }
158   }
159 }
160 
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163   // damage the tower
164   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167   float explosionRange2 = explosionRange * explosionRange;
168   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169   // explode the enemy
170   if (tower->damage >= TowerGetMaxHealth(tower))
171   {
172     tower->towerType = TOWER_TYPE_NONE;
173   }
174 
175   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
176     explosionSource, 
177     (Vector3){0, 0.1f, 0}, 1.0f);
178 
179   enemy->enemyType = ENEMY_TYPE_NONE;
180 
181   // push back enemies & dealing damage
182   for (int i = 0; i < enemyCount; i++)
183   {
184     Enemy *other = &enemies[i];
185     if (other->enemyType == ENEMY_TYPE_NONE)
186     {
187       continue;
188     }
189     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190     if (distanceSqr > 0 && distanceSqr < explosionRange2)
191     {
192       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194       EnemyAddDamage(other, explosionDamge);
195     }
196   }
197 }
198 
199 void EnemyUpdate()
200 {
201   const float castleX = 0;
202   const float castleY = 0;
203   const float maxPathDistance2 = 0.25f * 0.25f;
204   
205   for (int i = 0; i < enemyCount; i++)
206   {
207     Enemy *enemy = &enemies[i];
208     if (enemy->enemyType == ENEMY_TYPE_NONE)
209     {
210       continue;
211     }
212 
213     int waypointPassedCount = 0;
214     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215     enemy->startMovingTime = gameTime.time;
216     // track path of unit
217     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218     {
219       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220       {
221         enemy->movePath[j] = enemy->movePath[j - 1];
222       }
223       enemy->movePath[0] = enemy->simPosition;
224       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225       {
226         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227       }
228     }
229 
230     if (waypointPassedCount > 0)
231     {
232       enemy->currentX = enemy->nextX;
233       enemy->currentY = enemy->nextY;
234       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236       {
237         // enemy reached the castle; remove it
238         enemy->enemyType = ENEMY_TYPE_NONE;
239         continue;
240       }
241     }
242   }
243 
244   // handle collisions between enemies
245   for (int i = 0; i < enemyCount - 1; i++)
246   {
247     Enemy *enemyA = &enemies[i];
248     if (enemyA->enemyType == ENEMY_TYPE_NONE)
249     {
250       continue;
251     }
252     for (int j = i + 1; j < enemyCount; j++)
253     {
254       Enemy *enemyB = &enemies[j];
255       if (enemyB->enemyType == ENEMY_TYPE_NONE)
256       {
257         continue;
258       }
259       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262       float radiusSum = radiusA + radiusB;
263       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264       {
265         // collision
266         float distance = sqrtf(distanceSqr);
267         float overlap = radiusSum - distance;
268         // move the enemies apart, but softly; if we have a clog of enemies,
269         // moving them perfectly apart can cause them to jitter
270         float positionCorrection = overlap / 5.0f;
271         Vector2 direction = (Vector2){
272             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276       }
277     }
278   }
279 
280   // handle collisions between enemies and towers
281   for (int i = 0; i < enemyCount; i++)
282   {
283     Enemy *enemy = &enemies[i];
284     if (enemy->enemyType == ENEMY_TYPE_NONE)
285     {
286       continue;
287     }
288     enemy->contactTime -= gameTime.deltaTime;
289     if (enemy->contactTime < 0.0f)
290     {
291       enemy->contactTime = 0.0f;
292     }
293 
294     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295     // linear search over towers; could be optimized by using path finding tower map,
296     // but for now, we keep it simple
297     for (int j = 0; j < towerCount; j++)
298     {
299       Tower *tower = &towers[j];
300       if (tower->towerType == TOWER_TYPE_NONE)
301       {
302         continue;
303       }
304       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306       if (distanceSqr > combinedRadius * combinedRadius)
307       {
308         continue;
309       }
310       // potential collision; square / circle intersection
311       float dx = tower->x - enemy->simPosition.x;
312       float dy = tower->y - enemy->simPosition.y;
313       float absDx = fabsf(dx);
314       float absDy = fabsf(dy);
315       Vector3 contactPoint = {0};
316       if (absDx <= 0.5f && absDx <= absDy) {
317         // vertical collision; push the enemy out horizontally
318         float overlap = enemyRadius + 0.5f - absDy;
319         if (overlap < 0.0f)
320         {
321           continue;
322         }
323         float direction = dy > 0.0f ? -1.0f : 1.0f;
324         enemy->simPosition.y += direction * overlap;
325         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
326       }
327       else if (absDy <= 0.5f && absDy <= absDx)
328       {
329         // horizontal collision; push the enemy out vertically
330         float overlap = enemyRadius + 0.5f - absDx;
331         if (overlap < 0.0f)
332         {
333           continue;
334         }
335         float direction = dx > 0.0f ? -1.0f : 1.0f;
336         enemy->simPosition.x += direction * overlap;
337         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
338       }
339       else
340       {
341         // possible collision with a corner
342         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
343         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
344         float cornerX = tower->x + cornerDX;
345         float cornerY = tower->y + cornerDY;
346         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
347         if (cornerDistanceSqr > enemyRadius * enemyRadius)
348         {
349           continue;
350         }
351         // push the enemy out along the diagonal
352         float cornerDistance = sqrtf(cornerDistanceSqr);
353         float overlap = enemyRadius - cornerDistance;
354         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
355         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
356         enemy->simPosition.x -= directionX * overlap;
357         enemy->simPosition.y -= directionY * overlap;
358         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
359       }
360 
361       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
362       {
363         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
364         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
365         {
366           EnemyTriggerExplode(enemy, tower, contactPoint);
367         }
368       }
369     }
370   }
371 }
372 
373 EnemyId EnemyGetId(Enemy *enemy)
374 {
375   return (EnemyId){enemy - enemies, enemy->generation};
376 }
377 
378 Enemy *EnemyTryResolve(EnemyId enemyId)
379 {
380   if (enemyId.index >= ENEMY_MAX_COUNT)
381   {
382     return 0;
383   }
384   Enemy *enemy = &enemies[enemyId.index];
385   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
386   {
387     return 0;
388   }
389   return enemy;
390 }
391 
392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
393 {
394   Enemy *spawn = 0;
395   for (int i = 0; i < enemyCount; i++)
396   {
397     Enemy *enemy = &enemies[i];
398     if (enemy->enemyType == ENEMY_TYPE_NONE)
399     {
400       spawn = enemy;
401       break;
402     }
403   }
404 
405   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
406   {
407     spawn = &enemies[enemyCount++];
408   }
409 
410   if (spawn)
411   {
412     spawn->currentX = currentX;
413     spawn->currentY = currentY;
414     spawn->nextX = currentX;
415     spawn->nextY = currentY;
416     spawn->simPosition = (Vector2){currentX, currentY};
417     spawn->simVelocity = (Vector2){0, 0};
418     spawn->enemyType = enemyType;
419     spawn->startMovingTime = gameTime.time;
420     spawn->damage = 0.0f;
421     spawn->futureDamage = 0.0f;
422     spawn->generation++;
423     spawn->movePathCount = 0;
424   }
425 
426   return spawn;
427 }
428 
429 int EnemyAddDamage(Enemy *enemy, float damage)
430 {
431   enemy->damage += damage;
432   if (enemy->damage >= EnemyGetMaxHealth(enemy))
433   {
434     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
435     enemy->enemyType = ENEMY_TYPE_NONE;
436     return 1;
437   }
438 
439   return 0;
440 }
441 
442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
443 {
444   int16_t castleX = 0;
445   int16_t castleY = 0;
446   Enemy* closest = 0;
447   int16_t closestDistance = 0;
448   float range2 = range * range;
449   for (int i = 0; i < enemyCount; i++)
450   {
451     Enemy* enemy = &enemies[i];
452     if (enemy->enemyType == ENEMY_TYPE_NONE)
453     {
454       continue;
455     }
456     float maxHealth = EnemyGetMaxHealth(enemy);
457     if (enemy->futureDamage >= maxHealth)
458     {
459       // ignore enemies that will die soon
460       continue;
461     }
462     int16_t dx = castleX - enemy->currentX;
463     int16_t dy = castleY - enemy->currentY;
464     int16_t distance = abs(dx) + abs(dy);
465     if (!closest || distance < closestDistance)
466     {
467       float tdx = towerX - enemy->currentX;
468       float tdy = towerY - enemy->currentY;
469       float tdistance2 = tdx * tdx + tdy * tdy;
470       if (tdistance2 <= range2)
471       {
472         closest = enemy;
473         closestDistance = distance;
474       }
475     }
476   }
477   return closest;
478 }
479 
480 int EnemyCount()
481 {
482   int count = 0;
483   for (int i = 0; i < enemyCount; i++)
484   {
485     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
486     {
487       count++;
488     }
489   }
490   return count;
491 }
492 
493 void EnemyDrawHealthbars(Camera3D camera)
494 {
495   for (int i = 0; i < enemyCount; i++)
496   {
497     Enemy *enemy = &enemies[i];
498     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
499     {
500       continue;
501     }
502     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
503     float maxHealth = EnemyGetMaxHealth(enemy);
504     float health = maxHealth - enemy->damage;
505     float healthRatio = health / maxHealth;
506     
507     DrawHealthBar(camera, position, healthRatio, GREEN);
508   }
509 }
  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     Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
 30     float x = position.x;
 31     float y = position.y;
 32     float dx = projectile.directionNormal.x;
 33     float dy = projectile.directionNormal.y;
 34     for (float d = 1.0f; d > 0.0f; d -= 0.25f)
 35     {
 36       x -= dx * 0.1f;
 37       y -= dy * 0.1f;
 38       float size = 0.1f * d;
 39       DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
 40     }
 41   }
 42 }
 43 
 44 void ProjectileUpdate()
 45 {
 46   for (int i = 0; i < projectileCount; i++)
 47   {
 48     Projectile *projectile = &projectiles[i];
 49     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 50     {
 51       continue;
 52     }
 53     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 54     if (transition >= 1.0f)
 55     {
 56       projectile->projectileType = PROJECTILE_TYPE_NONE;
 57       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 58       if (enemy)
 59       {
 60         EnemyAddDamage(enemy, projectile->damage);
 61       }
 62       continue;
 63     }
 64   }
 65 }
 66 
 67 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
 68 {
 69   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 70   {
 71     Projectile *projectile = &projectiles[i];
 72     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 73     {
 74       projectile->projectileType = projectileType;
 75       projectile->shootTime = gameTime.time;
 76       projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
 77       projectile->damage = damage;
 78       projectile->position = position;
 79       projectile->target = target;
 80       projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
 81       projectile->targetEnemy = EnemyGetId(enemy);
 82       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 83       return projectile;
 84     }
 85   }
 86   return 0;
 87 }
  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 Texture2D palette, spriteSheet;
  9 
 10 // a unit that uses sprites to be drawn
 11 typedef struct SpriteUnit
 12 {
 13   Rectangle srcRect;
 14   Vector2 offset;
 15 } SpriteUnit;
 16 
 17 // definition of our archer unit
 18 SpriteUnit archerUnit = {
 19     .srcRect = {0, 0, 16, 16},
 20     .offset = {7, 1},
 21 };
 22 
 23 void DrawSpriteUnit(SpriteUnit unit, Vector3 position)
 24 {
 25   Camera3D camera = currentLevel->camera;
 26   float size = 0.5f;
 27   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size };
 28   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
 29   // we want the sprite to face the camera, so we need to calculate the up vector
 30   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 31   Vector3 up = {0, 1, 0};
 32   Vector3 right = Vector3CrossProduct(forward, up);
 33   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 34   DrawBillboardPro(camera, spriteSheet, unit.srcRect, position, up, scale, offset, 0, WHITE);
 35 }
 36 
 37 void TowerInit()
 38 {
 39   for (int i = 0; i < TOWER_MAX_COUNT; i++)
 40   {
 41     towers[i] = (Tower){0};
 42   }
 43   towerCount = 0;
 44 
 45   // load a sprite sheet that contains all units
 46   spriteSheet = LoadTexture("data/spritesheet.png");
 47 
 48   // we'll use a palette texture to colorize the all buildings and environment art
 49   palette = LoadTexture("data/palette.png");
 50   // The texture uses gradients on very small space, so we'll enable bilinear filtering
 51   SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
 52 
 53   towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
 54 
 55   for (int i = 0; i < TOWER_TYPE_COUNT; i++)
 56   {
 57     if (towerModels[i].materials)
 58     {
 59       // assign the palette texture to the material of the model (0 is not used afaik)
 60       towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
 61     }
 62   }
 63 }
 64 
 65 static void TowerGunUpdate(Tower *tower)
 66 {
 67   if (tower->cooldown <= 0)
 68   {
 69     Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
 70     if (enemy)
 71     {
 72       tower->cooldown = 0.5f;
 73       // shoot the enemy; determine future position of the enemy
 74       float bulletSpeed = 1.0f;
 75       float bulletDamage = 3.0f;
 76       Vector2 velocity = enemy->simVelocity;
 77       Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
 78       Vector2 towerPosition = {tower->x, tower->y};
 79       float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
 80       for (int i = 0; i < 8; i++) {
 81         velocity = enemy->simVelocity;
 82         futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
 83         float distance = Vector2Distance(towerPosition, futurePosition);
 84         float eta2 = distance / bulletSpeed;
 85         if (fabs(eta - eta2) < 0.01f) {
 86           break;
 87         }
 88         eta = (eta2 + eta) * 0.5f;
 89       }
 90       ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition, 
 91         bulletSpeed, bulletDamage);
 92       enemy->futureDamage += bulletDamage;
 93     }
 94   }
 95   else
 96   {
 97     tower->cooldown -= gameTime.deltaTime;
 98   }
 99 }
100 
101 Tower *TowerGetAt(int16_t x, int16_t y)
102 {
103   for (int i = 0; i < towerCount; i++)
104   {
105     if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
106     {
107       return &towers[i];
108     }
109   }
110   return 0;
111 }
112 
113 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
114 {
115   if (towerCount >= TOWER_MAX_COUNT)
116   {
117     return 0;
118   }
119 
120   Tower *tower = TowerGetAt(x, y);
121   if (tower)
122   {
123     return 0;
124   }
125 
126   tower = &towers[towerCount++];
127   tower->x = x;
128   tower->y = y;
129   tower->towerType = towerType;
130   tower->cooldown = 0.0f;
131   tower->damage = 0.0f;
132   return tower;
133 }
134 
135 Tower *GetTowerByType(uint8_t towerType)
136 {
137   for (int i = 0; i < towerCount; i++)
138   {
139     if (towers[i].towerType == towerType)
140     {
141       return &towers[i];
142     }
143   }
144   return 0;
145 }
146 
147 int GetTowerCosts(uint8_t towerType)
148 {
149   switch (towerType)
150   {
151   case TOWER_TYPE_BASE:
152     return 0;
153   case TOWER_TYPE_GUN:
154     return 6;
155   case TOWER_TYPE_WALL:
156     return 2;
157   }
158   return 0;
159 }
160 
161 float TowerGetMaxHealth(Tower *tower)
162 {
163   switch (tower->towerType)
164   {
165   case TOWER_TYPE_BASE:
166     return 10.0f;
167   case TOWER_TYPE_GUN:
168     return 3.0f;
169   case TOWER_TYPE_WALL:
170     return 5.0f;
171   }
172   return 0.0f;
173 }
174 
175 void TowerDraw()
176 {
177   for (int i = 0; i < towerCount; i++)
178   {
179     Tower tower = towers[i];
180     if (tower.towerType == TOWER_TYPE_NONE)
181     {
182       continue;
183     }
184 
185     switch (tower.towerType)
186     {
187     case TOWER_TYPE_BASE:
188       DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
189       break;
190     case TOWER_TYPE_GUN:
191       DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
192       DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y});
193       break;
194     default:
195       if (towerModels[tower.towerType].materials)
196       {
197         DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
198       } else {
199         DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
200       }
201       break;
202     }
203   }
204 }
205 
206 void TowerUpdate()
207 {
208   for (int i = 0; i < towerCount; i++)
209   {
210     Tower *tower = &towers[i];
211     switch (tower->towerType)
212     {
213     case TOWER_TYPE_GUN:
214       TowerGunUpdate(tower);
215       break;
216     }
217   }
218 }
219 
220 void TowerDrawHealthBars(Camera3D camera)
221 {
222   for (int i = 0; i < towerCount; i++)
223   {
224     Tower *tower = &towers[i];
225     if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
226     {
227       continue;
228     }
229     
230     Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
231     float maxHealth = TowerGetMaxHealth(tower);
232     float health = maxHealth - tower->damage;
233     float healthRatio = health / maxHealth;
234     
235     DrawHealthBar(camera, position, healthRatio, GREEN);
236   }
237 }
  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 result looks like this:

Orthogonal view of the game.

This starts looking decent! The orthogonal view makes the sprites fit better with the 3D models. There is still much to do, but this tutorial step has grown quite long already, so I will pause here.

Wrap up

We have now added healthbars that are aligned with the enemy positions and added some basic graphics for the wall and tower buildings. The camera is now in orthogonal view, which makes the sprites fit better with the 3D models. The graphics are simple and low poly, and we have a few more to add in the next steps!

So in the next part, we will continue with tweaking the graphics and adding more enemy types and towers. There's lots of details to add and improve, so next week, we will continue with this. Until then, have fun!

🍪