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