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:
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:
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
This is what it looks like when placing the "gun" towers and starting a wave:
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:
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:
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