Simple tower defense tutorial, part 8: Animating units

In the previous part, we added healthbars to towers and enemies. But our enemies are only wired cubes so far. The archers on the towers are currently just simple billboards and they shoot fireballs.

So in this part, we want to change this: The enemies should be orcs and the archers should have a bow and a small shooting animation. The fireballs should be replaced by something that looks more like arrows and these arrows should follow a slight arc when shot.

Orcs

For the orcs, we want to use a simple sprite sheet animation. It will look like this:

Spritesheet with archer and orc walking animation

We will have 6 frames that we want to use for the walking animation. The weapon is will also be its own animation overlay, so we can later modify the unit - together with a shield and different head.

But let's start with a static billboard sprite for the orc. We move the SpriteUnit struct from the tower_system.c file to the td_main.h file and specify which sprite our orc is using in the enemy.c file. In the tower system, we increase the bullet speed and also use this new projectile type for the fired shots.

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

Click "Begin waves" to see the enemy spawning as orcs:

Again, a little screenshot:

Orcs are now the enemies.

Neat! But the archers still shoot sort of fireballs and the base is still a red cube. Let's change this to arrows and replace the base with a keep as well. We also introduce a projectile type for arrows in the td_main.h file. The projectiles should also follow a slight arc when shot - which will require us to use some math, but I'll explain this below.

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

This is what it looks like when placing the "gun" towers and starting a wave:

Arrows are now shot by the archers.

So far so good. Let's break down the indivdual steps to see how this is all achieved:

In the tower system, we load a model for the keep and deleted the code to draw the red cube:

  1 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");

Another minor change in the tower system is an increase of the bullet speet and spawning an arrow type projectile at a reasonable height that matches the archer's position.

The more complex change is that the projectiles follow a slight arc now - let's see how this is done.

The respective code part where this is hiding is this piece here:

  1 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
  2 {
  3   float t = transition + transitionOffset * 0.3f;
  4   if (t > 1.0f)
  5   {
  6     break;
  7   }
  8   Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
  9   Color color = RED;
 10   if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 11   {
 12     // make tip red but quickly fade to brown
 13     color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 14     // fake a ballista flight path using parabola equation
 15     t = t - 0.5f;
 16     t = 1.0f - 4.0f * t * t;
 17     position.y += 0.15f * t * projectile.distance;
 18   }
 19 
 20   float size = 0.06f * (transitionOffset + 0.25f);
 21   DrawCube(position, size, size, size, color);
 22 }

Quite a bit of code, so let's take it apart even more. Starting with this line here:

  1 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)

The reason for this loop is that we want to draw a trail of the projectile drawing a number of smaller cubes. The transitionOffset is the offset to each cube we draw - so this loop is handling the trail effect by repeatedly drawing the arrow cubes with slightly different position, size and color. The next line is this one:

  1 float t = transition + transitionOffset * 0.3f

The way the projectile is drawn is by interpolating between the start and end position of the projectile and its trail.

Value t is the concrete transition point at which we want to draw the projectile.

The "0.3f" is the offset to the transition value and means that we look 30% further into the future when drawing the projectile and its trail. So the end of the trail is the actual position of the projectile, while the start of the trail is 30% in the future. That also means, that the hit is registered when the trail's end is at the target position - which explains why the projectiles appear instantly at the archer together with the trail but fully hit the target only when the trail is at the target (you can observe this in the GIF). This could be fixed by adjusting the transition time, but for now, this is fine.

So to get the current position, we use a transition value that goes from 0 to 1. We use this value to linearly interpolate between the start and end position of the projectile. When the value is > 1, we break the loop, since the projectile has reached its target.

  1 if (t > 1.0f)
  2 {
  3   break; // nothing to draw anymore
  4 }
  5 Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);

But linear interpolation means, that the projectile would fly in a straight line. To make it look more like a flying arrow, we have to modify the y position of the projectile, reflecting the rise and fall of the arrow. We know that a flight path in vacuum is a parabola, so we can use the calculation t * t to get a parabola, though we have to adjust the values so it's peak is at the middle of the flight path, which happens here:

  1 if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
  2 {
  3   // make tip red but quickly fade to brown
  4   color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
  5   // fake a ballista flight path using parabola equation
  6   float parabolaT = t - 0.5f;
  7   parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
  8   position.y += 0.15f * parabolaT * projectile.distance;
  9 }

The coloring is simply the way we want to color the "arrow" - it starts brown and ends red. The color lerp is transitionOffset², just so that only the tip of the arrow is red but the rest quickly fades to brown. Just to clarify what I mean: If the transitionOffset is 0, the color is brown, when it is 0.5, the color is 25% red and 75% brown and when it is 1, the color is red.

The parabola calculation isn't too complex either: The assignment of t - 0.5f to parabolaT is done to center the peak of the parabola at the middle of the flight path. The range for that our variable parabolaT is therefore between -0.5 and 0.5.

When we run the calculation parabolaT * parabolaT, the result is a value that is therefore between 0 and 0.25 since 0.5 * 0.5 = 0.25. We want to have the peak at 1, so we multiply the result by 4 and negate it, because the parabola should point up, not down. We can plot this function, which could be expressed as

f(parabolaT) = 1 - 4 * parabolaT * parabolaT

and it looks like this:

-0.5 0.0 0.5 0.0 0.5 1.0 f(parabolaT) = 1 - 4 * parabolaT * parabolaT
Parabola function visualizaion; remember the question "Why do we need to know this math stuff?"... finally something where this school knowledge is good for!

To reiterate, for the transition value of 1 and 0, the result is 0, while at 0.5 it is 1.

We can now take this value and multiply it with the distance of the projectile, so the arc's height is proportional to the distance the projectile has to travel:

  1 position.y += 0.15f * parabolaT * projectile.distance

The constant 0.15f is used to scale the arc to a reasonable height: At the peak of its flight, the arrow is 15% higher than the distance it has to travel. If we don't use the distance, the arc would always be the same height, regardless of the distance the arrow has to travel - so for long distances, the arc would be nearly flat and for short distances, the arc would be comically high.

Since this might be all a bit complicated to imagine, here's an interactive example of how the arc changes with the distance and how it is applied to different start and end positions:

Interactive parabola arc visualization. Animated via Javascript + Canvas element

For an arcvalue of 0, the arc is flat - just like the linear interpolation would be, while a large value makes the arc go very high. At a later point, we may come back at this formula to adjust it. For example, when having a mortar tower that shoots in a very high arc, we may want to adjust the formula so the slowdown at the peak is more pronounced. This can be achieved by manipulating input value t.

But for now, we are done with the arc.

Archer & orc animation

Currently, the archers don't have bows or even arms right now and the orcs are just static sprites that float over the ground (should have used ghosts, I guess). So let's change this: The archers should have a bow and the orcs should have a walking animation.

First, we'll animating the orc movement. It's quite simple: based on the walked distance, we select the frame of the sprite sheet to draw. The frame selection is happening in the DrawSpriteUnit function in the tower_system.c file. It uses a few new fields in the SpriteUnit struct to obtain the frame count and the frame duration. These changes are done in the td_main.h file while the configuration of the orc sprite is done in the enemy.c file.

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

When you press "Begin waves", you can see the orcs walking animations - but without weapons. Remember that I removed the weapon from orc's spritesheet so we can later replace the weapons without having to change the animation frames of the orc. Let's quickly review these few scattered changes;

  1 // in td_main.h
  2 typedef struct Enemy
  3 {
  4   ...
  5   float walkedDistance;
  6   ...
  7 } Enemy;
  8 
  9 // a unit that uses sprites to be drawn
 10 typedef struct SpriteUnit
 11 {
 12   Rectangle srcRect;
 13   Vector2 offset;
 14   int frameCount;
 15   float frameDuration;
 16 } SpriteUnit;

The SpriteUnit struct formerly had only the srcRect and offset, but now we added the frameCount and frameDuration. For the walking animation, the frameDuration maps to walked distance per frame. This can be seen here:

  1  // enemy.c
  2 switch (enemy.enemyType)
  3 {
  4 case ENEMY_TYPE_MINION:
  5   DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y}, 
  6     enemy.walkedDistance);
  7   break;
  8 }

As pointed out above, the DrawSpriteUnit function is responsible for drawing the sprite, which is done by selecting the frame based on the walked distance:

  1 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t)
  2 {
  3   Camera3D camera = currentLevel->camera;
  4   float size = 0.5f;
  5   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size };
  6   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
  7   // we want the sprite to face the camera, so we need to calculate the up vector
  8   Vector3 forward = Vector3Subtract(camera.target, camera.position);
  9   Vector3 up = {0, 1, 0};
 10   Vector3 right = Vector3CrossProduct(forward, up);
 11   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 12 
 13   Rectangle srcRect = unit.srcRect;
 14   if (unit.frameCount > 1)
 15   {
 16     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 17   }
 18 
 19   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 20 }

All in all, this were not many changes to have a walking animation for the orcs.

Now for the archers, we'll have to extend the sprite unit struct a little more to support the carrying weapon information (the bow). We also have 2 different states for the archer: Loading and shooting. This is reflected in the phase variable.

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

One issue, however: The archer orientation is just one sided, so when the enemies come from the wrong side, the archers shoot the wrong way (they still hit the enemies, apparently they are highly competent, even if it looks incompetent :D).

Archers shoot the wrong way when the direction is flipped.
See: The archers shoot the wrong way when the enemies come from the "wrong" side.

The function already has a flipped argument that is currently not used. We also have to figure out when to flip it. The sprite is not a 3d model, so 3d world space coordinates are not meaningful: We have to look at the direction from the screen space perspective. But this is fairly simple; we'll use the same function as for the healthbars: GetWorldToScreen. We compare the archer's screen position with the enemy's screen position and flip the sprite if the enemy is to the right of the archer. 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   level->currentWave = 0;
 58 
 59   Camera *camera = &level->camera;
 60   camera->position = (Vector3){4.0f, 8.0f, 8.0f};
 61   camera->target = (Vector3){0.0f, 0.0f, 0.0f};
 62   camera->up = (Vector3){0.0f, 1.0f, 0.0f};
 63   camera->fovy = 10.0f;
 64   camera->projection = CAMERA_ORTHOGRAPHIC;
 65 }
 66 
 67 void DrawLevelHud(Level *level)
 68 {
 69   const char *text = TextFormat("Gold: %d", level->playerGold);
 70   Font font = GetFontDefault();
 71   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
 72   DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
 73 }
 74 
 75 void DrawLevelReportLostWave(Level *level)
 76 {
 77   BeginMode3D(level->camera);
 78   DrawGrid(10, 1.0f);
 79   TowerDraw();
 80   EnemyDraw();
 81   ProjectileDraw();
 82   ParticleDraw();
 83   guiState.isBlocked = 0;
 84   EndMode3D();
 85 
 86   TowerDrawHealthBars(level->camera);
 87 
 88   const char *text = "Wave lost";
 89   int textWidth = MeasureText(text, 20);
 90   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
 91 
 92   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
 93   {
 94     level->nextState = LEVEL_STATE_RESET;
 95   }
 96 }
 97 
 98 int HasLevelNextWave(Level *level)
 99 {
100   for (int i = 0; i < 10; i++)
101   {
102     EnemyWave *wave = &level->waves[i];
103     if (wave->wave == level->currentWave)
104     {
105       return 1;
106     }
107   }
108   return 0;
109 }
110 
111 void DrawLevelReportWonWave(Level *level)
112 {
113   BeginMode3D(level->camera);
114   DrawGrid(10, 1.0f);
115   TowerDraw();
116   EnemyDraw();
117   ProjectileDraw();
118   ParticleDraw();
119   guiState.isBlocked = 0;
120   EndMode3D();
121 
122   TowerDrawHealthBars(level->camera);
123 
124   const char *text = "Wave won";
125   int textWidth = MeasureText(text, 20);
126   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
127 
128 
129   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
130   {
131     level->nextState = LEVEL_STATE_RESET;
132   }
133 
134   if (HasLevelNextWave(level))
135   {
136     if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
137     {
138       level->nextState = LEVEL_STATE_BUILDING;
139     }
140   }
141   else {
142     if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
143     {
144       level->nextState = LEVEL_STATE_WON_LEVEL;
145     }
146   }
147 }
148 
149 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
150 {
151   static ButtonState buttonStates[8] = {0};
152   int cost = GetTowerCosts(towerType);
153   const char *text = TextFormat("%s: %d", name, cost);
154   buttonStates[towerType].isSelected = level->placementMode == towerType;
155   buttonStates[towerType].isDisabled = level->playerGold < cost;
156   if (Button(text, x, y, width, height, &buttonStates[towerType]))
157   {
158     level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
159   }
160 }
161 
162 void DrawLevelBuildingState(Level *level)
163 {
164   BeginMode3D(level->camera);
165   DrawGrid(10, 1.0f);
166   TowerDraw();
167   EnemyDraw();
168   ProjectileDraw();
169   ParticleDraw();
170 
171   Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
172   float planeDistance = ray.position.y / -ray.direction.y;
173   float planeX = ray.direction.x * planeDistance + ray.position.x;
174   float planeY = ray.direction.z * planeDistance + ray.position.z;
175   int16_t mapX = (int16_t)floorf(planeX + 0.5f);
176   int16_t mapY = (int16_t)floorf(planeY + 0.5f);
177   if (level->placementMode && !guiState.isBlocked)
178   {
179     DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
180     if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
181     {
182       if (TowerTryAdd(level->placementMode, mapX, mapY))
183       {
184         level->playerGold -= GetTowerCosts(level->placementMode);
185         level->placementMode = TOWER_TYPE_NONE;
186       }
187     }
188   }
189 
190   guiState.isBlocked = 0;
191 
192   EndMode3D();
193 
194   TowerDrawHealthBars(level->camera);
195 
196   static ButtonState buildWallButtonState = {0};
197   static ButtonState buildGunButtonState = {0};
198   buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
199   buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
200 
201   DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
202   DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
203 
204   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
205   {
206     level->nextState = LEVEL_STATE_RESET;
207   }
208   
209   if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
210   {
211     level->nextState = LEVEL_STATE_BATTLE;
212   }
213 
214   const char *text = "Building phase";
215   int textWidth = MeasureText(text, 20);
216   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
217 }
218 
219 void InitBattleStateConditions(Level *level)
220 {
221   level->state = LEVEL_STATE_BATTLE;
222   level->nextState = LEVEL_STATE_NONE;
223   level->waveEndTimer = 0.0f;
224   for (int i = 0; i < 10; i++)
225   {
226     EnemyWave *wave = &level->waves[i];
227     wave->spawned = 0;
228     wave->timeToSpawnNext = wave->delay;
229   }
230 }
231 
232 void DrawLevelBattleState(Level *level)
233 {
234   BeginMode3D(level->camera);
235   DrawGrid(10, 1.0f);
236   TowerDraw();
237   EnemyDraw();
238   ProjectileDraw();
239   ParticleDraw();
240   guiState.isBlocked = 0;
241   EndMode3D();
242 
243   EnemyDrawHealthbars(level->camera);
244   TowerDrawHealthBars(level->camera);
245 
246   if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
247   {
248     level->nextState = LEVEL_STATE_RESET;
249   }
250 
251   int maxCount = 0;
252   int remainingCount = 0;
253   for (int i = 0; i < 10; i++)
254   {
255     EnemyWave *wave = &level->waves[i];
256     if (wave->wave != level->currentWave)
257     {
258       continue;
259     }
260     maxCount += wave->count;
261     remainingCount += wave->count - wave->spawned;
262   }
263   int aliveCount = EnemyCount();
264   remainingCount += aliveCount;
265 
266   const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
267   int textWidth = MeasureText(text, 20);
268   DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
269 }
270 
271 void DrawLevel(Level *level)
272 {
273   switch (level->state)
274   {
275     case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
276     case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
277     case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
278     case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
279     default: break;
280   }
281 
282   DrawLevelHud(level);
283 }
284 
285 void UpdateLevel(Level *level)
286 {
287   if (level->state == LEVEL_STATE_BATTLE)
288   {
289     int activeWaves = 0;
290     for (int i = 0; i < 10; i++)
291     {
292       EnemyWave *wave = &level->waves[i];
293       if (wave->spawned >= wave->count || wave->wave != level->currentWave)
294       {
295         continue;
296       }
297       activeWaves++;
298       wave->timeToSpawnNext -= gameTime.deltaTime;
299       if (wave->timeToSpawnNext <= 0.0f)
300       {
301         Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
302         if (enemy)
303         {
304           wave->timeToSpawnNext = wave->interval;
305           wave->spawned++;
306         }
307       }
308     }
309     if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
310       level->waveEndTimer += gameTime.deltaTime;
311       if (level->waveEndTimer >= 2.0f)
312       {
313         level->nextState = LEVEL_STATE_LOST_WAVE;
314       }
315     }
316     else if (activeWaves == 0 && EnemyCount() == 0)
317     {
318       level->waveEndTimer += gameTime.deltaTime;
319       if (level->waveEndTimer >= 2.0f)
320       {
321         level->nextState = LEVEL_STATE_WON_WAVE;
322       }
323     }
324   }
325 
326   PathFindingMapUpdate();
327   EnemyUpdate();
328   TowerUpdate();
329   ProjectileUpdate();
330   ParticleUpdate();
331 
332   if (level->nextState == LEVEL_STATE_RESET)
333   {
334     InitLevel(level);
335   }
336   
337   if (level->nextState == LEVEL_STATE_BATTLE)
338   {
339     InitBattleStateConditions(level);
340   }
341   
342   if (level->nextState == LEVEL_STATE_WON_WAVE)
343   {
344     level->currentWave++;
345     level->state = LEVEL_STATE_WON_WAVE;
346   }
347   
348   if (level->nextState == LEVEL_STATE_LOST_WAVE)
349   {
350     level->state = LEVEL_STATE_LOST_WAVE;
351   }
352 
353   if (level->nextState == LEVEL_STATE_BUILDING)
354   {
355     level->state = LEVEL_STATE_BUILDING;
356   }
357 
358   if (level->nextState == LEVEL_STATE_WON_LEVEL)
359   {
360     // make something of this later
361     InitLevel(level);
362   }
363 
364   level->nextState = LEVEL_STATE_NONE;
365 }
366 
367 float nextSpawnTime = 0.0f;
368 
369 void ResetGame()
370 {
371   InitLevel(currentLevel);
372 }
373 
374 void InitGame()
375 {
376   TowerInit();
377   EnemyInit();
378   ProjectileInit();
379   ParticleInit();
380   PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
381 
382   currentLevel = levels;
383   InitLevel(currentLevel);
384 }
385 
386 //# Immediate GUI functions
387 
388 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth) 389 {
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 Vector2 lastTargetPosition;
41 float cooldown; 42 float damage; 43 } Tower; 44 45 typedef struct GameTime 46 { 47 float time; 48 float deltaTime; 49 } GameTime; 50 51 typedef struct ButtonState { 52 char isSelected; 53 char isDisabled; 54 } ButtonState; 55 56 typedef struct GUIState { 57 int isBlocked; 58 } GUIState; 59 60 typedef enum LevelState 61 { 62 LEVEL_STATE_NONE, 63 LEVEL_STATE_BUILDING, 64 LEVEL_STATE_BATTLE, 65 LEVEL_STATE_WON_WAVE, 66 LEVEL_STATE_LOST_WAVE, 67 LEVEL_STATE_WON_LEVEL, 68 LEVEL_STATE_RESET, 69 } LevelState; 70 71 typedef struct EnemyWave { 72 uint8_t enemyType; 73 uint8_t wave; 74 uint16_t count; 75 float interval; 76 float delay; 77 Vector2 spawnPosition; 78 79 uint16_t spawned; 80 float timeToSpawnNext; 81 } EnemyWave; 82 83 typedef struct Level 84 { 85 LevelState state; 86 LevelState nextState; 87 Camera3D camera; 88 int placementMode; 89 90 int initialGold; 91 int playerGold; 92 93 EnemyWave waves[10]; 94 int currentWave; 95 float waveEndTimer; 96 } Level; 97 98 typedef struct DeltaSrc 99 { 100 char x, y; 101 } DeltaSrc; 102 103 typedef struct PathfindingMap 104 { 105 int width, height; 106 float scale; 107 float *distances; 108 long *towerIndex; 109 DeltaSrc *deltaSrc; 110 float maxDistance; 111 Matrix toMapSpace; 112 Matrix toWorldSpace; 113 } PathfindingMap; 114 115 // when we execute the pathfinding algorithm, we need to store the active nodes 116 // in a queue. Each node has a position, a distance from the start, and the 117 // position of the node that we came from. 118 typedef struct PathfindingNode 119 { 120 int16_t x, y, fromX, fromY; 121 float distance; 122 } PathfindingNode; 123 124 typedef struct EnemyId 125 { 126 uint16_t index; 127 uint16_t generation; 128 } EnemyId; 129 130 typedef struct EnemyClassConfig 131 { 132 float speed; 133 float health; 134 float radius; 135 float maxAcceleration; 136 float requiredContactTime; 137 float explosionDamage; 138 float explosionRange; 139 float explosionPushbackPower; 140 int goldValue; 141 } EnemyClassConfig; 142 143 typedef struct Enemy 144 { 145 int16_t currentX, currentY; 146 int16_t nextX, nextY; 147 Vector2 simPosition; 148 Vector2 simVelocity; 149 uint16_t generation; 150 float walkedDistance; 151 float startMovingTime; 152 float damage, futureDamage; 153 float contactTime; 154 uint8_t enemyType; 155 uint8_t movePathCount; 156 Vector2 movePath[ENEMY_MAX_PATH_COUNT]; 157 } Enemy; 158 159 // a unit that uses sprites to be drawn 160 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0 161 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1 162 typedef struct SpriteUnit 163 { 164 Rectangle srcRect; 165 Vector2 offset; 166 int frameCount; 167 float frameDuration; 168 Rectangle srcWeaponIdleRect; 169 Vector2 srcWeaponIdleOffset; 170 Rectangle srcWeaponCooldownRect; 171 Vector2 srcWeaponCooldownOffset; 172 } SpriteUnit; 173 174 #define PROJECTILE_MAX_COUNT 1200 175 #define PROJECTILE_TYPE_NONE 0 176 #define PROJECTILE_TYPE_ARROW 1 177 178 typedef struct Projectile 179 { 180 uint8_t projectileType; 181 float shootTime; 182 float arrivalTime; 183 float distance; 184 float damage; 185 Vector3 position; 186 Vector3 target; 187 Vector3 directionNormal; 188 EnemyId targetEnemy; 189 } Projectile; 190 191 //# Function declarations 192 float TowerGetMaxHealth(Tower *tower); 193 int Button(const char *text, int x, int y, int width, int height, ButtonState *state); 194 int EnemyAddDamage(Enemy *enemy, float damage); 195 196 //# Enemy functions 197 void EnemyInit(); 198 void EnemyDraw(); 199 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource); 200 void EnemyUpdate(); 201 float EnemyGetCurrentMaxSpeed(Enemy *enemy); 202 float EnemyGetMaxHealth(Enemy *enemy); 203 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY); 204 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount); 205 EnemyId EnemyGetId(Enemy *enemy); 206 Enemy *EnemyTryResolve(EnemyId enemyId); 207 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY); 208 int EnemyAddDamage(Enemy *enemy, float damage); 209 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range); 210 int EnemyCount(); 211 void EnemyDrawHealthbars(Camera3D camera); 212 213 //# Tower functions 214 void TowerInit(); 215 Tower *TowerGetAt(int16_t x, int16_t y); 216 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y); 217 Tower *GetTowerByType(uint8_t towerType); 218 int GetTowerCosts(uint8_t towerType); 219 float TowerGetMaxHealth(Tower *tower); 220 void TowerDraw(); 221 void TowerUpdate(); 222 void TowerDrawHealthBars(Camera3D camera); 223 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase); 224 225 //# Particles 226 void ParticleInit(); 227 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime); 228 void ParticleUpdate(); 229 void ParticleDraw(); 230 231 //# Projectiles 232 void ProjectileInit(); 233 void ProjectileDraw(); 234 void ProjectileUpdate(); 235 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage); 236 237 //# Pathfinding map 238 void PathfindingMapInit(int width, int height, Vector3 translate, float scale); 239 float PathFindingGetDistance(int mapX, int mapY); 240 Vector2 PathFindingGetGradient(Vector3 world); 241 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY); 242 void PathFindingMapUpdate(); 243 void PathFindingMapDraw(); 244 245 //# UI
246 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
247 248 //# variables 249 extern Level *currentLevel; 250 extern Enemy enemies[ENEMY_MAX_COUNT]; 251 extern int enemyCount; 252 extern EnemyClassConfig enemyClassConfigs[]; 253 254 extern GUIState guiState; 255 extern GameTime gameTime; 256 extern Tower towers[TOWER_MAX_COUNT]; 257 extern int towerCount; 258 259 #endif
  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 // definition of our archer unit
 11 SpriteUnit archerUnit = {
 12     .srcRect = {0, 0, 16, 16},
 13     .offset = {7, 1},
 14     .frameCount = 1,
 15     .frameDuration = 0.0f,
 16     .srcWeaponIdleRect = {16, 0, 6, 16},
 17     .srcWeaponIdleOffset = {8, 0},
 18     .srcWeaponCooldownRect = {22, 0, 11, 16},
 19     .srcWeaponCooldownOffset = {10, 0},
 20 };
 21 
 22 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
 23 {
24 float xScale = flip ? -1.0f : 1.0f;
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 * xScale };
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 35 Rectangle srcRect = unit.srcRect; 36 if (unit.frameCount > 1) 37 { 38 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width; 39 }
40 if (flip) 41 { 42 srcRect.x += srcRect.width; 43 srcRect.width = -srcRect.width; 44 }
45 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE); 46 47 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0) 48 { 49 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size }; 50 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
51 srcRect = unit.srcWeaponCooldownRect; 52 if (flip) 53 { 54 // position.x = flip * scale.x * 0.5f; 55 srcRect.x += srcRect.width; 56 srcRect.width = -srcRect.width; 57 offset.x = scale.x - offset.x; 58 } 59 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
60 } 61 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0) 62 { 63 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size }; 64 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
65 srcRect = unit.srcWeaponIdleRect; 66 if (flip) 67 { 68 // position.x = flip * scale.x * 0.5f; 69 srcRect.x += srcRect.width; 70 srcRect.width = -srcRect.width; 71 offset.x = scale.x - offset.x; 72 } 73 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
74 } 75 } 76 77 void TowerInit() 78 { 79 for (int i = 0; i < TOWER_MAX_COUNT; i++) 80 { 81 towers[i] = (Tower){0}; 82 } 83 towerCount = 0; 84 85 // load a sprite sheet that contains all units
86 spriteSheet = LoadTexture("data/spritesheet.png"); 87 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
88 89 // we'll use a palette texture to colorize the all buildings and environment art 90 palette = LoadTexture("data/palette.png"); 91 // The texture uses gradients on very small space, so we'll enable bilinear filtering 92 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR); 93 94 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb"); 95 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb"); 96 97 for (int i = 0; i < TOWER_TYPE_COUNT; i++) 98 { 99 if (towerModels[i].materials) 100 { 101 // assign the palette texture to the material of the model (0 is not used afaik) 102 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette; 103 } 104 } 105 } 106 107 static void TowerGunUpdate(Tower *tower) 108 { 109 if (tower->cooldown <= 0) 110 { 111 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 112 if (enemy) 113 { 114 tower->cooldown = 0.5f; 115 // shoot the enemy; determine future position of the enemy 116 float bulletSpeed = 4.0f; 117 float bulletDamage = 3.0f; 118 Vector2 velocity = enemy->simVelocity; 119 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0); 120 Vector2 towerPosition = {tower->x, tower->y}; 121 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed; 122 for (int i = 0; i < 8; i++) { 123 velocity = enemy->simVelocity; 124 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0); 125 float distance = Vector2Distance(towerPosition, futurePosition); 126 float eta2 = distance / bulletSpeed; 127 if (fabs(eta - eta2) < 0.01f) { 128 break; 129 } 130 eta = (eta2 + eta) * 0.5f; 131 } 132 ProjectileTryAdd(PROJECTILE_TYPE_ARROW, enemy, 133 (Vector3){towerPosition.x, 1.33f, towerPosition.y}, 134 (Vector3){futurePosition.x, 0.25f, futurePosition.y}, 135 bulletSpeed, bulletDamage);
136 enemy->futureDamage += bulletDamage; 137 tower->lastTargetPosition = futurePosition;
138 } 139 } 140 else 141 { 142 tower->cooldown -= gameTime.deltaTime; 143 } 144 } 145 146 Tower *TowerGetAt(int16_t x, int16_t y) 147 { 148 for (int i = 0; i < towerCount; i++) 149 { 150 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE) 151 { 152 return &towers[i]; 153 } 154 } 155 return 0; 156 } 157 158 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 159 { 160 if (towerCount >= TOWER_MAX_COUNT) 161 { 162 return 0; 163 } 164 165 Tower *tower = TowerGetAt(x, y); 166 if (tower) 167 { 168 return 0; 169 } 170 171 tower = &towers[towerCount++]; 172 tower->x = x; 173 tower->y = y; 174 tower->towerType = towerType; 175 tower->cooldown = 0.0f; 176 tower->damage = 0.0f; 177 return tower; 178 } 179 180 Tower *GetTowerByType(uint8_t towerType) 181 { 182 for (int i = 0; i < towerCount; i++) 183 { 184 if (towers[i].towerType == towerType) 185 { 186 return &towers[i]; 187 } 188 } 189 return 0; 190 } 191 192 int GetTowerCosts(uint8_t towerType) 193 { 194 switch (towerType) 195 { 196 case TOWER_TYPE_BASE: 197 return 0; 198 case TOWER_TYPE_GUN: 199 return 6; 200 case TOWER_TYPE_WALL: 201 return 2; 202 } 203 return 0; 204 } 205 206 float TowerGetMaxHealth(Tower *tower) 207 { 208 switch (tower->towerType) 209 { 210 case TOWER_TYPE_BASE: 211 return 10.0f; 212 case TOWER_TYPE_GUN: 213 return 3.0f; 214 case TOWER_TYPE_WALL: 215 return 5.0f; 216 } 217 return 0.0f; 218 } 219 220 void TowerDraw() 221 { 222 for (int i = 0; i < towerCount; i++) 223 { 224 Tower tower = towers[i]; 225 if (tower.towerType == TOWER_TYPE_NONE) 226 { 227 continue; 228 } 229 230 switch (tower.towerType) 231 {
232 case TOWER_TYPE_GUN: 233 { 234 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera); 235 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
236 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
237 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x, 238 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE); 239 }
240 break; 241 default: 242 if (towerModels[tower.towerType].materials) 243 { 244 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE); 245 } else { 246 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY); 247 } 248 break; 249 } 250 } 251 } 252 253 void TowerUpdate() 254 { 255 for (int i = 0; i < towerCount; i++) 256 { 257 Tower *tower = &towers[i]; 258 switch (tower->towerType) 259 { 260 case TOWER_TYPE_GUN: 261 TowerGunUpdate(tower); 262 break; 263 } 264 } 265 } 266 267 void TowerDrawHealthBars(Camera3D camera) 268 { 269 for (int i = 0; i < towerCount; i++) 270 { 271 Tower *tower = &towers[i]; 272 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f) 273 { 274 continue; 275 } 276 277 Vector3 position = (Vector3){tower->x, 0.5f, tower->y}; 278 float maxHealth = TowerGetMaxHealth(tower); 279 float health = maxHealth - tower->damage; 280 float healthRatio = health / maxHealth; 281
282 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
283 } 284 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 #include <stdlib.h>
  4 #include <math.h>
  5 
  6 EnemyClassConfig enemyClassConfigs[] = {
  7     [ENEMY_TYPE_MINION] = {
  8       .health = 10.0f, 
  9       .speed = 0.6f, 
 10       .radius = 0.25f, 
 11       .maxAcceleration = 1.0f,
 12       .explosionDamage = 1.0f,
 13       .requiredContactTime = 0.5f,
 14       .explosionRange = 1.0f,
 15       .explosionPushbackPower = 0.25f,
 16       .goldValue = 1,
 17     },
 18 };
 19 
 20 Enemy enemies[ENEMY_MAX_COUNT];
 21 int enemyCount = 0;
 22 
 23 SpriteUnit enemySprites[] = {
 24     [ENEMY_TYPE_MINION] = {
 25       .srcRect = {0, 16, 16, 16},
 26       .offset = {8.0f, 0.0f},
 27       .frameCount = 6,
 28       .frameDuration = 0.1f,
 29     },
 30 };
 31 
 32 void EnemyInit()
 33 {
 34   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 35   {
 36     enemies[i] = (Enemy){0};
 37   }
 38   enemyCount = 0;
 39 }
 40 
 41 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
 42 {
 43   return enemyClassConfigs[enemy->enemyType].speed;
 44 }
 45 
 46 float EnemyGetMaxHealth(Enemy *enemy)
 47 {
 48   return enemyClassConfigs[enemy->enemyType].health;
 49 }
 50 
 51 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
 52 {
 53   int16_t castleX = 0;
 54   int16_t castleY = 0;
 55   int16_t dx = castleX - currentX;
 56   int16_t dy = castleY - currentY;
 57   if (dx == 0 && dy == 0)
 58   {
 59     *nextX = currentX;
 60     *nextY = currentY;
 61     return 1;
 62   }
 63   Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
 64 
 65   if (gradient.x == 0 && gradient.y == 0)
 66   {
 67     *nextX = currentX;
 68     *nextY = currentY;
 69     return 1;
 70   }
 71 
 72   if (fabsf(gradient.x) > fabsf(gradient.y))
 73   {
 74     *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
 75     *nextY = currentY;
 76     return 0;
 77   }
 78   *nextX = currentX;
 79   *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
 80   return 0;
 81 }
 82 
 83 
 84 // this function predicts the movement of the unit for the next deltaT seconds
 85 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
 86 {
 87   const float pointReachedDistance = 0.25f;
 88   const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
 89   const float maxSimStepTime = 0.015625f;
 90   
 91   float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
 92   float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
 93   int16_t nextX = enemy->nextX;
 94   int16_t nextY = enemy->nextY;
 95   Vector2 position = enemy->simPosition;
 96   int passedCount = 0;
 97   for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
 98   {
 99     float stepTime = fminf(deltaT - t, maxSimStepTime);
100     Vector2 target = (Vector2){nextX, nextY};
101     float speed = Vector2Length(*velocity);
102     // draw the target position for debugging
103     DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
104     Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
105     if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
106     {
107       // we reached the target position, let's move to the next waypoint
108       EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
109       target = (Vector2){nextX, nextY};
110       // track how many waypoints we passed
111       passedCount++;
112     }
113     
114     // acceleration towards the target
115     Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
116     Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
117     *velocity = Vector2Add(*velocity, acceleration);
118 
119     // limit the speed to the maximum speed
120     if (speed > maxSpeed)
121     {
122       *velocity = Vector2Scale(*velocity, maxSpeed / speed);
123     }
124 
125     // move the enemy
126     position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
127   }
128 
129   if (waypointPassedCount)
130   {
131     (*waypointPassedCount) = passedCount;
132   }
133 
134   return position;
135 }
136 
137 void EnemyDraw()
138 {
139   for (int i = 0; i < enemyCount; i++)
140   {
141     Enemy enemy = enemies[i];
142     if (enemy.enemyType == ENEMY_TYPE_NONE)
143     {
144       continue;
145     }
146 
147     Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
148     
149     // don't draw any trails for now; might replace this with footprints later
150     // if (enemy.movePathCount > 0)
151     // {
152     //   Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
153     //   DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
154     // }
155     // for (int j = 1; j < enemy.movePathCount; j++)
156     // {
157     //   Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
158     //   Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
159     //   DrawLine3D(p, q, GREEN);
160     // }
161 
162     switch (enemy.enemyType)
163     {
164     case ENEMY_TYPE_MINION:
165       DrawSpriteUnit(enemySprites[ENEMY_TYPE_MINION], (Vector3){position.x, 0.0f, position.y}, 
166         enemy.walkedDistance, 0, 0);
167       break;
168     }
169   }
170 }
171 
172 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
173 {
174   // damage the tower
175   float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
176   float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
177   float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
178   float explosionRange2 = explosionRange * explosionRange;
179   tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
180   // explode the enemy
181   if (tower->damage >= TowerGetMaxHealth(tower))
182   {
183     tower->towerType = TOWER_TYPE_NONE;
184   }
185 
186   ParticleAdd(PARTICLE_TYPE_EXPLOSION, 
187     explosionSource, 
188     (Vector3){0, 0.1f, 0}, 1.0f);
189 
190   enemy->enemyType = ENEMY_TYPE_NONE;
191 
192   // push back enemies & dealing damage
193   for (int i = 0; i < enemyCount; i++)
194   {
195     Enemy *other = &enemies[i];
196     if (other->enemyType == ENEMY_TYPE_NONE)
197     {
198       continue;
199     }
200     float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
201     if (distanceSqr > 0 && distanceSqr < explosionRange2)
202     {
203       Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
204       other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
205       EnemyAddDamage(other, explosionDamge);
206     }
207   }
208 }
209 
210 void EnemyUpdate()
211 {
212   const float castleX = 0;
213   const float castleY = 0;
214   const float maxPathDistance2 = 0.25f * 0.25f;
215   
216   for (int i = 0; i < enemyCount; i++)
217   {
218     Enemy *enemy = &enemies[i];
219     if (enemy->enemyType == ENEMY_TYPE_NONE)
220     {
221       continue;
222     }
223 
224     int waypointPassedCount = 0;
225     Vector2 prevPosition = enemy->simPosition;
226     enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
227     enemy->startMovingTime = gameTime.time;
228     enemy->walkedDistance += Vector2Distance(prevPosition, enemy->simPosition);
229     // track path of unit
230     if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
231     {
232       for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
233       {
234         enemy->movePath[j] = enemy->movePath[j - 1];
235       }
236       enemy->movePath[0] = enemy->simPosition;
237       if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
238       {
239         enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
240       }
241     }
242 
243     if (waypointPassedCount > 0)
244     {
245       enemy->currentX = enemy->nextX;
246       enemy->currentY = enemy->nextY;
247       if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
248         Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
249       {
250         // enemy reached the castle; remove it
251         enemy->enemyType = ENEMY_TYPE_NONE;
252         continue;
253       }
254     }
255   }
256 
257   // handle collisions between enemies
258   for (int i = 0; i < enemyCount - 1; i++)
259   {
260     Enemy *enemyA = &enemies[i];
261     if (enemyA->enemyType == ENEMY_TYPE_NONE)
262     {
263       continue;
264     }
265     for (int j = i + 1; j < enemyCount; j++)
266     {
267       Enemy *enemyB = &enemies[j];
268       if (enemyB->enemyType == ENEMY_TYPE_NONE)
269       {
270         continue;
271       }
272       float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
273       float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
274       float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
275       float radiusSum = radiusA + radiusB;
276       if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
277       {
278         // collision
279         float distance = sqrtf(distanceSqr);
280         float overlap = radiusSum - distance;
281         // move the enemies apart, but softly; if we have a clog of enemies,
282         // moving them perfectly apart can cause them to jitter
283         float positionCorrection = overlap / 5.0f;
284         Vector2 direction = (Vector2){
285             (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
286             (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
287         enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
288         enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
289       }
290     }
291   }
292 
293   // handle collisions between enemies and towers
294   for (int i = 0; i < enemyCount; i++)
295   {
296     Enemy *enemy = &enemies[i];
297     if (enemy->enemyType == ENEMY_TYPE_NONE)
298     {
299       continue;
300     }
301     enemy->contactTime -= gameTime.deltaTime;
302     if (enemy->contactTime < 0.0f)
303     {
304       enemy->contactTime = 0.0f;
305     }
306 
307     float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
308     // linear search over towers; could be optimized by using path finding tower map,
309     // but for now, we keep it simple
310     for (int j = 0; j < towerCount; j++)
311     {
312       Tower *tower = &towers[j];
313       if (tower->towerType == TOWER_TYPE_NONE)
314       {
315         continue;
316       }
317       float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
318       float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
319       if (distanceSqr > combinedRadius * combinedRadius)
320       {
321         continue;
322       }
323       // potential collision; square / circle intersection
324       float dx = tower->x - enemy->simPosition.x;
325       float dy = tower->y - enemy->simPosition.y;
326       float absDx = fabsf(dx);
327       float absDy = fabsf(dy);
328       Vector3 contactPoint = {0};
329       if (absDx <= 0.5f && absDx <= absDy) {
330         // vertical collision; push the enemy out horizontally
331         float overlap = enemyRadius + 0.5f - absDy;
332         if (overlap < 0.0f)
333         {
334           continue;
335         }
336         float direction = dy > 0.0f ? -1.0f : 1.0f;
337         enemy->simPosition.y += direction * overlap;
338         contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
339       }
340       else if (absDy <= 0.5f && absDy <= absDx)
341       {
342         // horizontal collision; push the enemy out vertically
343         float overlap = enemyRadius + 0.5f - absDx;
344         if (overlap < 0.0f)
345         {
346           continue;
347         }
348         float direction = dx > 0.0f ? -1.0f : 1.0f;
349         enemy->simPosition.x += direction * overlap;
350         contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
351       }
352       else
353       {
354         // possible collision with a corner
355         float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
356         float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
357         float cornerX = tower->x + cornerDX;
358         float cornerY = tower->y + cornerDY;
359         float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
360         if (cornerDistanceSqr > enemyRadius * enemyRadius)
361         {
362           continue;
363         }
364         // push the enemy out along the diagonal
365         float cornerDistance = sqrtf(cornerDistanceSqr);
366         float overlap = enemyRadius - cornerDistance;
367         float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
368         float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
369         enemy->simPosition.x -= directionX * overlap;
370         enemy->simPosition.y -= directionY * overlap;
371         contactPoint = (Vector3){cornerX, 0.2f, cornerY};
372       }
373 
374       if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
375       {
376         enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
377         if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
378         {
379           EnemyTriggerExplode(enemy, tower, contactPoint);
380         }
381       }
382     }
383   }
384 }
385 
386 EnemyId EnemyGetId(Enemy *enemy)
387 {
388   return (EnemyId){enemy - enemies, enemy->generation};
389 }
390 
391 Enemy *EnemyTryResolve(EnemyId enemyId)
392 {
393   if (enemyId.index >= ENEMY_MAX_COUNT)
394   {
395     return 0;
396   }
397   Enemy *enemy = &enemies[enemyId.index];
398   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
399   {
400     return 0;
401   }
402   return enemy;
403 }
404 
405 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
406 {
407   Enemy *spawn = 0;
408   for (int i = 0; i < enemyCount; i++)
409   {
410     Enemy *enemy = &enemies[i];
411     if (enemy->enemyType == ENEMY_TYPE_NONE)
412     {
413       spawn = enemy;
414       break;
415     }
416   }
417 
418   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
419   {
420     spawn = &enemies[enemyCount++];
421   }
422 
423   if (spawn)
424   {
425     spawn->currentX = currentX;
426     spawn->currentY = currentY;
427     spawn->nextX = currentX;
428     spawn->nextY = currentY;
429     spawn->simPosition = (Vector2){currentX, currentY};
430     spawn->simVelocity = (Vector2){0, 0};
431     spawn->enemyType = enemyType;
432     spawn->startMovingTime = gameTime.time;
433     spawn->damage = 0.0f;
434     spawn->futureDamage = 0.0f;
435     spawn->generation++;
436     spawn->movePathCount = 0;
437     spawn->walkedDistance = 0.0f;
438   }
439 
440   return spawn;
441 }
442 
443 int EnemyAddDamage(Enemy *enemy, float damage)
444 {
445   enemy->damage += damage;
446   if (enemy->damage >= EnemyGetMaxHealth(enemy))
447   {
448     currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
449     enemy->enemyType = ENEMY_TYPE_NONE;
450     return 1;
451   }
452 
453   return 0;
454 }
455 
456 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
457 {
458   int16_t castleX = 0;
459   int16_t castleY = 0;
460   Enemy* closest = 0;
461   int16_t closestDistance = 0;
462   float range2 = range * range;
463   for (int i = 0; i < enemyCount; i++)
464   {
465     Enemy* enemy = &enemies[i];
466     if (enemy->enemyType == ENEMY_TYPE_NONE)
467     {
468       continue;
469     }
470     float maxHealth = EnemyGetMaxHealth(enemy);
471     if (enemy->futureDamage >= maxHealth)
472     {
473       // ignore enemies that will die soon
474       continue;
475     }
476     int16_t dx = castleX - enemy->currentX;
477     int16_t dy = castleY - enemy->currentY;
478     int16_t distance = abs(dx) + abs(dy);
479     if (!closest || distance < closestDistance)
480     {
481       float tdx = towerX - enemy->currentX;
482       float tdy = towerY - enemy->currentY;
483       float tdistance2 = tdx * tdx + tdy * tdy;
484       if (tdistance2 <= range2)
485       {
486         closest = enemy;
487         closestDistance = distance;
488       }
489     }
490   }
491   return closest;
492 }
493 
494 int EnemyCount()
495 {
496   int count = 0;
497   for (int i = 0; i < enemyCount; i++)
498   {
499     if (enemies[i].enemyType != ENEMY_TYPE_NONE)
500     {
501       count++;
502     }
503   }
504   return count;
505 }
506 
507 void EnemyDrawHealthbars(Camera3D camera)
508 {
509   for (int i = 0; i < enemyCount; i++)
510   {
511     Enemy *enemy = &enemies[i];
512     if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
513     {
514       continue;
515     }
516     Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
517     float maxHealth = EnemyGetMaxHealth(enemy);
518     float health = maxHealth - enemy->damage;
519     float healthRatio = health / maxHealth;
520     
521 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
522 } 523 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 // The queue is a simple array of nodes, we add nodes to the end and remove
  5 // nodes from the front. We keep the array around to avoid unnecessary allocations
  6 static PathfindingNode *pathfindingNodeQueue = 0;
  7 static int pathfindingNodeQueueCount = 0;
  8 static int pathfindingNodeQueueCapacity = 0;
  9 
 10 // The pathfinding map stores the distances from the castle to each cell in the map.
 11 static PathfindingMap pathfindingMap = {0};
 12 
 13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
 14 {
 15   // transforming between map space and world space allows us to adapt 
 16   // position and scale of the map without changing the pathfinding data
 17   pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
 18   pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
 19   pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
 20   pathfindingMap.width = width;
 21   pathfindingMap.height = height;
 22   pathfindingMap.scale = scale;
 23   pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
 24   for (int i = 0; i < width * height; i++)
 25   {
 26     pathfindingMap.distances[i] = -1.0f;
 27   }
 28 
 29   pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
 30   pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
 31 }
 32 
 33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
 34 {
 35   if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
 36   {
 37     pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
 38     // we use MemAlloc/MemRealloc to allocate memory for the queue
 39     // I am not entirely sure if MemRealloc allows passing a null pointer
 40     // so we check if the pointer is null and use MemAlloc in that case
 41     if (pathfindingNodeQueue == 0)
 42     {
 43       pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 44     }
 45     else
 46     {
 47       pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
 48     }
 49   }
 50 
 51   PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
 52   node->x = x;
 53   node->y = y;
 54   node->fromX = fromX;
 55   node->fromY = fromY;
 56   node->distance = distance;
 57 }
 58 
 59 static PathfindingNode *PathFindingNodePop()
 60 {
 61   if (pathfindingNodeQueueCount == 0)
 62   {
 63     return 0;
 64   }
 65   // we return the first node in the queue; we want to return a pointer to the node
 66   // so we can return 0 if the queue is empty. 
 67   // We should _not_ return a pointer to the element in the list, because the list
 68   // may be reallocated and the pointer would become invalid. Or the 
 69   // popped element is overwritten by the next push operation.
 70   // Using static here means that the variable is permanently allocated.
 71   static PathfindingNode node;
 72   node = pathfindingNodeQueue[0];
 73   // we shift all nodes one position to the front
 74   for (int i = 1; i < pathfindingNodeQueueCount; i++)
 75   {
 76     pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
 77   }
 78   --pathfindingNodeQueueCount;
 79   return &node;
 80 }
 81 
 82 float PathFindingGetDistance(int mapX, int mapY)
 83 {
 84   if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
 85   {
 86     // when outside the map, we return the manhattan distance to the castle (0,0)
 87     return fabsf((float)mapX) + fabsf((float)mapY);
 88   }
 89 
 90   return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
 91 }
 92 
 93 // transform a world position to a map position in the array; 
 94 // returns true if the position is inside the map
 95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
 96 {
 97   Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
 98   *mapX = (int16_t)mapPosition.x;
 99   *mapY = (int16_t)mapPosition.z;
100   return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102 
103 void PathFindingMapUpdate()
104 {
105   const int castleX = 0, castleY = 0;
106   int16_t castleMapX, castleMapY;
107   if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108   {
109     return;
110   }
111   int width = pathfindingMap.width, height = pathfindingMap.height;
112 
113   // reset the distances to -1
114   for (int i = 0; i < width * height; i++)
115   {
116     pathfindingMap.distances[i] = -1.0f;
117   }
118   // reset the tower indices
119   for (int i = 0; i < width * height; i++)
120   {
121     pathfindingMap.towerIndex[i] = -1;
122   }
123   // reset the delta src
124   for (int i = 0; i < width * height; i++)
125   {
126     pathfindingMap.deltaSrc[i].x = 0;
127     pathfindingMap.deltaSrc[i].y = 0;
128   }
129 
130   for (int i = 0; i < towerCount; i++)
131   {
132     Tower *tower = &towers[i];
133     if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134     {
135       continue;
136     }
137     int16_t mapX, mapY;
138     // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139     // this would not work correctly and needs to be refined to allow towers covering multiple cells
140     // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141     // one cell. For now.
142     if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143     {
144       continue;
145     }
146     int index = mapY * width + mapX;
147     pathfindingMap.towerIndex[index] = i;
148   }
149 
150   // we start at the castle and add the castle to the queue
151   pathfindingMap.maxDistance = 0.0f;
152   pathfindingNodeQueueCount = 0;
153   PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154   PathfindingNode *node = 0;
155   while ((node = PathFindingNodePop()))
156   {
157     if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158     {
159       continue;
160     }
161     int index = node->y * width + node->x;
162     if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163     {
164       continue;
165     }
166 
167     int deltaX = node->x - node->fromX;
168     int deltaY = node->y - node->fromY;
169     // even if the cell is blocked by a tower, we still may want to store the direction
170     // (though this might not be needed, IDK right now)
171     pathfindingMap.deltaSrc[index].x = (char) deltaX;
172     pathfindingMap.deltaSrc[index].y = (char) deltaY;
173 
174     // we skip nodes that are blocked by towers
175     if (pathfindingMap.towerIndex[index] >= 0)
176     {
177       node->distance += 8.0f;
178     }
179     pathfindingMap.distances[index] = node->distance;
180     pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181     PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182     PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183     PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184     PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185   }
186 }
187 
188 void PathFindingMapDraw()
189 {
190   float cellSize = pathfindingMap.scale * 0.9f;
191   float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192   for (int x = 0; x < pathfindingMap.width; x++)
193   {
194     for (int y = 0; y < pathfindingMap.height; y++)
195     {
196       float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197       float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198       Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199       Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200       // animate the distance "wave" to show how the pathfinding algorithm expands
201       // from the castle
202       if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203       {
204         color = BLACK;
205       }
206       DrawCube(position, cellSize, 0.1f, cellSize, color);
207     }
208   }
209 }
210 
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213   int16_t mapX, mapY;
214   if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215   {
216     DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217     return (Vector2){(float)-delta.x, (float)-delta.y};
218   }
219   // fallback to a simple gradient calculation
220   float n = PathFindingGetDistance(mapX, mapY - 1);
221   float s = PathFindingGetDistance(mapX, mapY + 1);
222   float w = PathFindingGetDistance(mapX - 1, mapY);
223   float e = PathFindingGetDistance(mapX + 1, mapY);
224   return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
  5 static int projectileCount = 0;
  6 
  7 void ProjectileInit()
  8 {
  9   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 10   {
 11     projectiles[i] = (Projectile){0};
 12   }
 13 }
 14 
 15 void ProjectileDraw()
 16 {
 17   for (int i = 0; i < projectileCount; i++)
 18   {
 19     Projectile projectile = projectiles[i];
 20     if (projectile.projectileType == PROJECTILE_TYPE_NONE)
 21     {
 22       continue;
 23     }
 24     float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
 25     if (transition >= 1.0f)
 26     {
 27       continue;
 28     }
 29     for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
 30     {
 31       float t = transition + transitionOffset * 0.3f;
 32       if (t > 1.0f)
 33       {
 34         break;
 35       }
 36       Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
 37       Color color = RED;
 38       if (projectile.projectileType == PROJECTILE_TYPE_ARROW)
 39       {
 40         // make tip red but quickly fade to brown
 41         color = ColorLerp(BROWN, RED, transitionOffset * transitionOffset);
 42         // fake a ballista flight path using parabola equation
 43         float parabolaT = t - 0.5f;
 44         parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
 45         position.y += 0.15f * parabolaT * projectile.distance;
 46       }
 47 
 48       float size = 0.06f * (transitionOffset + 0.25f);
 49       DrawCube(position, size, size, size, color);
 50     }
 51   }
 52 }
 53 
 54 void ProjectileUpdate()
 55 {
 56   for (int i = 0; i < projectileCount; i++)
 57   {
 58     Projectile *projectile = &projectiles[i];
 59     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 60     {
 61       continue;
 62     }
 63     float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
 64     if (transition >= 1.0f)
 65     {
 66       projectile->projectileType = PROJECTILE_TYPE_NONE;
 67       Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
 68       if (enemy)
 69       {
 70         EnemyAddDamage(enemy, projectile->damage);
 71       }
 72       continue;
 73     }
 74   }
 75 }
 76 
 77 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, float damage)
 78 {
 79   for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
 80   {
 81     Projectile *projectile = &projectiles[i];
 82     if (projectile->projectileType == PROJECTILE_TYPE_NONE)
 83     {
 84       projectile->projectileType = projectileType;
 85       projectile->shootTime = gameTime.time;
 86       float distance = Vector3Distance(position, target);
 87       projectile->arrivalTime = gameTime.time + distance / speed;
 88       projectile->damage = damage;
 89       projectile->position = position;
 90       projectile->target = target;
 91       projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
 92       projectile->distance = distance;
 93       projectile->targetEnemy = EnemyGetId(enemy);
 94       projectileCount = projectileCount <= i ? i + 1 : projectileCount;
 95       return projectile;
 96     }
 97   }
 98   return 0;
 99 }
  1 #include "td_main.h"
  2 #include <raymath.h>
  3 
  4 static Particle particles[PARTICLE_MAX_COUNT];
  5 static int particleCount = 0;
  6 
  7 void ParticleInit()
  8 {
  9   for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
 10   {
 11     particles[i] = (Particle){0};
 12   }
 13   particleCount = 0;
 14 }
 15 
 16 static void DrawExplosionParticle(Particle *particle, float transition)
 17 {
 18   float size = 1.2f * (1.0f - transition);
 19   Color startColor = WHITE;
 20   Color endColor = RED;
 21   Color color = ColorLerp(startColor, endColor, transition);
 22   DrawCube(particle->position, size, size, size, color);
 23 }
 24 
 25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
 26 {
 27   if (particleCount >= PARTICLE_MAX_COUNT)
 28   {
 29     return;
 30   }
 31 
 32   int index = -1;
 33   for (int i = 0; i < particleCount; i++)
 34   {
 35     if (particles[i].particleType == PARTICLE_TYPE_NONE)
 36     {
 37       index = i;
 38       break;
 39     }
 40   }
 41 
 42   if (index == -1)
 43   {
 44     index = particleCount++;
 45   }
 46 
 47   Particle *particle = &particles[index];
 48   particle->particleType = particleType;
 49   particle->spawnTime = gameTime.time;
 50   particle->lifetime = lifetime;
 51   particle->position = position;
 52   particle->velocity = velocity;
 53 }
 54 
 55 void ParticleUpdate()
 56 {
 57   for (int i = 0; i < particleCount; i++)
 58   {
 59     Particle *particle = &particles[i];
 60     if (particle->particleType == PARTICLE_TYPE_NONE)
 61     {
 62       continue;
 63     }
 64 
 65     float age = gameTime.time - particle->spawnTime;
 66 
 67     if (particle->lifetime > age)
 68     {
 69       particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
 70     }
 71     else {
 72       particle->particleType = PARTICLE_TYPE_NONE;
 73     }
 74   }
 75 }
 76 
 77 void ParticleDraw()
 78 {
 79   for (int i = 0; i < particleCount; i++)
 80   {
 81     Particle particle = particles[i];
 82     if (particle.particleType == PARTICLE_TYPE_NONE)
 83     {
 84       continue;
 85     }
 86 
 87     float age = gameTime.time - particle.spawnTime;
 88     float transition = age / particle.lifetime;
 89     switch (particle.particleType)
 90     {
 91     case PARTICLE_TYPE_EXPLOSION:
 92       DrawExplosionParticle(&particle, transition);
 93       break;
 94     default:
 95       DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
 96       break;
 97     }
 98   }
 99 }
  1 #include "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 archer towers and start the game to see the archers shooting in the right direction.
Archers shoot in the right direction.
The archers are now shooting in the right direction

The major change here happens again in the DrawSpriteUnit function in the tower_system.c file:

  1 
  2 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
  3 {
  4   float xScale = flip ? -1.0f : 1.0f;
  5   Camera3D camera = currentLevel->camera;
  6   float size = 0.5f;
  7   Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
  8   Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
  9   // we want the sprite to face the camera, so we need to calculate the up vector
 10   Vector3 forward = Vector3Subtract(camera.target, camera.position);
 11   Vector3 up = {0, 1, 0};
 12   Vector3 right = Vector3CrossProduct(forward, up);
 13   up = Vector3Normalize(Vector3CrossProduct(right, forward));
 14 
 15   Rectangle srcRect = unit.srcRect;
 16   if (unit.frameCount > 1)
 17   {
 18     srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
 19   }
 20   if (flip)
 21   {
 22     srcRect.x += srcRect.width;
 23     srcRect.width = -srcRect.width;
 24   }
 25   DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 26 
 27   if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
 28   {
 29     offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
 30     scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
 31     srcRect = unit.srcWeaponCooldownRect;
 32     if (flip)
 33     {
 34       // position.x = flip * scale.x * 0.5f;
 35       srcRect.x += srcRect.width;
 36       srcRect.width = -srcRect.width;
 37       offset.x = scale.x - offset.x;
 38     }
 39     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 40   }
 41   else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
 42   {
 43     offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
 44     scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
 45     srcRect = unit.srcWeaponIdleRect;
 46     if (flip)
 47     {
 48       // position.x = flip * scale.x * 0.5f;
 49       srcRect.x += srcRect.width;
 50       srcRect.width = -srcRect.width;
 51       offset.x = scale.x - offset.x;
 52     }
 53     DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
 54   }
 55 }

The function has become quite long by now, though it's reasonably simple still; the flip argument is used to flip the sprite and we have to mirror the graphics by adjusting the source rectangle and its offset and width.

The xscale is set to -1 when the flip argument is true, so the sprite is mirrored. In consequence however, various offsets and widths have to be adjusted as well. In this version, I still have 2 codepaths for the weapon idle and weapon cooldown phase, but it could be worthwhile at a later point to merge the two codepaths into one by abstracting the data. I would leave it like this for now.

A minor change I did next to that: I also changed the spritesheet filtering from point to bilinear:

Spritesheet filtering is now bilinear.

Not an optimal choice; the problem with point filtering is that the sprites don't map 1:1 to the pixels on the screen and the result is that the pixels are sharp but sometimes doubled or missing. Bilinear filtering smooths the pixels, but the result is quite blurry - but it still looks better. For now, this is good enough.

At this point I'd like to defer the weapon animations and 3d decorations to the next post.

Wrap up

In this part, we continued adding graphics to the game. The archer now has a bow, shoots arrows that fly in a slight arc and the orcs have a walking animation. The archer's orientation is now correct and the spritesheet filtering is set to bilinear.

In the next part, we'll add more decorative elements to the game and make it look a bit more polished this way before looking into how to introduce more complex towers that use 3d models and animations.

In the meantime, I will also look into adding a makefile to the example zip files so compilation becomes easier.

🍪