Simple tower defense tutorial, part 7: Healthbars and first building graphics
After the refactoring in the last part, let's do something more visual again. It should still keep our overall goal in mind, so let's recap what we have so far:
- Buildings & enemies
- Enemies attacking buildings
- Walls blocking enemies
- Enemies getting damaged and removed
- Buildings getting damaged and removed
- Resources
- Level system with enemy wave definitions
- Simple UI to place buildings
- Principle level loop: build, watch, repeat
Let's compare this with what we ultimately want:
- Challenging levels:
- More enemy types
- More building types
- Proper balancing of enemy and tower strengths and rewards
Looking at the first sub point, it's clear that we can't continue with cubes. Different enemies means that we need different graphics for each enemy type. And stronger enemies means that our towers need more than one shot to kill them. To signal this to the player, we need healthbars.
This is why graphics and healthbars are the topic of this part. Let's start with the healthbars.
From world to screen
When we want to display a healthbar, we need to know where to draw it. The healthbar should be drawn above the enemy (or building) that it belongs to. The healthbar is drawn in 2D screen space, so we need to convert the 3D world position of the entity to 2D screen space coordinates.
Coordinate conversions are a common task in game development and we already have used the reverse conversion in the last parts: When we placed a building, we converted the mouse position to a ray in the 3D world space. This time, we want to convert a 3D world position to a 2D screen position.
Luckily, raylib provides a function for this: GetWorldToScreen. This function takes a 3D world position and returns the 2D screen position. The function is quite simple to use, see enemy.c, line 509:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Variables
7 GUIState guiState = {0};
8 GameTime gameTime = {0};
9
10 Level levels[] = {
11 [0] = {
12 .state = LEVEL_STATE_BUILDING,
13 .initialGold = 10,
14 .waves[0] = {
15 .enemyType = ENEMY_TYPE_MINION,
16 .wave = 0,
17 .count = 10,
18 .interval = 2.5f,
19 .delay = 1.0f,
20 .spawnPosition = {0, 6},
21 },
22 .waves[1] = {
23 .enemyType = ENEMY_TYPE_MINION,
24 .wave = 1,
25 .count = 20,
26 .interval = 1.5f,
27 .delay = 1.0f,
28 .spawnPosition = {0, 6},
29 },
30 .waves[2] = {
31 .enemyType = ENEMY_TYPE_MINION,
32 .wave = 2,
33 .count = 30,
34 .interval = 1.2f,
35 .delay = 1.0f,
36 .spawnPosition = {0, 6},
37 }
38 },
39 };
40
41 Level *currentLevel = levels;
42
43 //# Game
44
45 void InitLevel(Level *level)
46 {
47 TowerInit();
48 EnemyInit();
49 ProjectileInit();
50 ParticleInit();
51 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
52
53 level->placementMode = 0;
54 level->state = LEVEL_STATE_BUILDING;
55 level->nextState = LEVEL_STATE_NONE;
56 level->playerGold = level->initialGold;
57
58 Camera *camera = &level->camera;
59 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
60 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
61 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
62 camera->fovy = 45.0f;
63 camera->projection = CAMERA_PERSPECTIVE;
64 }
65
66 void DrawLevelHud(Level *level)
67 {
68 const char *text = TextFormat("Gold: %d", level->playerGold);
69 Font font = GetFontDefault();
70 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
71 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
72 }
73
74 void DrawLevelReportLostWave(Level *level)
75 {
76 BeginMode3D(level->camera);
77 DrawGrid(10, 1.0f);
78 TowerDraw();
79 EnemyDraw();
80 ProjectileDraw();
81 ParticleDraw();
82 guiState.isBlocked = 0;
83 EndMode3D();
84
85 const char *text = "Wave lost";
86 int textWidth = MeasureText(text, 20);
87 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
88
89 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
90 {
91 level->nextState = LEVEL_STATE_RESET;
92 }
93 }
94
95 int HasLevelNextWave(Level *level)
96 {
97 for (int i = 0; i < 10; i++)
98 {
99 EnemyWave *wave = &level->waves[i];
100 if (wave->wave == level->currentWave)
101 {
102 return 1;
103 }
104 }
105 return 0;
106 }
107
108 void DrawLevelReportWonWave(Level *level)
109 {
110 BeginMode3D(level->camera);
111 DrawGrid(10, 1.0f);
112 TowerDraw();
113 EnemyDraw();
114 ProjectileDraw();
115 ParticleDraw();
116 guiState.isBlocked = 0;
117 EndMode3D();
118
119 const char *text = "Wave won";
120 int textWidth = MeasureText(text, 20);
121 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
122
123
124 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
125 {
126 level->nextState = LEVEL_STATE_RESET;
127 }
128
129 if (HasLevelNextWave(level))
130 {
131 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
132 {
133 level->nextState = LEVEL_STATE_BUILDING;
134 }
135 }
136 else {
137 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
138 {
139 level->nextState = LEVEL_STATE_WON_LEVEL;
140 }
141 }
142 }
143
144 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
145 {
146 static ButtonState buttonStates[8] = {0};
147 int cost = GetTowerCosts(towerType);
148 const char *text = TextFormat("%s: %d", name, cost);
149 buttonStates[towerType].isSelected = level->placementMode == towerType;
150 buttonStates[towerType].isDisabled = level->playerGold < cost;
151 if (Button(text, x, y, width, height, &buttonStates[towerType]))
152 {
153 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
154 }
155 }
156
157 void DrawLevelBuildingState(Level *level)
158 {
159 BeginMode3D(level->camera);
160 DrawGrid(10, 1.0f);
161 TowerDraw();
162 EnemyDraw();
163 ProjectileDraw();
164 ParticleDraw();
165
166 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
167 float planeDistance = ray.position.y / -ray.direction.y;
168 float planeX = ray.direction.x * planeDistance + ray.position.x;
169 float planeY = ray.direction.z * planeDistance + ray.position.z;
170 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
171 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
172 if (level->placementMode && !guiState.isBlocked)
173 {
174 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
175 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
176 {
177 if (TowerTryAdd(level->placementMode, mapX, mapY))
178 {
179 level->playerGold -= GetTowerCosts(level->placementMode);
180 level->placementMode = TOWER_TYPE_NONE;
181 }
182 }
183 }
184
185 guiState.isBlocked = 0;
186
187 EndMode3D();
188
189 static ButtonState buildWallButtonState = {0};
190 static ButtonState buildGunButtonState = {0};
191 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
192 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
193
194 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
195 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
196
197 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
198 {
199 level->nextState = LEVEL_STATE_RESET;
200 }
201
202 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
203 {
204 level->nextState = LEVEL_STATE_BATTLE;
205 }
206
207 const char *text = "Building phase";
208 int textWidth = MeasureText(text, 20);
209 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
210 }
211
212 void InitBattleStateConditions(Level *level)
213 {
214 level->state = LEVEL_STATE_BATTLE;
215 level->nextState = LEVEL_STATE_NONE;
216 level->waveEndTimer = 0.0f;
217 for (int i = 0; i < 10; i++)
218 {
219 EnemyWave *wave = &level->waves[i];
220 wave->spawned = 0;
221 wave->timeToSpawnNext = wave->delay;
222 }
223 }
224
225 void DrawLevelBattleState(Level *level)
226 {
227 BeginMode3D(level->camera);
228 DrawGrid(10, 1.0f);
229 TowerDraw();
230 EnemyDraw();
231 ProjectileDraw();
232 ParticleDraw();
233 guiState.isBlocked = 0;
234 EndMode3D();
235
236 EnemyDrawHealthbars(level->camera);
237
238 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
239 {
240 level->nextState = LEVEL_STATE_RESET;
241 }
242
243 int maxCount = 0;
244 int remainingCount = 0;
245 for (int i = 0; i < 10; i++)
246 {
247 EnemyWave *wave = &level->waves[i];
248 if (wave->wave != level->currentWave)
249 {
250 continue;
251 }
252 maxCount += wave->count;
253 remainingCount += wave->count - wave->spawned;
254 }
255 int aliveCount = EnemyCount();
256 remainingCount += aliveCount;
257
258 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
259 int textWidth = MeasureText(text, 20);
260 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
261 }
262
263 void DrawLevel(Level *level)
264 {
265 switch (level->state)
266 {
267 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
268 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
269 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
270 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
271 default: break;
272 }
273
274 DrawLevelHud(level);
275 }
276
277 void UpdateLevel(Level *level)
278 {
279 if (level->state == LEVEL_STATE_BATTLE)
280 {
281 int activeWaves = 0;
282 for (int i = 0; i < 10; i++)
283 {
284 EnemyWave *wave = &level->waves[i];
285 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
286 {
287 continue;
288 }
289 activeWaves++;
290 wave->timeToSpawnNext -= gameTime.deltaTime;
291 if (wave->timeToSpawnNext <= 0.0f)
292 {
293 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
294 if (enemy)
295 {
296 wave->timeToSpawnNext = wave->interval;
297 wave->spawned++;
298 }
299 }
300 }
301 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
302 level->waveEndTimer += gameTime.deltaTime;
303 if (level->waveEndTimer >= 2.0f)
304 {
305 level->nextState = LEVEL_STATE_LOST_WAVE;
306 }
307 }
308 else if (activeWaves == 0 && EnemyCount() == 0)
309 {
310 level->waveEndTimer += gameTime.deltaTime;
311 if (level->waveEndTimer >= 2.0f)
312 {
313 level->nextState = LEVEL_STATE_WON_WAVE;
314 }
315 }
316 }
317
318 PathFindingMapUpdate();
319 EnemyUpdate();
320 TowerUpdate();
321 ProjectileUpdate();
322 ParticleUpdate();
323
324 if (level->nextState == LEVEL_STATE_RESET)
325 {
326 InitLevel(level);
327 }
328
329 if (level->nextState == LEVEL_STATE_BATTLE)
330 {
331 InitBattleStateConditions(level);
332 }
333
334 if (level->nextState == LEVEL_STATE_WON_WAVE)
335 {
336 level->currentWave++;
337 level->state = LEVEL_STATE_WON_WAVE;
338 }
339
340 if (level->nextState == LEVEL_STATE_LOST_WAVE)
341 {
342 level->state = LEVEL_STATE_LOST_WAVE;
343 }
344
345 if (level->nextState == LEVEL_STATE_BUILDING)
346 {
347 level->state = LEVEL_STATE_BUILDING;
348 }
349
350 if (level->nextState == LEVEL_STATE_WON_LEVEL)
351 {
352 // make something of this later
353 InitLevel(level);
354 }
355
356 level->nextState = LEVEL_STATE_NONE;
357 }
358
359 float nextSpawnTime = 0.0f;
360
361 void ResetGame()
362 {
363 InitLevel(currentLevel);
364 }
365
366 void InitGame()
367 {
368 TowerInit();
369 EnemyInit();
370 ProjectileInit();
371 ParticleInit();
372 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
373
374 currentLevel = levels;
375 InitLevel(currentLevel);
376 }
377
378 //# Immediate GUI functions
379
380 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
381 {
382 Rectangle bounds = {x, y, width, height};
383 int isPressed = 0;
384 int isSelected = state && state->isSelected;
385 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
386 {
387 Color color = isSelected ? DARKGRAY : GRAY;
388 DrawRectangle(x, y, width, height, color);
389 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
390 {
391 isPressed = 1;
392 }
393 guiState.isBlocked = 1;
394 }
395 else
396 {
397 Color color = isSelected ? WHITE : LIGHTGRAY;
398 DrawRectangle(x, y, width, height, color);
399 }
400 Font font = GetFontDefault();
401 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
402 Color textColor = state->isDisabled ? GRAY : BLACK;
403 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
404 return isPressed;
405 }
406
407 //# Main game loop
408
409 void GameUpdate()
410 {
411 float dt = GetFrameTime();
412 // cap maximum delta time to 0.1 seconds to prevent large time steps
413 if (dt > 0.1f) dt = 0.1f;
414 gameTime.time += dt;
415 gameTime.deltaTime = dt;
416
417 UpdateLevel(currentLevel);
418 }
419
420 int main(void)
421 {
422 int screenWidth, screenHeight;
423 GetPreferredSize(&screenWidth, &screenHeight);
424 InitWindow(screenWidth, screenHeight, "Tower defense");
425 SetTargetFPS(30);
426
427 InitGame();
428
429 while (!WindowShouldClose())
430 {
431 if (IsPaused()) {
432 // canvas is not visible in browser - do nothing
433 continue;
434 }
435
436 BeginDrawing();
437 ClearBackground(DARKBLUE);
438
439 GameUpdate();
440 DrawLevel(currentLevel);
441
442 EndDrawing();
443 }
444
445 CloseWindow();
446
447 return 0;
448 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 } Particle;
28
29 #define TOWER_MAX_COUNT 400
30 #define TOWER_TYPE_NONE 0
31 #define TOWER_TYPE_BASE 1
32 #define TOWER_TYPE_GUN 2
33 #define TOWER_TYPE_WALL 3
34
35 typedef struct Tower
36 {
37 int16_t x, y;
38 uint8_t towerType;
39 float cooldown;
40 float damage;
41 } Tower;
42
43 typedef struct GameTime
44 {
45 float time;
46 float deltaTime;
47 } GameTime;
48
49 typedef struct ButtonState {
50 char isSelected;
51 char isDisabled;
52 } ButtonState;
53
54 typedef struct GUIState {
55 int isBlocked;
56 } GUIState;
57
58 typedef enum LevelState
59 {
60 LEVEL_STATE_NONE,
61 LEVEL_STATE_BUILDING,
62 LEVEL_STATE_BATTLE,
63 LEVEL_STATE_WON_WAVE,
64 LEVEL_STATE_LOST_WAVE,
65 LEVEL_STATE_WON_LEVEL,
66 LEVEL_STATE_RESET,
67 } LevelState;
68
69 typedef struct EnemyWave {
70 uint8_t enemyType;
71 uint8_t wave;
72 uint16_t count;
73 float interval;
74 float delay;
75 Vector2 spawnPosition;
76
77 uint16_t spawned;
78 float timeToSpawnNext;
79 } EnemyWave;
80
81 typedef struct Level
82 {
83 LevelState state;
84 LevelState nextState;
85 Camera3D camera;
86 int placementMode;
87
88 int initialGold;
89 int playerGold;
90
91 EnemyWave waves[10];
92 int currentWave;
93 float waveEndTimer;
94 } Level;
95
96 typedef struct DeltaSrc
97 {
98 char x, y;
99 } DeltaSrc;
100
101 typedef struct PathfindingMap
102 {
103 int width, height;
104 float scale;
105 float *distances;
106 long *towerIndex;
107 DeltaSrc *deltaSrc;
108 float maxDistance;
109 Matrix toMapSpace;
110 Matrix toWorldSpace;
111 } PathfindingMap;
112
113 // when we execute the pathfinding algorithm, we need to store the active nodes
114 // in a queue. Each node has a position, a distance from the start, and the
115 // position of the node that we came from.
116 typedef struct PathfindingNode
117 {
118 int16_t x, y, fromX, fromY;
119 float distance;
120 } PathfindingNode;
121
122 typedef struct EnemyId
123 {
124 uint16_t index;
125 uint16_t generation;
126 } EnemyId;
127
128 typedef struct EnemyClassConfig
129 {
130 float speed;
131 float health;
132 float radius;
133 float maxAcceleration;
134 float requiredContactTime;
135 float explosionDamage;
136 float explosionRange;
137 float explosionPushbackPower;
138 int goldValue;
139 } EnemyClassConfig;
140
141 typedef struct Enemy
142 {
143 int16_t currentX, currentY;
144 int16_t nextX, nextY;
145 Vector2 simPosition;
146 Vector2 simVelocity;
147 uint16_t generation;
148 float startMovingTime;
149 float damage, futureDamage;
150 float contactTime;
151 uint8_t enemyType;
152 uint8_t movePathCount;
153 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
154 } Enemy;
155
156 #define PROJECTILE_MAX_COUNT 1200
157 #define PROJECTILE_TYPE_NONE 0
158 #define PROJECTILE_TYPE_BULLET 1
159
160 typedef struct Projectile
161 {
162 uint8_t projectileType;
163 float shootTime;
164 float arrivalTime;
165 float damage;
166 Vector2 position;
167 Vector2 target;
168 Vector2 directionNormal;
169 EnemyId targetEnemy;
170 } Projectile;
171
172 //# Function declarations
173 float TowerGetMaxHealth(Tower *tower);
174 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
175 int EnemyAddDamage(Enemy *enemy, float damage);
176
177 //# Enemy functions
178 void EnemyInit();
179 void EnemyDraw();
180 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
181 void EnemyUpdate();
182 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
183 float EnemyGetMaxHealth(Enemy *enemy);
184 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
185 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
186 EnemyId EnemyGetId(Enemy *enemy);
187 Enemy *EnemyTryResolve(EnemyId enemyId);
188 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
189 int EnemyAddDamage(Enemy *enemy, float damage);
190 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
191 int EnemyCount();
192 void EnemyDrawHealthbars(Camera3D camera);
193
194 //# Tower functions
195 void TowerInit();
196 Tower *TowerGetAt(int16_t x, int16_t y);
197 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
198 Tower *GetTowerByType(uint8_t towerType);
199 int GetTowerCosts(uint8_t towerType);
200 float TowerGetMaxHealth(Tower *tower);
201 void TowerDraw();
202 void TowerUpdate();
203
204 //# Particles
205 void ParticleInit();
206 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
207 void ParticleUpdate();
208 void ParticleDraw();
209
210 //# Projectiles
211 void ProjectileInit();
212 void ProjectileDraw();
213 void ProjectileUpdate();
214 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage);
215
216 //# Pathfinding map
217 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
218 float PathFindingGetDistance(int mapX, int mapY);
219 Vector2 PathFindingGetGradient(Vector3 world);
220 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
221 void PathFindingMapUpdate();
222 void PathFindingMapDraw();
223
224 //# variables
225 extern Level *currentLevel;
226 extern Enemy enemies[ENEMY_MAX_COUNT];
227 extern int enemyCount;
228 extern EnemyClassConfig enemyClassConfigs[];
229
230 extern GUIState guiState;
231 extern GameTime gameTime;
232 extern Tower towers[TOWER_MAX_COUNT];
233 extern int towerCount;
234
235 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 void EnemyInit()
24 {
25 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
26 {
27 enemies[i] = (Enemy){0};
28 }
29 enemyCount = 0;
30 }
31
32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
33 {
34 return enemyClassConfigs[enemy->enemyType].speed;
35 }
36
37 float EnemyGetMaxHealth(Enemy *enemy)
38 {
39 return enemyClassConfigs[enemy->enemyType].health;
40 }
41
42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
43 {
44 int16_t castleX = 0;
45 int16_t castleY = 0;
46 int16_t dx = castleX - currentX;
47 int16_t dy = castleY - currentY;
48 if (dx == 0 && dy == 0)
49 {
50 *nextX = currentX;
51 *nextY = currentY;
52 return 1;
53 }
54 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
55
56 if (gradient.x == 0 && gradient.y == 0)
57 {
58 *nextX = currentX;
59 *nextY = currentY;
60 return 1;
61 }
62
63 if (fabsf(gradient.x) > fabsf(gradient.y))
64 {
65 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
66 *nextY = currentY;
67 return 0;
68 }
69 *nextX = currentX;
70 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
71 return 0;
72 }
73
74
75 // this function predicts the movement of the unit for the next deltaT seconds
76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
77 {
78 const float pointReachedDistance = 0.25f;
79 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
80 const float maxSimStepTime = 0.015625f;
81
82 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
83 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
84 int16_t nextX = enemy->nextX;
85 int16_t nextY = enemy->nextY;
86 Vector2 position = enemy->simPosition;
87 int passedCount = 0;
88 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
89 {
90 float stepTime = fminf(deltaT - t, maxSimStepTime);
91 Vector2 target = (Vector2){nextX, nextY};
92 float speed = Vector2Length(*velocity);
93 // draw the target position for debugging
94 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
95 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
96 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
97 {
98 // we reached the target position, let's move to the next waypoint
99 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100 target = (Vector2){nextX, nextY};
101 // track how many waypoints we passed
102 passedCount++;
103 }
104
105 // acceleration towards the target
106 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108 *velocity = Vector2Add(*velocity, acceleration);
109
110 // limit the speed to the maximum speed
111 if (speed > maxSpeed)
112 {
113 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114 }
115
116 // move the enemy
117 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118 }
119
120 if (waypointPassedCount)
121 {
122 (*waypointPassedCount) = passedCount;
123 }
124
125 return position;
126 }
127
128 void EnemyDraw()
129 {
130 for (int i = 0; i < enemyCount; i++)
131 {
132 Enemy enemy = enemies[i];
133 if (enemy.enemyType == ENEMY_TYPE_NONE)
134 {
135 continue;
136 }
137
138 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139
140 if (enemy.movePathCount > 0)
141 {
142 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144 }
145 for (int j = 1; j < enemy.movePathCount; j++)
146 {
147 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149 DrawLine3D(p, q, GREEN);
150 }
151
152 switch (enemy.enemyType)
153 {
154 case ENEMY_TYPE_MINION:
155 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156 break;
157 }
158 }
159 }
160
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163 // damage the tower
164 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167 float explosionRange2 = explosionRange * explosionRange;
168 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169 // explode the enemy
170 if (tower->damage >= TowerGetMaxHealth(tower))
171 {
172 tower->towerType = TOWER_TYPE_NONE;
173 }
174
175 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
176 explosionSource,
177 (Vector3){0, 0.1f, 0}, 1.0f);
178
179 enemy->enemyType = ENEMY_TYPE_NONE;
180
181 // push back enemies & dealing damage
182 for (int i = 0; i < enemyCount; i++)
183 {
184 Enemy *other = &enemies[i];
185 if (other->enemyType == ENEMY_TYPE_NONE)
186 {
187 continue;
188 }
189 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190 if (distanceSqr > 0 && distanceSqr < explosionRange2)
191 {
192 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194 EnemyAddDamage(other, explosionDamge);
195 }
196 }
197 }
198
199 void EnemyUpdate()
200 {
201 const float castleX = 0;
202 const float castleY = 0;
203 const float maxPathDistance2 = 0.25f * 0.25f;
204
205 for (int i = 0; i < enemyCount; i++)
206 {
207 Enemy *enemy = &enemies[i];
208 if (enemy->enemyType == ENEMY_TYPE_NONE)
209 {
210 continue;
211 }
212
213 int waypointPassedCount = 0;
214 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215 enemy->startMovingTime = gameTime.time;
216 // track path of unit
217 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218 {
219 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220 {
221 enemy->movePath[j] = enemy->movePath[j - 1];
222 }
223 enemy->movePath[0] = enemy->simPosition;
224 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225 {
226 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227 }
228 }
229
230 if (waypointPassedCount > 0)
231 {
232 enemy->currentX = enemy->nextX;
233 enemy->currentY = enemy->nextY;
234 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236 {
237 // enemy reached the castle; remove it
238 enemy->enemyType = ENEMY_TYPE_NONE;
239 continue;
240 }
241 }
242 }
243
244 // handle collisions between enemies
245 for (int i = 0; i < enemyCount - 1; i++)
246 {
247 Enemy *enemyA = &enemies[i];
248 if (enemyA->enemyType == ENEMY_TYPE_NONE)
249 {
250 continue;
251 }
252 for (int j = i + 1; j < enemyCount; j++)
253 {
254 Enemy *enemyB = &enemies[j];
255 if (enemyB->enemyType == ENEMY_TYPE_NONE)
256 {
257 continue;
258 }
259 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262 float radiusSum = radiusA + radiusB;
263 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264 {
265 // collision
266 float distance = sqrtf(distanceSqr);
267 float overlap = radiusSum - distance;
268 // move the enemies apart, but softly; if we have a clog of enemies,
269 // moving them perfectly apart can cause them to jitter
270 float positionCorrection = overlap / 5.0f;
271 Vector2 direction = (Vector2){
272 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276 }
277 }
278 }
279
280 // handle collisions between enemies and towers
281 for (int i = 0; i < enemyCount; i++)
282 {
283 Enemy *enemy = &enemies[i];
284 if (enemy->enemyType == ENEMY_TYPE_NONE)
285 {
286 continue;
287 }
288 enemy->contactTime -= gameTime.deltaTime;
289 if (enemy->contactTime < 0.0f)
290 {
291 enemy->contactTime = 0.0f;
292 }
293
294 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295 // linear search over towers; could be optimized by using path finding tower map,
296 // but for now, we keep it simple
297 for (int j = 0; j < towerCount; j++)
298 {
299 Tower *tower = &towers[j];
300 if (tower->towerType == TOWER_TYPE_NONE)
301 {
302 continue;
303 }
304 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306 if (distanceSqr > combinedRadius * combinedRadius)
307 {
308 continue;
309 }
310 // potential collision; square / circle intersection
311 float dx = tower->x - enemy->simPosition.x;
312 float dy = tower->y - enemy->simPosition.y;
313 float absDx = fabsf(dx);
314 float absDy = fabsf(dy);
315 Vector3 contactPoint = {0};
316 if (absDx <= 0.5f && absDx <= absDy) {
317 // vertical collision; push the enemy out horizontally
318 float overlap = enemyRadius + 0.5f - absDy;
319 if (overlap < 0.0f)
320 {
321 continue;
322 }
323 float direction = dy > 0.0f ? -1.0f : 1.0f;
324 enemy->simPosition.y += direction * overlap;
325 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
326 }
327 else if (absDy <= 0.5f && absDy <= absDx)
328 {
329 // horizontal collision; push the enemy out vertically
330 float overlap = enemyRadius + 0.5f - absDx;
331 if (overlap < 0.0f)
332 {
333 continue;
334 }
335 float direction = dx > 0.0f ? -1.0f : 1.0f;
336 enemy->simPosition.x += direction * overlap;
337 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
338 }
339 else
340 {
341 // possible collision with a corner
342 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
343 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
344 float cornerX = tower->x + cornerDX;
345 float cornerY = tower->y + cornerDY;
346 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
347 if (cornerDistanceSqr > enemyRadius * enemyRadius)
348 {
349 continue;
350 }
351 // push the enemy out along the diagonal
352 float cornerDistance = sqrtf(cornerDistanceSqr);
353 float overlap = enemyRadius - cornerDistance;
354 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
355 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
356 enemy->simPosition.x -= directionX * overlap;
357 enemy->simPosition.y -= directionY * overlap;
358 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
359 }
360
361 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
362 {
363 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
364 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
365 {
366 EnemyTriggerExplode(enemy, tower, contactPoint);
367 }
368 }
369 }
370 }
371 }
372
373 EnemyId EnemyGetId(Enemy *enemy)
374 {
375 return (EnemyId){enemy - enemies, enemy->generation};
376 }
377
378 Enemy *EnemyTryResolve(EnemyId enemyId)
379 {
380 if (enemyId.index >= ENEMY_MAX_COUNT)
381 {
382 return 0;
383 }
384 Enemy *enemy = &enemies[enemyId.index];
385 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
386 {
387 return 0;
388 }
389 return enemy;
390 }
391
392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
393 {
394 Enemy *spawn = 0;
395 for (int i = 0; i < enemyCount; i++)
396 {
397 Enemy *enemy = &enemies[i];
398 if (enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 spawn = enemy;
401 break;
402 }
403 }
404
405 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
406 {
407 spawn = &enemies[enemyCount++];
408 }
409
410 if (spawn)
411 {
412 spawn->currentX = currentX;
413 spawn->currentY = currentY;
414 spawn->nextX = currentX;
415 spawn->nextY = currentY;
416 spawn->simPosition = (Vector2){currentX, currentY};
417 spawn->simVelocity = (Vector2){0, 0};
418 spawn->enemyType = enemyType;
419 spawn->startMovingTime = gameTime.time;
420 spawn->damage = 0.0f;
421 spawn->futureDamage = 0.0f;
422 spawn->generation++;
423 spawn->movePathCount = 0;
424 }
425
426 return spawn;
427 }
428
429 int EnemyAddDamage(Enemy *enemy, float damage)
430 {
431 enemy->damage += damage;
432 if (enemy->damage >= EnemyGetMaxHealth(enemy))
433 {
434 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
435 enemy->enemyType = ENEMY_TYPE_NONE;
436 return 1;
437 }
438
439 return 0;
440 }
441
442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
443 {
444 int16_t castleX = 0;
445 int16_t castleY = 0;
446 Enemy* closest = 0;
447 int16_t closestDistance = 0;
448 float range2 = range * range;
449 for (int i = 0; i < enemyCount; i++)
450 {
451 Enemy* enemy = &enemies[i];
452 if (enemy->enemyType == ENEMY_TYPE_NONE)
453 {
454 continue;
455 }
456 float maxHealth = EnemyGetMaxHealth(enemy);
457 if (enemy->futureDamage >= maxHealth)
458 {
459 // ignore enemies that will die soon
460 continue;
461 }
462 int16_t dx = castleX - enemy->currentX;
463 int16_t dy = castleY - enemy->currentY;
464 int16_t distance = abs(dx) + abs(dy);
465 if (!closest || distance < closestDistance)
466 {
467 float tdx = towerX - enemy->currentX;
468 float tdy = towerY - enemy->currentY;
469 float tdistance2 = tdx * tdx + tdy * tdy;
470 if (tdistance2 <= range2)
471 {
472 closest = enemy;
473 closestDistance = distance;
474 }
475 }
476 }
477 return closest;
478 }
479
480 int EnemyCount()
481 {
482 int count = 0;
483 for (int i = 0; i < enemyCount; i++)
484 {
485 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
486 {
487 count++;
488 }
489 }
490 return count;
491 }
492
493 void EnemyDrawHealthbars(Camera3D camera)
494 {
495 const float healthBarWidth = 40.0f;
496 const float healthBarHeight = 6.0f;
497 const float healthBarOffset = 15.0f;
498 const float inset = 2.0f;
499 const float innerWidth = healthBarWidth - inset * 2;
500 const float innerHeight = healthBarHeight - inset * 2;
501 for (int i = 0; i < enemyCount; i++)
502 {
503 Enemy *enemy = &enemies[i];
504 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
505 {
506 continue;
507 }
508 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
509 Vector2 screenPos = GetWorldToScreen(position, camera);
510 float maxHealth = EnemyGetMaxHealth(enemy);
511 float health = maxHealth - enemy->damage;
512 float healthRatio = health / maxHealth;
513 float centerX = screenPos.x - healthBarWidth * 0.5f;
514 float topY = screenPos.y - healthBarOffset;
515 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
516 float healthWidth = innerWidth * healthRatio;
517 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, GREEN);
518 }
519 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate()
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < towerCount; i++)
131 {
132 Tower *tower = &towers[i];
133 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134 {
135 continue;
136 }
137 int16_t mapX, mapY;
138 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139 // this would not work correctly and needs to be refined to allow towers covering multiple cells
140 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141 // one cell. For now.
142 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143 {
144 continue;
145 }
146 int index = mapY * width + mapX;
147 pathfindingMap.towerIndex[index] = i;
148 }
149
150 // we start at the castle and add the castle to the queue
151 pathfindingMap.maxDistance = 0.0f;
152 pathfindingNodeQueueCount = 0;
153 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154 PathfindingNode *node = 0;
155 while ((node = PathFindingNodePop()))
156 {
157 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158 {
159 continue;
160 }
161 int index = node->y * width + node->x;
162 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163 {
164 continue;
165 }
166
167 int deltaX = node->x - node->fromX;
168 int deltaY = node->y - node->fromY;
169 // even if the cell is blocked by a tower, we still may want to store the direction
170 // (though this might not be needed, IDK right now)
171 pathfindingMap.deltaSrc[index].x = (char) deltaX;
172 pathfindingMap.deltaSrc[index].y = (char) deltaY;
173
174 // we skip nodes that are blocked by towers
175 if (pathfindingMap.towerIndex[index] >= 0)
176 {
177 node->distance += 8.0f;
178 }
179 pathfindingMap.distances[index] = node->distance;
180 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185 }
186 }
187
188 void PathFindingMapDraw()
189 {
190 float cellSize = pathfindingMap.scale * 0.9f;
191 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192 for (int x = 0; x < pathfindingMap.width; x++)
193 {
194 for (int y = 0; y < pathfindingMap.height; y++)
195 {
196 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200 // animate the distance "wave" to show how the pathfinding algorithm expands
201 // from the castle
202 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203 {
204 color = BLACK;
205 }
206 DrawCube(position, cellSize, 0.1f, cellSize, color);
207 }
208 }
209 }
210
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213 int16_t mapX, mapY;
214 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215 {
216 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217 return (Vector2){(float)-delta.x, (float)-delta.y};
218 }
219 // fallback to a simple gradient calculation
220 float n = PathFindingGetDistance(mapX, mapY - 1);
221 float s = PathFindingGetDistance(mapX, mapY + 1);
222 float w = PathFindingGetDistance(mapX - 1, mapY);
223 float e = PathFindingGetDistance(mapX + 1, mapY);
224 return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 void ProjectileInit()
8 {
9 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
10 {
11 projectiles[i] = (Projectile){0};
12 }
13 }
14
15 void ProjectileDraw()
16 {
17 for (int i = 0; i < projectileCount; i++)
18 {
19 Projectile projectile = projectiles[i];
20 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
21 {
22 continue;
23 }
24 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
25 if (transition >= 1.0f)
26 {
27 continue;
28 }
29 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
30 float x = position.x;
31 float y = position.y;
32 float dx = projectile.directionNormal.x;
33 float dy = projectile.directionNormal.y;
34 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
35 {
36 x -= dx * 0.1f;
37 y -= dy * 0.1f;
38 float size = 0.1f * d;
39 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
40 }
41 }
42 }
43
44 void ProjectileUpdate()
45 {
46 for (int i = 0; i < projectileCount; i++)
47 {
48 Projectile *projectile = &projectiles[i];
49 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
50 {
51 continue;
52 }
53 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
54 if (transition >= 1.0f)
55 {
56 projectile->projectileType = PROJECTILE_TYPE_NONE;
57 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
58 if (enemy)
59 {
60 EnemyAddDamage(enemy, projectile->damage);
61 }
62 continue;
63 }
64 }
65 }
66
67 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
68 {
69 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
70 {
71 Projectile *projectile = &projectiles[i];
72 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
73 {
74 projectile->projectileType = projectileType;
75 projectile->shootTime = gameTime.time;
76 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
77 projectile->damage = damage;
78 projectile->position = position;
79 projectile->target = target;
80 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
81 projectile->targetEnemy = EnemyGetId(enemy);
82 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
83 return projectile;
84 }
85 }
86 return 0;
87 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Particle particles[PARTICLE_MAX_COUNT];
5 static int particleCount = 0;
6
7 void ParticleInit()
8 {
9 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
10 {
11 particles[i] = (Particle){0};
12 }
13 particleCount = 0;
14 }
15
16 static void DrawExplosionParticle(Particle *particle, float transition)
17 {
18 float size = 1.2f * (1.0f - transition);
19 Color startColor = WHITE;
20 Color endColor = RED;
21 Color color = ColorLerp(startColor, endColor, transition);
22 DrawCube(particle->position, size, size, size, color);
23 }
24
25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
26 {
27 if (particleCount >= PARTICLE_MAX_COUNT)
28 {
29 return;
30 }
31
32 int index = -1;
33 for (int i = 0; i < particleCount; i++)
34 {
35 if (particles[i].particleType == PARTICLE_TYPE_NONE)
36 {
37 index = i;
38 break;
39 }
40 }
41
42 if (index == -1)
43 {
44 index = particleCount++;
45 }
46
47 Particle *particle = &particles[index];
48 particle->particleType = particleType;
49 particle->spawnTime = gameTime.time;
50 particle->lifetime = lifetime;
51 particle->position = position;
52 particle->velocity = velocity;
53 }
54
55 void ParticleUpdate()
56 {
57 for (int i = 0; i < particleCount; i++)
58 {
59 Particle *particle = &particles[i];
60 if (particle->particleType == PARTICLE_TYPE_NONE)
61 {
62 continue;
63 }
64
65 float age = gameTime.time - particle->spawnTime;
66
67 if (particle->lifetime > age)
68 {
69 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
70 }
71 else {
72 particle->particleType = PARTICLE_TYPE_NONE;
73 }
74 }
75 }
76
77 void ParticleDraw()
78 {
79 for (int i = 0; i < particleCount; i++)
80 {
81 Particle particle = particles[i];
82 if (particle.particleType == PARTICLE_TYPE_NONE)
83 {
84 continue;
85 }
86
87 float age = gameTime.time - particle.spawnTime;
88 float transition = age / particle.lifetime;
89 switch (particle.particleType)
90 {
91 case PARTICLE_TYPE_EXPLOSION:
92 DrawExplosionParticle(&particle, transition);
93 break;
94 default:
95 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
96 break;
97 }
98 }
99 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 Tower towers[TOWER_MAX_COUNT];
5 int towerCount = 0;
6
7 void TowerInit()
8 {
9 for (int i = 0; i < TOWER_MAX_COUNT; i++)
10 {
11 towers[i] = (Tower){0};
12 }
13 towerCount = 0;
14 }
15
16 static void TowerGunUpdate(Tower *tower)
17 {
18 if (tower->cooldown <= 0)
19 {
20 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
21 if (enemy)
22 {
23 tower->cooldown = 0.5f;
24 // shoot the enemy; determine future position of the enemy
25 float bulletSpeed = 1.0f;
26 float bulletDamage = 3.0f;
27 Vector2 velocity = enemy->simVelocity;
28 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
29 Vector2 towerPosition = {tower->x, tower->y};
30 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
31 for (int i = 0; i < 8; i++) {
32 velocity = enemy->simVelocity;
33 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
34 float distance = Vector2Distance(towerPosition, futurePosition);
35 float eta2 = distance / bulletSpeed;
36 if (fabs(eta - eta2) < 0.01f) {
37 break;
38 }
39 eta = (eta2 + eta) * 0.5f;
40 }
41 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
42 bulletSpeed, bulletDamage);
43 enemy->futureDamage += bulletDamage;
44 }
45 }
46 else
47 {
48 tower->cooldown -= gameTime.deltaTime;
49 }
50 }
51
52 Tower *TowerGetAt(int16_t x, int16_t y)
53 {
54 for (int i = 0; i < towerCount; i++)
55 {
56 if (towers[i].x == x && towers[i].y == y)
57 {
58 return &towers[i];
59 }
60 }
61 return 0;
62 }
63
64 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
65 {
66 if (towerCount >= TOWER_MAX_COUNT)
67 {
68 return 0;
69 }
70
71 Tower *tower = TowerGetAt(x, y);
72 if (tower)
73 {
74 return 0;
75 }
76
77 tower = &towers[towerCount++];
78 tower->x = x;
79 tower->y = y;
80 tower->towerType = towerType;
81 tower->cooldown = 0.0f;
82 tower->damage = 0.0f;
83 return tower;
84 }
85
86 Tower *GetTowerByType(uint8_t towerType)
87 {
88 for (int i = 0; i < towerCount; i++)
89 {
90 if (towers[i].towerType == towerType)
91 {
92 return &towers[i];
93 }
94 }
95 return 0;
96 }
97
98 int GetTowerCosts(uint8_t towerType)
99 {
100 switch (towerType)
101 {
102 case TOWER_TYPE_BASE:
103 return 0;
104 case TOWER_TYPE_GUN:
105 return 6;
106 case TOWER_TYPE_WALL:
107 return 2;
108 }
109 return 0;
110 }
111
112 float TowerGetMaxHealth(Tower *tower)
113 {
114 switch (tower->towerType)
115 {
116 case TOWER_TYPE_BASE:
117 return 10.0f;
118 case TOWER_TYPE_GUN:
119 return 3.0f;
120 case TOWER_TYPE_WALL:
121 return 5.0f;
122 }
123 return 0.0f;
124 }
125
126 void TowerDraw()
127 {
128 for (int i = 0; i < towerCount; i++)
129 {
130 Tower tower = towers[i];
131 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
132 switch (tower.towerType)
133 {
134 case TOWER_TYPE_BASE:
135 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
136 break;
137 case TOWER_TYPE_GUN:
138 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
139 break;
140 case TOWER_TYPE_WALL:
141 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
142 break;
143 }
144 }
145 }
146
147 void TowerUpdate()
148 {
149 for (int i = 0; i < towerCount; i++)
150 {
151 Tower *tower = &towers[i];
152 switch (tower->towerType)
153 {
154 case TOWER_TYPE_GUN:
155 TowerGunUpdate(tower);
156 break;
157 }
158 }
159 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
When placing a tower and starting the wave, once the tower hits an enemy, the enemy's healthbar appears above the enemy:
To highlight once more the code that does this, here is the relevant part:
1 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
2 Vector2 screenPos = GetWorldToScreen(position, camera);
3 float maxHealth = EnemyGetMaxHealth(enemy);
4 float health = maxHealth - enemy->damage;
5 float healthRatio = health / maxHealth;
6 float centerX = screenPos.x - healthBarWidth * 0.5f;
7 float topY = screenPos.y - healthBarOffset;
8 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
9 float healthWidth = innerWidth * healthRatio;
10 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, GREEN);
This is pretty straight forward: We get the screen position of the enemy, calculate the health ratio and draw two rectangles: One black rectangle as the background and one green rectangle as the health bar itself. The green rectangle is scaled according to the health ratio.
Now we want to add a healthbar for the buildings as well. We could copy paste the code above and adapt it for the buildings, but we could also extract the logic for rendering the healthbar into a separate function. This way, we can reuse the code for both enemies and buildings, though we might need to adapt it slightly for each case. Let's do this:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Variables
7 GUIState guiState = {0};
8 GameTime gameTime = {0};
9
10 Level levels[] = {
11 [0] = {
12 .state = LEVEL_STATE_BUILDING,
13 .initialGold = 20,
14 .waves[0] = {
15 .enemyType = ENEMY_TYPE_MINION,
16 .wave = 0,
17 .count = 10,
18 .interval = 2.5f,
19 .delay = 1.0f,
20 .spawnPosition = {0, 6},
21 },
22 .waves[1] = {
23 .enemyType = ENEMY_TYPE_MINION,
24 .wave = 1,
25 .count = 20,
26 .interval = 1.5f,
27 .delay = 1.0f,
28 .spawnPosition = {0, 6},
29 },
30 .waves[2] = {
31 .enemyType = ENEMY_TYPE_MINION,
32 .wave = 2,
33 .count = 30,
34 .interval = 1.2f,
35 .delay = 1.0f,
36 .spawnPosition = {0, 6},
37 }
38 },
39 };
40
41 Level *currentLevel = levels;
42
43 //# Game
44
45 void InitLevel(Level *level)
46 {
47 TowerInit();
48 EnemyInit();
49 ProjectileInit();
50 ParticleInit();
51 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
52
53 level->placementMode = 0;
54 level->state = LEVEL_STATE_BUILDING;
55 level->nextState = LEVEL_STATE_NONE;
56 level->playerGold = level->initialGold;
57
58 Camera *camera = &level->camera;
59 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
60 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
61 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
62 camera->fovy = 45.0f;
63 camera->projection = CAMERA_PERSPECTIVE;
64 }
65
66 void DrawLevelHud(Level *level)
67 {
68 const char *text = TextFormat("Gold: %d", level->playerGold);
69 Font font = GetFontDefault();
70 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
71 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
72 }
73
74 void DrawLevelReportLostWave(Level *level)
75 {
76 BeginMode3D(level->camera);
77 DrawGrid(10, 1.0f);
78 TowerDraw();
79 EnemyDraw();
80 ProjectileDraw();
81 ParticleDraw();
82 guiState.isBlocked = 0;
83 EndMode3D();
84
85 TowerDrawHealthBars(level->camera);
86
87 const char *text = "Wave lost";
88 int textWidth = MeasureText(text, 20);
89 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
90
91 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
92 {
93 level->nextState = LEVEL_STATE_RESET;
94 }
95 }
96
97 int HasLevelNextWave(Level *level)
98 {
99 for (int i = 0; i < 10; i++)
100 {
101 EnemyWave *wave = &level->waves[i];
102 if (wave->wave == level->currentWave)
103 {
104 return 1;
105 }
106 }
107 return 0;
108 }
109
110 void DrawLevelReportWonWave(Level *level)
111 {
112 BeginMode3D(level->camera);
113 DrawGrid(10, 1.0f);
114 TowerDraw();
115 EnemyDraw();
116 ProjectileDraw();
117 ParticleDraw();
118 guiState.isBlocked = 0;
119 EndMode3D();
120
121 TowerDrawHealthBars(level->camera);
122
123 const char *text = "Wave won";
124 int textWidth = MeasureText(text, 20);
125 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
126
127
128 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
129 {
130 level->nextState = LEVEL_STATE_RESET;
131 }
132
133 if (HasLevelNextWave(level))
134 {
135 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
136 {
137 level->nextState = LEVEL_STATE_BUILDING;
138 }
139 }
140 else {
141 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
142 {
143 level->nextState = LEVEL_STATE_WON_LEVEL;
144 }
145 }
146 }
147
148 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
149 {
150 static ButtonState buttonStates[8] = {0};
151 int cost = GetTowerCosts(towerType);
152 const char *text = TextFormat("%s: %d", name, cost);
153 buttonStates[towerType].isSelected = level->placementMode == towerType;
154 buttonStates[towerType].isDisabled = level->playerGold < cost;
155 if (Button(text, x, y, width, height, &buttonStates[towerType]))
156 {
157 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
158 }
159 }
160
161 void DrawLevelBuildingState(Level *level)
162 {
163 BeginMode3D(level->camera);
164 DrawGrid(10, 1.0f);
165 TowerDraw();
166 EnemyDraw();
167 ProjectileDraw();
168 ParticleDraw();
169
170 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
171 float planeDistance = ray.position.y / -ray.direction.y;
172 float planeX = ray.direction.x * planeDistance + ray.position.x;
173 float planeY = ray.direction.z * planeDistance + ray.position.z;
174 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
175 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
176 if (level->placementMode && !guiState.isBlocked)
177 {
178 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
179 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
180 {
181 if (TowerTryAdd(level->placementMode, mapX, mapY))
182 {
183 level->playerGold -= GetTowerCosts(level->placementMode);
184 level->placementMode = TOWER_TYPE_NONE;
185 }
186 }
187 }
188
189 guiState.isBlocked = 0;
190
191 EndMode3D();
192
193 TowerDrawHealthBars(level->camera);
194
195 static ButtonState buildWallButtonState = {0};
196 static ButtonState buildGunButtonState = {0};
197 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
198 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
199
200 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
201 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
202
203 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
204 {
205 level->nextState = LEVEL_STATE_RESET;
206 }
207
208 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
209 {
210 level->nextState = LEVEL_STATE_BATTLE;
211 }
212
213 const char *text = "Building phase";
214 int textWidth = MeasureText(text, 20);
215 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
216 }
217
218 void InitBattleStateConditions(Level *level)
219 {
220 level->state = LEVEL_STATE_BATTLE;
221 level->nextState = LEVEL_STATE_NONE;
222 level->waveEndTimer = 0.0f;
223 for (int i = 0; i < 10; i++)
224 {
225 EnemyWave *wave = &level->waves[i];
226 wave->spawned = 0;
227 wave->timeToSpawnNext = wave->delay;
228 }
229 }
230
231 void DrawLevelBattleState(Level *level)
232 {
233 BeginMode3D(level->camera);
234 DrawGrid(10, 1.0f);
235 TowerDraw();
236 EnemyDraw();
237 ProjectileDraw();
238 ParticleDraw();
239 guiState.isBlocked = 0;
240 EndMode3D();
241
242 EnemyDrawHealthbars(level->camera);
243 TowerDrawHealthBars(level->camera);
244
245 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
246 {
247 level->nextState = LEVEL_STATE_RESET;
248 }
249
250 int maxCount = 0;
251 int remainingCount = 0;
252 for (int i = 0; i < 10; i++)
253 {
254 EnemyWave *wave = &level->waves[i];
255 if (wave->wave != level->currentWave)
256 {
257 continue;
258 }
259 maxCount += wave->count;
260 remainingCount += wave->count - wave->spawned;
261 }
262 int aliveCount = EnemyCount();
263 remainingCount += aliveCount;
264
265 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
266 int textWidth = MeasureText(text, 20);
267 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
268 }
269
270 void DrawLevel(Level *level)
271 {
272 switch (level->state)
273 {
274 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
275 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
276 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
277 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
278 default: break;
279 }
280
281 DrawLevelHud(level);
282 }
283
284 void UpdateLevel(Level *level)
285 {
286 if (level->state == LEVEL_STATE_BATTLE)
287 {
288 int activeWaves = 0;
289 for (int i = 0; i < 10; i++)
290 {
291 EnemyWave *wave = &level->waves[i];
292 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
293 {
294 continue;
295 }
296 activeWaves++;
297 wave->timeToSpawnNext -= gameTime.deltaTime;
298 if (wave->timeToSpawnNext <= 0.0f)
299 {
300 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
301 if (enemy)
302 {
303 wave->timeToSpawnNext = wave->interval;
304 wave->spawned++;
305 }
306 }
307 }
308 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
309 level->waveEndTimer += gameTime.deltaTime;
310 if (level->waveEndTimer >= 2.0f)
311 {
312 level->nextState = LEVEL_STATE_LOST_WAVE;
313 }
314 }
315 else if (activeWaves == 0 && EnemyCount() == 0)
316 {
317 level->waveEndTimer += gameTime.deltaTime;
318 if (level->waveEndTimer >= 2.0f)
319 {
320 level->nextState = LEVEL_STATE_WON_WAVE;
321 }
322 }
323 }
324
325 PathFindingMapUpdate();
326 EnemyUpdate();
327 TowerUpdate();
328 ProjectileUpdate();
329 ParticleUpdate();
330
331 if (level->nextState == LEVEL_STATE_RESET)
332 {
333 InitLevel(level);
334 }
335
336 if (level->nextState == LEVEL_STATE_BATTLE)
337 {
338 InitBattleStateConditions(level);
339 }
340
341 if (level->nextState == LEVEL_STATE_WON_WAVE)
342 {
343 level->currentWave++;
344 level->state = LEVEL_STATE_WON_WAVE;
345 }
346
347 if (level->nextState == LEVEL_STATE_LOST_WAVE)
348 {
349 level->state = LEVEL_STATE_LOST_WAVE;
350 }
351
352 if (level->nextState == LEVEL_STATE_BUILDING)
353 {
354 level->state = LEVEL_STATE_BUILDING;
355 }
356
357 if (level->nextState == LEVEL_STATE_WON_LEVEL)
358 {
359 // make something of this later
360 InitLevel(level);
361 }
362
363 level->nextState = LEVEL_STATE_NONE;
364 }
365
366 float nextSpawnTime = 0.0f;
367
368 void ResetGame()
369 {
370 InitLevel(currentLevel);
371 }
372
373 void InitGame()
374 {
375 TowerInit();
376 EnemyInit();
377 ProjectileInit();
378 ParticleInit();
379 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
380
381 currentLevel = levels;
382 InitLevel(currentLevel);
383 }
384
385 //# Immediate GUI functions
386
387 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor)
388 {
389 const float healthBarWidth = 40.0f;
390 const float healthBarHeight = 6.0f;
391 const float healthBarOffset = 15.0f;
392 const float inset = 2.0f;
393 const float innerWidth = healthBarWidth - inset * 2;
394 const float innerHeight = healthBarHeight - inset * 2;
395
396 Vector2 screenPos = GetWorldToScreen(position, camera);
397 float centerX = screenPos.x - healthBarWidth * 0.5f;
398 float topY = screenPos.y - healthBarOffset;
399 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
400 float healthWidth = innerWidth * healthRatio;
401 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
402 }
403
404 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
405 {
406 Rectangle bounds = {x, y, width, height};
407 int isPressed = 0;
408 int isSelected = state && state->isSelected;
409 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
410 {
411 Color color = isSelected ? DARKGRAY : GRAY;
412 DrawRectangle(x, y, width, height, color);
413 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
414 {
415 isPressed = 1;
416 }
417 guiState.isBlocked = 1;
418 }
419 else
420 {
421 Color color = isSelected ? WHITE : LIGHTGRAY;
422 DrawRectangle(x, y, width, height, color);
423 }
424 Font font = GetFontDefault();
425 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
426 Color textColor = state->isDisabled ? GRAY : BLACK;
427 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
428 return isPressed;
429 }
430
431 //# Main game loop
432
433 void GameUpdate()
434 {
435 float dt = GetFrameTime();
436 // cap maximum delta time to 0.1 seconds to prevent large time steps
437 if (dt > 0.1f) dt = 0.1f;
438 gameTime.time += dt;
439 gameTime.deltaTime = dt;
440
441 UpdateLevel(currentLevel);
442 }
443
444 int main(void)
445 {
446 int screenWidth, screenHeight;
447 GetPreferredSize(&screenWidth, &screenHeight);
448 InitWindow(screenWidth, screenHeight, "Tower defense");
449 SetTargetFPS(30);
450
451 InitGame();
452
453 while (!WindowShouldClose())
454 {
455 if (IsPaused()) {
456 // canvas is not visible in browser - do nothing
457 continue;
458 }
459
460 BeginDrawing();
461 ClearBackground(DARKBLUE);
462
463 GameUpdate();
464 DrawLevel(currentLevel);
465
466 EndDrawing();
467 }
468
469 CloseWindow();
470
471 return 0;
472 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 } Particle;
28
29 #define TOWER_MAX_COUNT 400
30 #define TOWER_TYPE_NONE 0
31 #define TOWER_TYPE_BASE 1
32 #define TOWER_TYPE_GUN 2
33 #define TOWER_TYPE_WALL 3
34
35 typedef struct Tower
36 {
37 int16_t x, y;
38 uint8_t towerType;
39 float cooldown;
40 float damage;
41 } Tower;
42
43 typedef struct GameTime
44 {
45 float time;
46 float deltaTime;
47 } GameTime;
48
49 typedef struct ButtonState {
50 char isSelected;
51 char isDisabled;
52 } ButtonState;
53
54 typedef struct GUIState {
55 int isBlocked;
56 } GUIState;
57
58 typedef enum LevelState
59 {
60 LEVEL_STATE_NONE,
61 LEVEL_STATE_BUILDING,
62 LEVEL_STATE_BATTLE,
63 LEVEL_STATE_WON_WAVE,
64 LEVEL_STATE_LOST_WAVE,
65 LEVEL_STATE_WON_LEVEL,
66 LEVEL_STATE_RESET,
67 } LevelState;
68
69 typedef struct EnemyWave {
70 uint8_t enemyType;
71 uint8_t wave;
72 uint16_t count;
73 float interval;
74 float delay;
75 Vector2 spawnPosition;
76
77 uint16_t spawned;
78 float timeToSpawnNext;
79 } EnemyWave;
80
81 typedef struct Level
82 {
83 LevelState state;
84 LevelState nextState;
85 Camera3D camera;
86 int placementMode;
87
88 int initialGold;
89 int playerGold;
90
91 EnemyWave waves[10];
92 int currentWave;
93 float waveEndTimer;
94 } Level;
95
96 typedef struct DeltaSrc
97 {
98 char x, y;
99 } DeltaSrc;
100
101 typedef struct PathfindingMap
102 {
103 int width, height;
104 float scale;
105 float *distances;
106 long *towerIndex;
107 DeltaSrc *deltaSrc;
108 float maxDistance;
109 Matrix toMapSpace;
110 Matrix toWorldSpace;
111 } PathfindingMap;
112
113 // when we execute the pathfinding algorithm, we need to store the active nodes
114 // in a queue. Each node has a position, a distance from the start, and the
115 // position of the node that we came from.
116 typedef struct PathfindingNode
117 {
118 int16_t x, y, fromX, fromY;
119 float distance;
120 } PathfindingNode;
121
122 typedef struct EnemyId
123 {
124 uint16_t index;
125 uint16_t generation;
126 } EnemyId;
127
128 typedef struct EnemyClassConfig
129 {
130 float speed;
131 float health;
132 float radius;
133 float maxAcceleration;
134 float requiredContactTime;
135 float explosionDamage;
136 float explosionRange;
137 float explosionPushbackPower;
138 int goldValue;
139 } EnemyClassConfig;
140
141 typedef struct Enemy
142 {
143 int16_t currentX, currentY;
144 int16_t nextX, nextY;
145 Vector2 simPosition;
146 Vector2 simVelocity;
147 uint16_t generation;
148 float startMovingTime;
149 float damage, futureDamage;
150 float contactTime;
151 uint8_t enemyType;
152 uint8_t movePathCount;
153 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
154 } Enemy;
155
156 #define PROJECTILE_MAX_COUNT 1200
157 #define PROJECTILE_TYPE_NONE 0
158 #define PROJECTILE_TYPE_BULLET 1
159
160 typedef struct Projectile
161 {
162 uint8_t projectileType;
163 float shootTime;
164 float arrivalTime;
165 float damage;
166 Vector2 position;
167 Vector2 target;
168 Vector2 directionNormal;
169 EnemyId targetEnemy;
170 } Projectile;
171
172 //# Function declarations
173 float TowerGetMaxHealth(Tower *tower);
174 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
175 int EnemyAddDamage(Enemy *enemy, float damage);
176
177 //# Enemy functions
178 void EnemyInit();
179 void EnemyDraw();
180 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
181 void EnemyUpdate();
182 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
183 float EnemyGetMaxHealth(Enemy *enemy);
184 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
185 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
186 EnemyId EnemyGetId(Enemy *enemy);
187 Enemy *EnemyTryResolve(EnemyId enemyId);
188 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
189 int EnemyAddDamage(Enemy *enemy, float damage);
190 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
191 int EnemyCount();
192 void EnemyDrawHealthbars(Camera3D camera);
193
194 //# Tower functions
195 void TowerInit();
196 Tower *TowerGetAt(int16_t x, int16_t y);
197 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
198 Tower *GetTowerByType(uint8_t towerType);
199 int GetTowerCosts(uint8_t towerType);
200 float TowerGetMaxHealth(Tower *tower);
201 void TowerDraw();
202 void TowerUpdate();
203 void TowerDrawHealthBars(Camera3D camera);
204
205 //# Particles
206 void ParticleInit();
207 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
208 void ParticleUpdate();
209 void ParticleDraw();
210
211 //# Projectiles
212 void ProjectileInit();
213 void ProjectileDraw();
214 void ProjectileUpdate();
215 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage);
216
217 //# Pathfinding map
218 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
219 float PathFindingGetDistance(int mapX, int mapY);
220 Vector2 PathFindingGetGradient(Vector3 world);
221 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
222 void PathFindingMapUpdate();
223 void PathFindingMapDraw();
224
225 //# UI
226 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor);
227
228 //# variables
229 extern Level *currentLevel;
230 extern Enemy enemies[ENEMY_MAX_COUNT];
231 extern int enemyCount;
232 extern EnemyClassConfig enemyClassConfigs[];
233
234 extern GUIState guiState;
235 extern GameTime gameTime;
236 extern Tower towers[TOWER_MAX_COUNT];
237 extern int towerCount;
238
239 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 void EnemyInit()
24 {
25 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
26 {
27 enemies[i] = (Enemy){0};
28 }
29 enemyCount = 0;
30 }
31
32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
33 {
34 return enemyClassConfigs[enemy->enemyType].speed;
35 }
36
37 float EnemyGetMaxHealth(Enemy *enemy)
38 {
39 return enemyClassConfigs[enemy->enemyType].health;
40 }
41
42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
43 {
44 int16_t castleX = 0;
45 int16_t castleY = 0;
46 int16_t dx = castleX - currentX;
47 int16_t dy = castleY - currentY;
48 if (dx == 0 && dy == 0)
49 {
50 *nextX = currentX;
51 *nextY = currentY;
52 return 1;
53 }
54 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
55
56 if (gradient.x == 0 && gradient.y == 0)
57 {
58 *nextX = currentX;
59 *nextY = currentY;
60 return 1;
61 }
62
63 if (fabsf(gradient.x) > fabsf(gradient.y))
64 {
65 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
66 *nextY = currentY;
67 return 0;
68 }
69 *nextX = currentX;
70 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
71 return 0;
72 }
73
74
75 // this function predicts the movement of the unit for the next deltaT seconds
76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
77 {
78 const float pointReachedDistance = 0.25f;
79 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
80 const float maxSimStepTime = 0.015625f;
81
82 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
83 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
84 int16_t nextX = enemy->nextX;
85 int16_t nextY = enemy->nextY;
86 Vector2 position = enemy->simPosition;
87 int passedCount = 0;
88 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
89 {
90 float stepTime = fminf(deltaT - t, maxSimStepTime);
91 Vector2 target = (Vector2){nextX, nextY};
92 float speed = Vector2Length(*velocity);
93 // draw the target position for debugging
94 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
95 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
96 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
97 {
98 // we reached the target position, let's move to the next waypoint
99 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100 target = (Vector2){nextX, nextY};
101 // track how many waypoints we passed
102 passedCount++;
103 }
104
105 // acceleration towards the target
106 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108 *velocity = Vector2Add(*velocity, acceleration);
109
110 // limit the speed to the maximum speed
111 if (speed > maxSpeed)
112 {
113 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114 }
115
116 // move the enemy
117 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118 }
119
120 if (waypointPassedCount)
121 {
122 (*waypointPassedCount) = passedCount;
123 }
124
125 return position;
126 }
127
128 void EnemyDraw()
129 {
130 for (int i = 0; i < enemyCount; i++)
131 {
132 Enemy enemy = enemies[i];
133 if (enemy.enemyType == ENEMY_TYPE_NONE)
134 {
135 continue;
136 }
137
138 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139
140 if (enemy.movePathCount > 0)
141 {
142 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144 }
145 for (int j = 1; j < enemy.movePathCount; j++)
146 {
147 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149 DrawLine3D(p, q, GREEN);
150 }
151
152 switch (enemy.enemyType)
153 {
154 case ENEMY_TYPE_MINION:
155 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156 break;
157 }
158 }
159 }
160
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163 // damage the tower
164 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167 float explosionRange2 = explosionRange * explosionRange;
168 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169 // explode the enemy
170 if (tower->damage >= TowerGetMaxHealth(tower))
171 {
172 tower->towerType = TOWER_TYPE_NONE;
173 }
174
175 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
176 explosionSource,
177 (Vector3){0, 0.1f, 0}, 1.0f);
178
179 enemy->enemyType = ENEMY_TYPE_NONE;
180
181 // push back enemies & dealing damage
182 for (int i = 0; i < enemyCount; i++)
183 {
184 Enemy *other = &enemies[i];
185 if (other->enemyType == ENEMY_TYPE_NONE)
186 {
187 continue;
188 }
189 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190 if (distanceSqr > 0 && distanceSqr < explosionRange2)
191 {
192 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194 EnemyAddDamage(other, explosionDamge);
195 }
196 }
197 }
198
199 void EnemyUpdate()
200 {
201 const float castleX = 0;
202 const float castleY = 0;
203 const float maxPathDistance2 = 0.25f * 0.25f;
204
205 for (int i = 0; i < enemyCount; i++)
206 {
207 Enemy *enemy = &enemies[i];
208 if (enemy->enemyType == ENEMY_TYPE_NONE)
209 {
210 continue;
211 }
212
213 int waypointPassedCount = 0;
214 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215 enemy->startMovingTime = gameTime.time;
216 // track path of unit
217 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218 {
219 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220 {
221 enemy->movePath[j] = enemy->movePath[j - 1];
222 }
223 enemy->movePath[0] = enemy->simPosition;
224 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225 {
226 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227 }
228 }
229
230 if (waypointPassedCount > 0)
231 {
232 enemy->currentX = enemy->nextX;
233 enemy->currentY = enemy->nextY;
234 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236 {
237 // enemy reached the castle; remove it
238 enemy->enemyType = ENEMY_TYPE_NONE;
239 continue;
240 }
241 }
242 }
243
244 // handle collisions between enemies
245 for (int i = 0; i < enemyCount - 1; i++)
246 {
247 Enemy *enemyA = &enemies[i];
248 if (enemyA->enemyType == ENEMY_TYPE_NONE)
249 {
250 continue;
251 }
252 for (int j = i + 1; j < enemyCount; j++)
253 {
254 Enemy *enemyB = &enemies[j];
255 if (enemyB->enemyType == ENEMY_TYPE_NONE)
256 {
257 continue;
258 }
259 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262 float radiusSum = radiusA + radiusB;
263 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264 {
265 // collision
266 float distance = sqrtf(distanceSqr);
267 float overlap = radiusSum - distance;
268 // move the enemies apart, but softly; if we have a clog of enemies,
269 // moving them perfectly apart can cause them to jitter
270 float positionCorrection = overlap / 5.0f;
271 Vector2 direction = (Vector2){
272 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276 }
277 }
278 }
279
280 // handle collisions between enemies and towers
281 for (int i = 0; i < enemyCount; i++)
282 {
283 Enemy *enemy = &enemies[i];
284 if (enemy->enemyType == ENEMY_TYPE_NONE)
285 {
286 continue;
287 }
288 enemy->contactTime -= gameTime.deltaTime;
289 if (enemy->contactTime < 0.0f)
290 {
291 enemy->contactTime = 0.0f;
292 }
293
294 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295 // linear search over towers; could be optimized by using path finding tower map,
296 // but for now, we keep it simple
297 for (int j = 0; j < towerCount; j++)
298 {
299 Tower *tower = &towers[j];
300 if (tower->towerType == TOWER_TYPE_NONE)
301 {
302 continue;
303 }
304 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306 if (distanceSqr > combinedRadius * combinedRadius)
307 {
308 continue;
309 }
310 // potential collision; square / circle intersection
311 float dx = tower->x - enemy->simPosition.x;
312 float dy = tower->y - enemy->simPosition.y;
313 float absDx = fabsf(dx);
314 float absDy = fabsf(dy);
315 Vector3 contactPoint = {0};
316 if (absDx <= 0.5f && absDx <= absDy) {
317 // vertical collision; push the enemy out horizontally
318 float overlap = enemyRadius + 0.5f - absDy;
319 if (overlap < 0.0f)
320 {
321 continue;
322 }
323 float direction = dy > 0.0f ? -1.0f : 1.0f;
324 enemy->simPosition.y += direction * overlap;
325 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
326 }
327 else if (absDy <= 0.5f && absDy <= absDx)
328 {
329 // horizontal collision; push the enemy out vertically
330 float overlap = enemyRadius + 0.5f - absDx;
331 if (overlap < 0.0f)
332 {
333 continue;
334 }
335 float direction = dx > 0.0f ? -1.0f : 1.0f;
336 enemy->simPosition.x += direction * overlap;
337 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
338 }
339 else
340 {
341 // possible collision with a corner
342 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
343 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
344 float cornerX = tower->x + cornerDX;
345 float cornerY = tower->y + cornerDY;
346 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
347 if (cornerDistanceSqr > enemyRadius * enemyRadius)
348 {
349 continue;
350 }
351 // push the enemy out along the diagonal
352 float cornerDistance = sqrtf(cornerDistanceSqr);
353 float overlap = enemyRadius - cornerDistance;
354 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
355 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
356 enemy->simPosition.x -= directionX * overlap;
357 enemy->simPosition.y -= directionY * overlap;
358 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
359 }
360
361 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
362 {
363 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
364 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
365 {
366 EnemyTriggerExplode(enemy, tower, contactPoint);
367 }
368 }
369 }
370 }
371 }
372
373 EnemyId EnemyGetId(Enemy *enemy)
374 {
375 return (EnemyId){enemy - enemies, enemy->generation};
376 }
377
378 Enemy *EnemyTryResolve(EnemyId enemyId)
379 {
380 if (enemyId.index >= ENEMY_MAX_COUNT)
381 {
382 return 0;
383 }
384 Enemy *enemy = &enemies[enemyId.index];
385 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
386 {
387 return 0;
388 }
389 return enemy;
390 }
391
392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
393 {
394 Enemy *spawn = 0;
395 for (int i = 0; i < enemyCount; i++)
396 {
397 Enemy *enemy = &enemies[i];
398 if (enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 spawn = enemy;
401 break;
402 }
403 }
404
405 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
406 {
407 spawn = &enemies[enemyCount++];
408 }
409
410 if (spawn)
411 {
412 spawn->currentX = currentX;
413 spawn->currentY = currentY;
414 spawn->nextX = currentX;
415 spawn->nextY = currentY;
416 spawn->simPosition = (Vector2){currentX, currentY};
417 spawn->simVelocity = (Vector2){0, 0};
418 spawn->enemyType = enemyType;
419 spawn->startMovingTime = gameTime.time;
420 spawn->damage = 0.0f;
421 spawn->futureDamage = 0.0f;
422 spawn->generation++;
423 spawn->movePathCount = 0;
424 }
425
426 return spawn;
427 }
428
429 int EnemyAddDamage(Enemy *enemy, float damage)
430 {
431 enemy->damage += damage;
432 if (enemy->damage >= EnemyGetMaxHealth(enemy))
433 {
434 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
435 enemy->enemyType = ENEMY_TYPE_NONE;
436 return 1;
437 }
438
439 return 0;
440 }
441
442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
443 {
444 int16_t castleX = 0;
445 int16_t castleY = 0;
446 Enemy* closest = 0;
447 int16_t closestDistance = 0;
448 float range2 = range * range;
449 for (int i = 0; i < enemyCount; i++)
450 {
451 Enemy* enemy = &enemies[i];
452 if (enemy->enemyType == ENEMY_TYPE_NONE)
453 {
454 continue;
455 }
456 float maxHealth = EnemyGetMaxHealth(enemy);
457 if (enemy->futureDamage >= maxHealth)
458 {
459 // ignore enemies that will die soon
460 continue;
461 }
462 int16_t dx = castleX - enemy->currentX;
463 int16_t dy = castleY - enemy->currentY;
464 int16_t distance = abs(dx) + abs(dy);
465 if (!closest || distance < closestDistance)
466 {
467 float tdx = towerX - enemy->currentX;
468 float tdy = towerY - enemy->currentY;
469 float tdistance2 = tdx * tdx + tdy * tdy;
470 if (tdistance2 <= range2)
471 {
472 closest = enemy;
473 closestDistance = distance;
474 }
475 }
476 }
477 return closest;
478 }
479
480 int EnemyCount()
481 {
482 int count = 0;
483 for (int i = 0; i < enemyCount; i++)
484 {
485 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
486 {
487 count++;
488 }
489 }
490 return count;
491 }
492
493 void EnemyDrawHealthbars(Camera3D camera)
494 {
495 for (int i = 0; i < enemyCount; i++)
496 {
497 Enemy *enemy = &enemies[i];
498 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
499 {
500 continue;
501 }
502 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
503 float maxHealth = EnemyGetMaxHealth(enemy);
504 float health = maxHealth - enemy->damage;
505 float healthRatio = health / maxHealth;
506
507 DrawHealthBar(camera, position, healthRatio, GREEN);
508 }
509 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate()
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < towerCount; i++)
131 {
132 Tower *tower = &towers[i];
133 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134 {
135 continue;
136 }
137 int16_t mapX, mapY;
138 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139 // this would not work correctly and needs to be refined to allow towers covering multiple cells
140 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141 // one cell. For now.
142 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143 {
144 continue;
145 }
146 int index = mapY * width + mapX;
147 pathfindingMap.towerIndex[index] = i;
148 }
149
150 // we start at the castle and add the castle to the queue
151 pathfindingMap.maxDistance = 0.0f;
152 pathfindingNodeQueueCount = 0;
153 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154 PathfindingNode *node = 0;
155 while ((node = PathFindingNodePop()))
156 {
157 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158 {
159 continue;
160 }
161 int index = node->y * width + node->x;
162 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163 {
164 continue;
165 }
166
167 int deltaX = node->x - node->fromX;
168 int deltaY = node->y - node->fromY;
169 // even if the cell is blocked by a tower, we still may want to store the direction
170 // (though this might not be needed, IDK right now)
171 pathfindingMap.deltaSrc[index].x = (char) deltaX;
172 pathfindingMap.deltaSrc[index].y = (char) deltaY;
173
174 // we skip nodes that are blocked by towers
175 if (pathfindingMap.towerIndex[index] >= 0)
176 {
177 node->distance += 8.0f;
178 }
179 pathfindingMap.distances[index] = node->distance;
180 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185 }
186 }
187
188 void PathFindingMapDraw()
189 {
190 float cellSize = pathfindingMap.scale * 0.9f;
191 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192 for (int x = 0; x < pathfindingMap.width; x++)
193 {
194 for (int y = 0; y < pathfindingMap.height; y++)
195 {
196 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200 // animate the distance "wave" to show how the pathfinding algorithm expands
201 // from the castle
202 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203 {
204 color = BLACK;
205 }
206 DrawCube(position, cellSize, 0.1f, cellSize, color);
207 }
208 }
209 }
210
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213 int16_t mapX, mapY;
214 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215 {
216 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217 return (Vector2){(float)-delta.x, (float)-delta.y};
218 }
219 // fallback to a simple gradient calculation
220 float n = PathFindingGetDistance(mapX, mapY - 1);
221 float s = PathFindingGetDistance(mapX, mapY + 1);
222 float w = PathFindingGetDistance(mapX - 1, mapY);
223 float e = PathFindingGetDistance(mapX + 1, mapY);
224 return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 void ProjectileInit()
8 {
9 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
10 {
11 projectiles[i] = (Projectile){0};
12 }
13 }
14
15 void ProjectileDraw()
16 {
17 for (int i = 0; i < projectileCount; i++)
18 {
19 Projectile projectile = projectiles[i];
20 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
21 {
22 continue;
23 }
24 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
25 if (transition >= 1.0f)
26 {
27 continue;
28 }
29 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
30 float x = position.x;
31 float y = position.y;
32 float dx = projectile.directionNormal.x;
33 float dy = projectile.directionNormal.y;
34 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
35 {
36 x -= dx * 0.1f;
37 y -= dy * 0.1f;
38 float size = 0.1f * d;
39 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
40 }
41 }
42 }
43
44 void ProjectileUpdate()
45 {
46 for (int i = 0; i < projectileCount; i++)
47 {
48 Projectile *projectile = &projectiles[i];
49 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
50 {
51 continue;
52 }
53 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
54 if (transition >= 1.0f)
55 {
56 projectile->projectileType = PROJECTILE_TYPE_NONE;
57 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
58 if (enemy)
59 {
60 EnemyAddDamage(enemy, projectile->damage);
61 }
62 continue;
63 }
64 }
65 }
66
67 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
68 {
69 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
70 {
71 Projectile *projectile = &projectiles[i];
72 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
73 {
74 projectile->projectileType = projectileType;
75 projectile->shootTime = gameTime.time;
76 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
77 projectile->damage = damage;
78 projectile->position = position;
79 projectile->target = target;
80 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
81 projectile->targetEnemy = EnemyGetId(enemy);
82 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
83 return projectile;
84 }
85 }
86 return 0;
87 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Particle particles[PARTICLE_MAX_COUNT];
5 static int particleCount = 0;
6
7 void ParticleInit()
8 {
9 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
10 {
11 particles[i] = (Particle){0};
12 }
13 particleCount = 0;
14 }
15
16 static void DrawExplosionParticle(Particle *particle, float transition)
17 {
18 float size = 1.2f * (1.0f - transition);
19 Color startColor = WHITE;
20 Color endColor = RED;
21 Color color = ColorLerp(startColor, endColor, transition);
22 DrawCube(particle->position, size, size, size, color);
23 }
24
25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
26 {
27 if (particleCount >= PARTICLE_MAX_COUNT)
28 {
29 return;
30 }
31
32 int index = -1;
33 for (int i = 0; i < particleCount; i++)
34 {
35 if (particles[i].particleType == PARTICLE_TYPE_NONE)
36 {
37 index = i;
38 break;
39 }
40 }
41
42 if (index == -1)
43 {
44 index = particleCount++;
45 }
46
47 Particle *particle = &particles[index];
48 particle->particleType = particleType;
49 particle->spawnTime = gameTime.time;
50 particle->lifetime = lifetime;
51 particle->position = position;
52 particle->velocity = velocity;
53 }
54
55 void ParticleUpdate()
56 {
57 for (int i = 0; i < particleCount; i++)
58 {
59 Particle *particle = &particles[i];
60 if (particle->particleType == PARTICLE_TYPE_NONE)
61 {
62 continue;
63 }
64
65 float age = gameTime.time - particle->spawnTime;
66
67 if (particle->lifetime > age)
68 {
69 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
70 }
71 else {
72 particle->particleType = PARTICLE_TYPE_NONE;
73 }
74 }
75 }
76
77 void ParticleDraw()
78 {
79 for (int i = 0; i < particleCount; i++)
80 {
81 Particle particle = particles[i];
82 if (particle.particleType == PARTICLE_TYPE_NONE)
83 {
84 continue;
85 }
86
87 float age = gameTime.time - particle.spawnTime;
88 float transition = age / particle.lifetime;
89 switch (particle.particleType)
90 {
91 case PARTICLE_TYPE_EXPLOSION:
92 DrawExplosionParticle(&particle, transition);
93 break;
94 default:
95 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
96 break;
97 }
98 }
99 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 Tower towers[TOWER_MAX_COUNT];
5 int towerCount = 0;
6
7 void TowerInit()
8 {
9 for (int i = 0; i < TOWER_MAX_COUNT; i++)
10 {
11 towers[i] = (Tower){0};
12 }
13 towerCount = 0;
14 }
15
16 static void TowerGunUpdate(Tower *tower)
17 {
18 if (tower->cooldown <= 0)
19 {
20 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
21 if (enemy)
22 {
23 tower->cooldown = 0.5f;
24 // shoot the enemy; determine future position of the enemy
25 float bulletSpeed = 1.0f;
26 float bulletDamage = 3.0f;
27 Vector2 velocity = enemy->simVelocity;
28 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
29 Vector2 towerPosition = {tower->x, tower->y};
30 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
31 for (int i = 0; i < 8; i++) {
32 velocity = enemy->simVelocity;
33 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
34 float distance = Vector2Distance(towerPosition, futurePosition);
35 float eta2 = distance / bulletSpeed;
36 if (fabs(eta - eta2) < 0.01f) {
37 break;
38 }
39 eta = (eta2 + eta) * 0.5f;
40 }
41 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
42 bulletSpeed, bulletDamage);
43 enemy->futureDamage += bulletDamage;
44 }
45 }
46 else
47 {
48 tower->cooldown -= gameTime.deltaTime;
49 }
50 }
51
52 Tower *TowerGetAt(int16_t x, int16_t y)
53 {
54 for (int i = 0; i < towerCount; i++)
55 {
56 if (towers[i].x == x && towers[i].y == y)
57 {
58 return &towers[i];
59 }
60 }
61 return 0;
62 }
63
64 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
65 {
66 if (towerCount >= TOWER_MAX_COUNT)
67 {
68 return 0;
69 }
70
71 Tower *tower = TowerGetAt(x, y);
72 if (tower)
73 {
74 return 0;
75 }
76
77 tower = &towers[towerCount++];
78 tower->x = x;
79 tower->y = y;
80 tower->towerType = towerType;
81 tower->cooldown = 0.0f;
82 tower->damage = 0.0f;
83 return tower;
84 }
85
86 Tower *GetTowerByType(uint8_t towerType)
87 {
88 for (int i = 0; i < towerCount; i++)
89 {
90 if (towers[i].towerType == towerType)
91 {
92 return &towers[i];
93 }
94 }
95 return 0;
96 }
97
98 int GetTowerCosts(uint8_t towerType)
99 {
100 switch (towerType)
101 {
102 case TOWER_TYPE_BASE:
103 return 0;
104 case TOWER_TYPE_GUN:
105 return 6;
106 case TOWER_TYPE_WALL:
107 return 2;
108 }
109 return 0;
110 }
111
112 float TowerGetMaxHealth(Tower *tower)
113 {
114 switch (tower->towerType)
115 {
116 case TOWER_TYPE_BASE:
117 return 10.0f;
118 case TOWER_TYPE_GUN:
119 return 3.0f;
120 case TOWER_TYPE_WALL:
121 return 5.0f;
122 }
123 return 0.0f;
124 }
125
126 void TowerDraw()
127 {
128 for (int i = 0; i < towerCount; i++)
129 {
130 Tower tower = towers[i];
131 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
132 switch (tower.towerType)
133 {
134 case TOWER_TYPE_BASE:
135 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
136 break;
137 case TOWER_TYPE_GUN:
138 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
139 break;
140 case TOWER_TYPE_WALL:
141 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
142 break;
143 }
144 }
145 }
146
147 void TowerUpdate()
148 {
149 for (int i = 0; i < towerCount; i++)
150 {
151 Tower *tower = &towers[i];
152 switch (tower->towerType)
153 {
154 case TOWER_TYPE_GUN:
155 TowerGunUpdate(tower);
156 break;
157 }
158 }
159 }
160
161 void TowerDrawHealthBars(Camera3D camera)
162 {
163 for (int i = 0; i < towerCount; i++)
164 {
165 Tower *tower = &towers[i];
166 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
167 {
168 continue;
169 }
170
171 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
172 float maxHealth = TowerGetMaxHealth(tower);
173 float health = maxHealth - tower->damage;
174 float healthRatio = health / maxHealth;
175
176 DrawHealthBar(camera, position, healthRatio, GREEN);
177 }
178 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
Here's how it looks like when you put it up to a test:
This wasn't difficult at all! Seeing how damaged your buildings are provides a lot of insights, especially since it shows that the damage remains even after the wave is over:
... which leads up to a question: Can the player repair buildings? How and when? I am not a game designer and I know that an important aspect of this profession is to think about these kind of questions before implementing the game. But one can't think of everything in advance and very often you only realize something when actually seeing it in action. I find this part of game development to be one of the most interesting ones: Once you start to see your game in action, you get a lot of ideas on how to improve it. You "just" have to start this journey. It's really the most important thing you can do: Start doing it!
Graphics
Speaking of seeing stuff in action: We need to replace the enemy and tower cubes with some graphics for the purpose of being able to distinguish between different enemy types and towers. Anything simple will do for now, so let's just use some billboard sprites for the enemies and simple 3d meshes for the towers.
In this tutorial, I won't go into detail how to create 3D models or sprites and will focus on how to integrate them into the game. You can use the graphics I provide (they are contained in the ZIP files below), search for assets on the internet or create your own.
What type of graphic style and quality you choose is up to you. You can get an asset pack from sites like Kenney.nl or OpenGameArt or you can create your own. For prototype game development, simpler graphics are usually better, as they are faster to create and don't distract from gameplay evaluation. A consistent style and sensible use of colors is still important, though.
Chosing a premade colorpalette can help a great deal to use colors that fit together. The Lospec Palette List is a great resource for this. My favorite 32 color palette is the Dawnbringer 32 palette.
I love graphical details, so I usually spend more time on this than I should, but for me it is also important to have fun while developing (remember, free time project, not work!). I am not a professional artist, but I have come up with some ways to create simple graphics that still have a certain charm and that I can create in quite a short time.
Here's an overview of the 3d models that I will use:
As you can see, the models are quite simple and look fairly low poly, though the tesselation is still high due to not using textures but only a gradient texture, which looks like this:
While this technique may look a little strange at first, it has a few advantages:
- A single texture for all objects
- No texture resolution to worry about
- No complex UV unwrapping & texture painting
- Can integrate lighting so it looks good even without lighting
- Quick modelling due to choosing simplicity over details
- Consistent style: Having a color palette texture makes it easy to pick fitting colors
- Low memory usage: The texture is usually fairly small.
- Easy to change colors: Creating a color variation of an object requires only moving a few UVs around. It's almost magical!
To illustrate the last point, here's in real time how I change the color of the tree model:
And if you're curious how the modelling looks like, here's a video on how I model a simple low poly rock in Blender (also real time):
Apparently, this isn't the first time I've done this, so this one went quite fast. It takes more time when creating something new, but in general, all objects I created for this tutorial were done within a few hours.
As for the sprites, I am using a texture atlas with sprites for the enemies and I use the same color palette as for the 3D models (albeit limited to the 32 colors, the color palette is btw. the Dawnbringer 32 palette).
Here's a part of the texture atlas that I worked out in Aseprite:
The sprites are each around 16x16, so fairly small. That makes it easier and fast to create.
All the models and sprites are available in the ZIP files that you can download from the respective parts of this tutorial. Feel free to use them for your own projects: I release them under the CC0 license, so you can use them as you like.
Replacing the cubes
Let's start with placing actual walls. We load the 3D and draw it in place of the cube we used before:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Variables
7 GUIState guiState = {0};
8 GameTime gameTime = {0};
9
10 Level levels[] = {
11 [0] = {
12 .state = LEVEL_STATE_BUILDING,
13 .initialGold = 20,
14 .waves[0] = {
15 .enemyType = ENEMY_TYPE_MINION,
16 .wave = 0,
17 .count = 10,
18 .interval = 2.5f,
19 .delay = 1.0f,
20 .spawnPosition = {0, 6},
21 },
22 .waves[1] = {
23 .enemyType = ENEMY_TYPE_MINION,
24 .wave = 1,
25 .count = 20,
26 .interval = 1.5f,
27 .delay = 1.0f,
28 .spawnPosition = {0, 6},
29 },
30 .waves[2] = {
31 .enemyType = ENEMY_TYPE_MINION,
32 .wave = 2,
33 .count = 30,
34 .interval = 1.2f,
35 .delay = 1.0f,
36 .spawnPosition = {0, 6},
37 }
38 },
39 };
40
41 Level *currentLevel = levels;
42
43 //# Game
44
45 void InitLevel(Level *level)
46 {
47 TowerInit();
48 EnemyInit();
49 ProjectileInit();
50 ParticleInit();
51 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
52
53 level->placementMode = 0;
54 level->state = LEVEL_STATE_BUILDING;
55 level->nextState = LEVEL_STATE_NONE;
56 level->playerGold = level->initialGold;
57
58 Camera *camera = &level->camera;
59 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
60 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
61 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
62 camera->fovy = 45.0f;
63 camera->projection = CAMERA_PERSPECTIVE;
64 }
65
66 void DrawLevelHud(Level *level)
67 {
68 const char *text = TextFormat("Gold: %d", level->playerGold);
69 Font font = GetFontDefault();
70 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
71 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
72 }
73
74 void DrawLevelReportLostWave(Level *level)
75 {
76 BeginMode3D(level->camera);
77 DrawGrid(10, 1.0f);
78 TowerDraw();
79 EnemyDraw();
80 ProjectileDraw();
81 ParticleDraw();
82 guiState.isBlocked = 0;
83 EndMode3D();
84
85 TowerDrawHealthBars(level->camera);
86
87 const char *text = "Wave lost";
88 int textWidth = MeasureText(text, 20);
89 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
90
91 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
92 {
93 level->nextState = LEVEL_STATE_RESET;
94 }
95 }
96
97 int HasLevelNextWave(Level *level)
98 {
99 for (int i = 0; i < 10; i++)
100 {
101 EnemyWave *wave = &level->waves[i];
102 if (wave->wave == level->currentWave)
103 {
104 return 1;
105 }
106 }
107 return 0;
108 }
109
110 void DrawLevelReportWonWave(Level *level)
111 {
112 BeginMode3D(level->camera);
113 DrawGrid(10, 1.0f);
114 TowerDraw();
115 EnemyDraw();
116 ProjectileDraw();
117 ParticleDraw();
118 guiState.isBlocked = 0;
119 EndMode3D();
120
121 TowerDrawHealthBars(level->camera);
122
123 const char *text = "Wave won";
124 int textWidth = MeasureText(text, 20);
125 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
126
127
128 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
129 {
130 level->nextState = LEVEL_STATE_RESET;
131 }
132
133 if (HasLevelNextWave(level))
134 {
135 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
136 {
137 level->nextState = LEVEL_STATE_BUILDING;
138 }
139 }
140 else {
141 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
142 {
143 level->nextState = LEVEL_STATE_WON_LEVEL;
144 }
145 }
146 }
147
148 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
149 {
150 static ButtonState buttonStates[8] = {0};
151 int cost = GetTowerCosts(towerType);
152 const char *text = TextFormat("%s: %d", name, cost);
153 buttonStates[towerType].isSelected = level->placementMode == towerType;
154 buttonStates[towerType].isDisabled = level->playerGold < cost;
155 if (Button(text, x, y, width, height, &buttonStates[towerType]))
156 {
157 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
158 }
159 }
160
161 void DrawLevelBuildingState(Level *level)
162 {
163 BeginMode3D(level->camera);
164 DrawGrid(10, 1.0f);
165 TowerDraw();
166 EnemyDraw();
167 ProjectileDraw();
168 ParticleDraw();
169
170 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
171 float planeDistance = ray.position.y / -ray.direction.y;
172 float planeX = ray.direction.x * planeDistance + ray.position.x;
173 float planeY = ray.direction.z * planeDistance + ray.position.z;
174 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
175 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
176 if (level->placementMode && !guiState.isBlocked)
177 {
178 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
179 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
180 {
181 if (TowerTryAdd(level->placementMode, mapX, mapY))
182 {
183 level->playerGold -= GetTowerCosts(level->placementMode);
184 level->placementMode = TOWER_TYPE_NONE;
185 }
186 }
187 }
188
189 guiState.isBlocked = 0;
190
191 EndMode3D();
192
193 TowerDrawHealthBars(level->camera);
194
195 static ButtonState buildWallButtonState = {0};
196 static ButtonState buildGunButtonState = {0};
197 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
198 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
199
200 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
201 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
202
203 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
204 {
205 level->nextState = LEVEL_STATE_RESET;
206 }
207
208 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
209 {
210 level->nextState = LEVEL_STATE_BATTLE;
211 }
212
213 const char *text = "Building phase";
214 int textWidth = MeasureText(text, 20);
215 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
216 }
217
218 void InitBattleStateConditions(Level *level)
219 {
220 level->state = LEVEL_STATE_BATTLE;
221 level->nextState = LEVEL_STATE_NONE;
222 level->waveEndTimer = 0.0f;
223 for (int i = 0; i < 10; i++)
224 {
225 EnemyWave *wave = &level->waves[i];
226 wave->spawned = 0;
227 wave->timeToSpawnNext = wave->delay;
228 }
229 }
230
231 void DrawLevelBattleState(Level *level)
232 {
233 BeginMode3D(level->camera);
234 DrawGrid(10, 1.0f);
235 TowerDraw();
236 EnemyDraw();
237 ProjectileDraw();
238 ParticleDraw();
239 guiState.isBlocked = 0;
240 EndMode3D();
241
242 EnemyDrawHealthbars(level->camera);
243 TowerDrawHealthBars(level->camera);
244
245 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
246 {
247 level->nextState = LEVEL_STATE_RESET;
248 }
249
250 int maxCount = 0;
251 int remainingCount = 0;
252 for (int i = 0; i < 10; i++)
253 {
254 EnemyWave *wave = &level->waves[i];
255 if (wave->wave != level->currentWave)
256 {
257 continue;
258 }
259 maxCount += wave->count;
260 remainingCount += wave->count - wave->spawned;
261 }
262 int aliveCount = EnemyCount();
263 remainingCount += aliveCount;
264
265 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
266 int textWidth = MeasureText(text, 20);
267 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
268 }
269
270 void DrawLevel(Level *level)
271 {
272 switch (level->state)
273 {
274 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
275 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
276 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
277 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
278 default: break;
279 }
280
281 DrawLevelHud(level);
282 }
283
284 void UpdateLevel(Level *level)
285 {
286 if (level->state == LEVEL_STATE_BATTLE)
287 {
288 int activeWaves = 0;
289 for (int i = 0; i < 10; i++)
290 {
291 EnemyWave *wave = &level->waves[i];
292 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
293 {
294 continue;
295 }
296 activeWaves++;
297 wave->timeToSpawnNext -= gameTime.deltaTime;
298 if (wave->timeToSpawnNext <= 0.0f)
299 {
300 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
301 if (enemy)
302 {
303 wave->timeToSpawnNext = wave->interval;
304 wave->spawned++;
305 }
306 }
307 }
308 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
309 level->waveEndTimer += gameTime.deltaTime;
310 if (level->waveEndTimer >= 2.0f)
311 {
312 level->nextState = LEVEL_STATE_LOST_WAVE;
313 }
314 }
315 else if (activeWaves == 0 && EnemyCount() == 0)
316 {
317 level->waveEndTimer += gameTime.deltaTime;
318 if (level->waveEndTimer >= 2.0f)
319 {
320 level->nextState = LEVEL_STATE_WON_WAVE;
321 }
322 }
323 }
324
325 PathFindingMapUpdate();
326 EnemyUpdate();
327 TowerUpdate();
328 ProjectileUpdate();
329 ParticleUpdate();
330
331 if (level->nextState == LEVEL_STATE_RESET)
332 {
333 InitLevel(level);
334 }
335
336 if (level->nextState == LEVEL_STATE_BATTLE)
337 {
338 InitBattleStateConditions(level);
339 }
340
341 if (level->nextState == LEVEL_STATE_WON_WAVE)
342 {
343 level->currentWave++;
344 level->state = LEVEL_STATE_WON_WAVE;
345 }
346
347 if (level->nextState == LEVEL_STATE_LOST_WAVE)
348 {
349 level->state = LEVEL_STATE_LOST_WAVE;
350 }
351
352 if (level->nextState == LEVEL_STATE_BUILDING)
353 {
354 level->state = LEVEL_STATE_BUILDING;
355 }
356
357 if (level->nextState == LEVEL_STATE_WON_LEVEL)
358 {
359 // make something of this later
360 InitLevel(level);
361 }
362
363 level->nextState = LEVEL_STATE_NONE;
364 }
365
366 float nextSpawnTime = 0.0f;
367
368 void ResetGame()
369 {
370 InitLevel(currentLevel);
371 }
372
373 void InitGame()
374 {
375 TowerInit();
376 EnemyInit();
377 ProjectileInit();
378 ParticleInit();
379 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
380
381 currentLevel = levels;
382 InitLevel(currentLevel);
383 }
384
385 //# Immediate GUI functions
386
387 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor)
388 {
389 const float healthBarWidth = 40.0f;
390 const float healthBarHeight = 6.0f;
391 const float healthBarOffset = 15.0f;
392 const float inset = 2.0f;
393 const float innerWidth = healthBarWidth - inset * 2;
394 const float innerHeight = healthBarHeight - inset * 2;
395
396 Vector2 screenPos = GetWorldToScreen(position, camera);
397 float centerX = screenPos.x - healthBarWidth * 0.5f;
398 float topY = screenPos.y - healthBarOffset;
399 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
400 float healthWidth = innerWidth * healthRatio;
401 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
402 }
403
404 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
405 {
406 Rectangle bounds = {x, y, width, height};
407 int isPressed = 0;
408 int isSelected = state && state->isSelected;
409 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
410 {
411 Color color = isSelected ? DARKGRAY : GRAY;
412 DrawRectangle(x, y, width, height, color);
413 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
414 {
415 isPressed = 1;
416 }
417 guiState.isBlocked = 1;
418 }
419 else
420 {
421 Color color = isSelected ? WHITE : LIGHTGRAY;
422 DrawRectangle(x, y, width, height, color);
423 }
424 Font font = GetFontDefault();
425 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
426 Color textColor = state->isDisabled ? GRAY : BLACK;
427 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
428 return isPressed;
429 }
430
431 //# Main game loop
432
433 void GameUpdate()
434 {
435 float dt = GetFrameTime();
436 // cap maximum delta time to 0.1 seconds to prevent large time steps
437 if (dt > 0.1f) dt = 0.1f;
438 gameTime.time += dt;
439 gameTime.deltaTime = dt;
440
441 UpdateLevel(currentLevel);
442 }
443
444 int main(void)
445 {
446 int screenWidth, screenHeight;
447 GetPreferredSize(&screenWidth, &screenHeight);
448 InitWindow(screenWidth, screenHeight, "Tower defense");
449 SetTargetFPS(30);
450
451 InitGame();
452
453 while (!WindowShouldClose())
454 {
455 if (IsPaused()) {
456 // canvas is not visible in browser - do nothing
457 continue;
458 }
459
460 BeginDrawing();
461 ClearBackground(DARKBLUE);
462
463 GameUpdate();
464 DrawLevel(currentLevel);
465
466 EndDrawing();
467 }
468
469 CloseWindow();
470
471 return 0;
472 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 } Particle;
28
29 #define TOWER_MAX_COUNT 400
30 #define TOWER_TYPE_NONE 0
31 #define TOWER_TYPE_BASE 1
32 #define TOWER_TYPE_GUN 2
33 #define TOWER_TYPE_WALL 3
34 #define TOWER_TYPE_COUNT 4
35
36 typedef struct Tower
37 {
38 int16_t x, y;
39 uint8_t towerType;
40 float cooldown;
41 float damage;
42 } Tower;
43
44 typedef struct GameTime
45 {
46 float time;
47 float deltaTime;
48 } GameTime;
49
50 typedef struct ButtonState {
51 char isSelected;
52 char isDisabled;
53 } ButtonState;
54
55 typedef struct GUIState {
56 int isBlocked;
57 } GUIState;
58
59 typedef enum LevelState
60 {
61 LEVEL_STATE_NONE,
62 LEVEL_STATE_BUILDING,
63 LEVEL_STATE_BATTLE,
64 LEVEL_STATE_WON_WAVE,
65 LEVEL_STATE_LOST_WAVE,
66 LEVEL_STATE_WON_LEVEL,
67 LEVEL_STATE_RESET,
68 } LevelState;
69
70 typedef struct EnemyWave {
71 uint8_t enemyType;
72 uint8_t wave;
73 uint16_t count;
74 float interval;
75 float delay;
76 Vector2 spawnPosition;
77
78 uint16_t spawned;
79 float timeToSpawnNext;
80 } EnemyWave;
81
82 typedef struct Level
83 {
84 LevelState state;
85 LevelState nextState;
86 Camera3D camera;
87 int placementMode;
88
89 int initialGold;
90 int playerGold;
91
92 EnemyWave waves[10];
93 int currentWave;
94 float waveEndTimer;
95 } Level;
96
97 typedef struct DeltaSrc
98 {
99 char x, y;
100 } DeltaSrc;
101
102 typedef struct PathfindingMap
103 {
104 int width, height;
105 float scale;
106 float *distances;
107 long *towerIndex;
108 DeltaSrc *deltaSrc;
109 float maxDistance;
110 Matrix toMapSpace;
111 Matrix toWorldSpace;
112 } PathfindingMap;
113
114 // when we execute the pathfinding algorithm, we need to store the active nodes
115 // in a queue. Each node has a position, a distance from the start, and the
116 // position of the node that we came from.
117 typedef struct PathfindingNode
118 {
119 int16_t x, y, fromX, fromY;
120 float distance;
121 } PathfindingNode;
122
123 typedef struct EnemyId
124 {
125 uint16_t index;
126 uint16_t generation;
127 } EnemyId;
128
129 typedef struct EnemyClassConfig
130 {
131 float speed;
132 float health;
133 float radius;
134 float maxAcceleration;
135 float requiredContactTime;
136 float explosionDamage;
137 float explosionRange;
138 float explosionPushbackPower;
139 int goldValue;
140 } EnemyClassConfig;
141
142 typedef struct Enemy
143 {
144 int16_t currentX, currentY;
145 int16_t nextX, nextY;
146 Vector2 simPosition;
147 Vector2 simVelocity;
148 uint16_t generation;
149 float startMovingTime;
150 float damage, futureDamage;
151 float contactTime;
152 uint8_t enemyType;
153 uint8_t movePathCount;
154 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
155 } Enemy;
156
157 #define PROJECTILE_MAX_COUNT 1200
158 #define PROJECTILE_TYPE_NONE 0
159 #define PROJECTILE_TYPE_BULLET 1
160
161 typedef struct Projectile
162 {
163 uint8_t projectileType;
164 float shootTime;
165 float arrivalTime;
166 float damage;
167 Vector2 position;
168 Vector2 target;
169 Vector2 directionNormal;
170 EnemyId targetEnemy;
171 } Projectile;
172
173 //# Function declarations
174 float TowerGetMaxHealth(Tower *tower);
175 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
176 int EnemyAddDamage(Enemy *enemy, float damage);
177
178 //# Enemy functions
179 void EnemyInit();
180 void EnemyDraw();
181 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
182 void EnemyUpdate();
183 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
184 float EnemyGetMaxHealth(Enemy *enemy);
185 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
186 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
187 EnemyId EnemyGetId(Enemy *enemy);
188 Enemy *EnemyTryResolve(EnemyId enemyId);
189 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
190 int EnemyAddDamage(Enemy *enemy, float damage);
191 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
192 int EnemyCount();
193 void EnemyDrawHealthbars(Camera3D camera);
194
195 //# Tower functions
196 void TowerInit();
197 Tower *TowerGetAt(int16_t x, int16_t y);
198 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
199 Tower *GetTowerByType(uint8_t towerType);
200 int GetTowerCosts(uint8_t towerType);
201 float TowerGetMaxHealth(Tower *tower);
202 void TowerDraw();
203 void TowerUpdate();
204 void TowerDrawHealthBars(Camera3D camera);
205
206 //# Particles
207 void ParticleInit();
208 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
209 void ParticleUpdate();
210 void ParticleDraw();
211
212 //# Projectiles
213 void ProjectileInit();
214 void ProjectileDraw();
215 void ProjectileUpdate();
216 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage);
217
218 //# Pathfinding map
219 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
220 float PathFindingGetDistance(int mapX, int mapY);
221 Vector2 PathFindingGetGradient(Vector3 world);
222 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
223 void PathFindingMapUpdate();
224 void PathFindingMapDraw();
225
226 //# UI
227 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor);
228
229 //# variables
230 extern Level *currentLevel;
231 extern Enemy enemies[ENEMY_MAX_COUNT];
232 extern int enemyCount;
233 extern EnemyClassConfig enemyClassConfigs[];
234
235 extern GUIState guiState;
236 extern GameTime gameTime;
237 extern Tower towers[TOWER_MAX_COUNT];
238 extern int towerCount;
239
240 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 void EnemyInit()
24 {
25 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
26 {
27 enemies[i] = (Enemy){0};
28 }
29 enemyCount = 0;
30 }
31
32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
33 {
34 return enemyClassConfigs[enemy->enemyType].speed;
35 }
36
37 float EnemyGetMaxHealth(Enemy *enemy)
38 {
39 return enemyClassConfigs[enemy->enemyType].health;
40 }
41
42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
43 {
44 int16_t castleX = 0;
45 int16_t castleY = 0;
46 int16_t dx = castleX - currentX;
47 int16_t dy = castleY - currentY;
48 if (dx == 0 && dy == 0)
49 {
50 *nextX = currentX;
51 *nextY = currentY;
52 return 1;
53 }
54 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
55
56 if (gradient.x == 0 && gradient.y == 0)
57 {
58 *nextX = currentX;
59 *nextY = currentY;
60 return 1;
61 }
62
63 if (fabsf(gradient.x) > fabsf(gradient.y))
64 {
65 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
66 *nextY = currentY;
67 return 0;
68 }
69 *nextX = currentX;
70 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
71 return 0;
72 }
73
74
75 // this function predicts the movement of the unit for the next deltaT seconds
76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
77 {
78 const float pointReachedDistance = 0.25f;
79 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
80 const float maxSimStepTime = 0.015625f;
81
82 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
83 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
84 int16_t nextX = enemy->nextX;
85 int16_t nextY = enemy->nextY;
86 Vector2 position = enemy->simPosition;
87 int passedCount = 0;
88 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
89 {
90 float stepTime = fminf(deltaT - t, maxSimStepTime);
91 Vector2 target = (Vector2){nextX, nextY};
92 float speed = Vector2Length(*velocity);
93 // draw the target position for debugging
94 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
95 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
96 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
97 {
98 // we reached the target position, let's move to the next waypoint
99 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100 target = (Vector2){nextX, nextY};
101 // track how many waypoints we passed
102 passedCount++;
103 }
104
105 // acceleration towards the target
106 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108 *velocity = Vector2Add(*velocity, acceleration);
109
110 // limit the speed to the maximum speed
111 if (speed > maxSpeed)
112 {
113 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114 }
115
116 // move the enemy
117 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118 }
119
120 if (waypointPassedCount)
121 {
122 (*waypointPassedCount) = passedCount;
123 }
124
125 return position;
126 }
127
128 void EnemyDraw()
129 {
130 for (int i = 0; i < enemyCount; i++)
131 {
132 Enemy enemy = enemies[i];
133 if (enemy.enemyType == ENEMY_TYPE_NONE)
134 {
135 continue;
136 }
137
138 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139
140 if (enemy.movePathCount > 0)
141 {
142 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144 }
145 for (int j = 1; j < enemy.movePathCount; j++)
146 {
147 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149 DrawLine3D(p, q, GREEN);
150 }
151
152 switch (enemy.enemyType)
153 {
154 case ENEMY_TYPE_MINION:
155 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156 break;
157 }
158 }
159 }
160
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163 // damage the tower
164 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167 float explosionRange2 = explosionRange * explosionRange;
168 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169 // explode the enemy
170 if (tower->damage >= TowerGetMaxHealth(tower))
171 {
172 tower->towerType = TOWER_TYPE_NONE;
173 }
174
175 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
176 explosionSource,
177 (Vector3){0, 0.1f, 0}, 1.0f);
178
179 enemy->enemyType = ENEMY_TYPE_NONE;
180
181 // push back enemies & dealing damage
182 for (int i = 0; i < enemyCount; i++)
183 {
184 Enemy *other = &enemies[i];
185 if (other->enemyType == ENEMY_TYPE_NONE)
186 {
187 continue;
188 }
189 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190 if (distanceSqr > 0 && distanceSqr < explosionRange2)
191 {
192 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194 EnemyAddDamage(other, explosionDamge);
195 }
196 }
197 }
198
199 void EnemyUpdate()
200 {
201 const float castleX = 0;
202 const float castleY = 0;
203 const float maxPathDistance2 = 0.25f * 0.25f;
204
205 for (int i = 0; i < enemyCount; i++)
206 {
207 Enemy *enemy = &enemies[i];
208 if (enemy->enemyType == ENEMY_TYPE_NONE)
209 {
210 continue;
211 }
212
213 int waypointPassedCount = 0;
214 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215 enemy->startMovingTime = gameTime.time;
216 // track path of unit
217 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218 {
219 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220 {
221 enemy->movePath[j] = enemy->movePath[j - 1];
222 }
223 enemy->movePath[0] = enemy->simPosition;
224 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225 {
226 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227 }
228 }
229
230 if (waypointPassedCount > 0)
231 {
232 enemy->currentX = enemy->nextX;
233 enemy->currentY = enemy->nextY;
234 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236 {
237 // enemy reached the castle; remove it
238 enemy->enemyType = ENEMY_TYPE_NONE;
239 continue;
240 }
241 }
242 }
243
244 // handle collisions between enemies
245 for (int i = 0; i < enemyCount - 1; i++)
246 {
247 Enemy *enemyA = &enemies[i];
248 if (enemyA->enemyType == ENEMY_TYPE_NONE)
249 {
250 continue;
251 }
252 for (int j = i + 1; j < enemyCount; j++)
253 {
254 Enemy *enemyB = &enemies[j];
255 if (enemyB->enemyType == ENEMY_TYPE_NONE)
256 {
257 continue;
258 }
259 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262 float radiusSum = radiusA + radiusB;
263 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264 {
265 // collision
266 float distance = sqrtf(distanceSqr);
267 float overlap = radiusSum - distance;
268 // move the enemies apart, but softly; if we have a clog of enemies,
269 // moving them perfectly apart can cause them to jitter
270 float positionCorrection = overlap / 5.0f;
271 Vector2 direction = (Vector2){
272 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276 }
277 }
278 }
279
280 // handle collisions between enemies and towers
281 for (int i = 0; i < enemyCount; i++)
282 {
283 Enemy *enemy = &enemies[i];
284 if (enemy->enemyType == ENEMY_TYPE_NONE)
285 {
286 continue;
287 }
288 enemy->contactTime -= gameTime.deltaTime;
289 if (enemy->contactTime < 0.0f)
290 {
291 enemy->contactTime = 0.0f;
292 }
293
294 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295 // linear search over towers; could be optimized by using path finding tower map,
296 // but for now, we keep it simple
297 for (int j = 0; j < towerCount; j++)
298 {
299 Tower *tower = &towers[j];
300 if (tower->towerType == TOWER_TYPE_NONE)
301 {
302 continue;
303 }
304 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306 if (distanceSqr > combinedRadius * combinedRadius)
307 {
308 continue;
309 }
310 // potential collision; square / circle intersection
311 float dx = tower->x - enemy->simPosition.x;
312 float dy = tower->y - enemy->simPosition.y;
313 float absDx = fabsf(dx);
314 float absDy = fabsf(dy);
315 Vector3 contactPoint = {0};
316 if (absDx <= 0.5f && absDx <= absDy) {
317 // vertical collision; push the enemy out horizontally
318 float overlap = enemyRadius + 0.5f - absDy;
319 if (overlap < 0.0f)
320 {
321 continue;
322 }
323 float direction = dy > 0.0f ? -1.0f : 1.0f;
324 enemy->simPosition.y += direction * overlap;
325 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
326 }
327 else if (absDy <= 0.5f && absDy <= absDx)
328 {
329 // horizontal collision; push the enemy out vertically
330 float overlap = enemyRadius + 0.5f - absDx;
331 if (overlap < 0.0f)
332 {
333 continue;
334 }
335 float direction = dx > 0.0f ? -1.0f : 1.0f;
336 enemy->simPosition.x += direction * overlap;
337 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
338 }
339 else
340 {
341 // possible collision with a corner
342 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
343 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
344 float cornerX = tower->x + cornerDX;
345 float cornerY = tower->y + cornerDY;
346 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
347 if (cornerDistanceSqr > enemyRadius * enemyRadius)
348 {
349 continue;
350 }
351 // push the enemy out along the diagonal
352 float cornerDistance = sqrtf(cornerDistanceSqr);
353 float overlap = enemyRadius - cornerDistance;
354 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
355 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
356 enemy->simPosition.x -= directionX * overlap;
357 enemy->simPosition.y -= directionY * overlap;
358 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
359 }
360
361 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
362 {
363 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
364 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
365 {
366 EnemyTriggerExplode(enemy, tower, contactPoint);
367 }
368 }
369 }
370 }
371 }
372
373 EnemyId EnemyGetId(Enemy *enemy)
374 {
375 return (EnemyId){enemy - enemies, enemy->generation};
376 }
377
378 Enemy *EnemyTryResolve(EnemyId enemyId)
379 {
380 if (enemyId.index >= ENEMY_MAX_COUNT)
381 {
382 return 0;
383 }
384 Enemy *enemy = &enemies[enemyId.index];
385 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
386 {
387 return 0;
388 }
389 return enemy;
390 }
391
392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
393 {
394 Enemy *spawn = 0;
395 for (int i = 0; i < enemyCount; i++)
396 {
397 Enemy *enemy = &enemies[i];
398 if (enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 spawn = enemy;
401 break;
402 }
403 }
404
405 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
406 {
407 spawn = &enemies[enemyCount++];
408 }
409
410 if (spawn)
411 {
412 spawn->currentX = currentX;
413 spawn->currentY = currentY;
414 spawn->nextX = currentX;
415 spawn->nextY = currentY;
416 spawn->simPosition = (Vector2){currentX, currentY};
417 spawn->simVelocity = (Vector2){0, 0};
418 spawn->enemyType = enemyType;
419 spawn->startMovingTime = gameTime.time;
420 spawn->damage = 0.0f;
421 spawn->futureDamage = 0.0f;
422 spawn->generation++;
423 spawn->movePathCount = 0;
424 }
425
426 return spawn;
427 }
428
429 int EnemyAddDamage(Enemy *enemy, float damage)
430 {
431 enemy->damage += damage;
432 if (enemy->damage >= EnemyGetMaxHealth(enemy))
433 {
434 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
435 enemy->enemyType = ENEMY_TYPE_NONE;
436 return 1;
437 }
438
439 return 0;
440 }
441
442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
443 {
444 int16_t castleX = 0;
445 int16_t castleY = 0;
446 Enemy* closest = 0;
447 int16_t closestDistance = 0;
448 float range2 = range * range;
449 for (int i = 0; i < enemyCount; i++)
450 {
451 Enemy* enemy = &enemies[i];
452 if (enemy->enemyType == ENEMY_TYPE_NONE)
453 {
454 continue;
455 }
456 float maxHealth = EnemyGetMaxHealth(enemy);
457 if (enemy->futureDamage >= maxHealth)
458 {
459 // ignore enemies that will die soon
460 continue;
461 }
462 int16_t dx = castleX - enemy->currentX;
463 int16_t dy = castleY - enemy->currentY;
464 int16_t distance = abs(dx) + abs(dy);
465 if (!closest || distance < closestDistance)
466 {
467 float tdx = towerX - enemy->currentX;
468 float tdy = towerY - enemy->currentY;
469 float tdistance2 = tdx * tdx + tdy * tdy;
470 if (tdistance2 <= range2)
471 {
472 closest = enemy;
473 closestDistance = distance;
474 }
475 }
476 }
477 return closest;
478 }
479
480 int EnemyCount()
481 {
482 int count = 0;
483 for (int i = 0; i < enemyCount; i++)
484 {
485 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
486 {
487 count++;
488 }
489 }
490 return count;
491 }
492
493 void EnemyDrawHealthbars(Camera3D camera)
494 {
495 for (int i = 0; i < enemyCount; i++)
496 {
497 Enemy *enemy = &enemies[i];
498 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
499 {
500 continue;
501 }
502 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
503 float maxHealth = EnemyGetMaxHealth(enemy);
504 float health = maxHealth - enemy->damage;
505 float healthRatio = health / maxHealth;
506
507 DrawHealthBar(camera, position, healthRatio, GREEN);
508 }
509 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate()
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < towerCount; i++)
131 {
132 Tower *tower = &towers[i];
133 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134 {
135 continue;
136 }
137 int16_t mapX, mapY;
138 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139 // this would not work correctly and needs to be refined to allow towers covering multiple cells
140 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141 // one cell. For now.
142 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143 {
144 continue;
145 }
146 int index = mapY * width + mapX;
147 pathfindingMap.towerIndex[index] = i;
148 }
149
150 // we start at the castle and add the castle to the queue
151 pathfindingMap.maxDistance = 0.0f;
152 pathfindingNodeQueueCount = 0;
153 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154 PathfindingNode *node = 0;
155 while ((node = PathFindingNodePop()))
156 {
157 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158 {
159 continue;
160 }
161 int index = node->y * width + node->x;
162 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163 {
164 continue;
165 }
166
167 int deltaX = node->x - node->fromX;
168 int deltaY = node->y - node->fromY;
169 // even if the cell is blocked by a tower, we still may want to store the direction
170 // (though this might not be needed, IDK right now)
171 pathfindingMap.deltaSrc[index].x = (char) deltaX;
172 pathfindingMap.deltaSrc[index].y = (char) deltaY;
173
174 // we skip nodes that are blocked by towers
175 if (pathfindingMap.towerIndex[index] >= 0)
176 {
177 node->distance += 8.0f;
178 }
179 pathfindingMap.distances[index] = node->distance;
180 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185 }
186 }
187
188 void PathFindingMapDraw()
189 {
190 float cellSize = pathfindingMap.scale * 0.9f;
191 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192 for (int x = 0; x < pathfindingMap.width; x++)
193 {
194 for (int y = 0; y < pathfindingMap.height; y++)
195 {
196 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200 // animate the distance "wave" to show how the pathfinding algorithm expands
201 // from the castle
202 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203 {
204 color = BLACK;
205 }
206 DrawCube(position, cellSize, 0.1f, cellSize, color);
207 }
208 }
209 }
210
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213 int16_t mapX, mapY;
214 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215 {
216 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217 return (Vector2){(float)-delta.x, (float)-delta.y};
218 }
219 // fallback to a simple gradient calculation
220 float n = PathFindingGetDistance(mapX, mapY - 1);
221 float s = PathFindingGetDistance(mapX, mapY + 1);
222 float w = PathFindingGetDistance(mapX - 1, mapY);
223 float e = PathFindingGetDistance(mapX + 1, mapY);
224 return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 void ProjectileInit()
8 {
9 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
10 {
11 projectiles[i] = (Projectile){0};
12 }
13 }
14
15 void ProjectileDraw()
16 {
17 for (int i = 0; i < projectileCount; i++)
18 {
19 Projectile projectile = projectiles[i];
20 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
21 {
22 continue;
23 }
24 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
25 if (transition >= 1.0f)
26 {
27 continue;
28 }
29 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
30 float x = position.x;
31 float y = position.y;
32 float dx = projectile.directionNormal.x;
33 float dy = projectile.directionNormal.y;
34 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
35 {
36 x -= dx * 0.1f;
37 y -= dy * 0.1f;
38 float size = 0.1f * d;
39 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
40 }
41 }
42 }
43
44 void ProjectileUpdate()
45 {
46 for (int i = 0; i < projectileCount; i++)
47 {
48 Projectile *projectile = &projectiles[i];
49 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
50 {
51 continue;
52 }
53 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
54 if (transition >= 1.0f)
55 {
56 projectile->projectileType = PROJECTILE_TYPE_NONE;
57 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
58 if (enemy)
59 {
60 EnemyAddDamage(enemy, projectile->damage);
61 }
62 continue;
63 }
64 }
65 }
66
67 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
68 {
69 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
70 {
71 Projectile *projectile = &projectiles[i];
72 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
73 {
74 projectile->projectileType = projectileType;
75 projectile->shootTime = gameTime.time;
76 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
77 projectile->damage = damage;
78 projectile->position = position;
79 projectile->target = target;
80 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
81 projectile->targetEnemy = EnemyGetId(enemy);
82 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
83 return projectile;
84 }
85 }
86 return 0;
87 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Particle particles[PARTICLE_MAX_COUNT];
5 static int particleCount = 0;
6
7 void ParticleInit()
8 {
9 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
10 {
11 particles[i] = (Particle){0};
12 }
13 particleCount = 0;
14 }
15
16 static void DrawExplosionParticle(Particle *particle, float transition)
17 {
18 float size = 1.2f * (1.0f - transition);
19 Color startColor = WHITE;
20 Color endColor = RED;
21 Color color = ColorLerp(startColor, endColor, transition);
22 DrawCube(particle->position, size, size, size, color);
23 }
24
25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
26 {
27 if (particleCount >= PARTICLE_MAX_COUNT)
28 {
29 return;
30 }
31
32 int index = -1;
33 for (int i = 0; i < particleCount; i++)
34 {
35 if (particles[i].particleType == PARTICLE_TYPE_NONE)
36 {
37 index = i;
38 break;
39 }
40 }
41
42 if (index == -1)
43 {
44 index = particleCount++;
45 }
46
47 Particle *particle = &particles[index];
48 particle->particleType = particleType;
49 particle->spawnTime = gameTime.time;
50 particle->lifetime = lifetime;
51 particle->position = position;
52 particle->velocity = velocity;
53 }
54
55 void ParticleUpdate()
56 {
57 for (int i = 0; i < particleCount; i++)
58 {
59 Particle *particle = &particles[i];
60 if (particle->particleType == PARTICLE_TYPE_NONE)
61 {
62 continue;
63 }
64
65 float age = gameTime.time - particle->spawnTime;
66
67 if (particle->lifetime > age)
68 {
69 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
70 }
71 else {
72 particle->particleType = PARTICLE_TYPE_NONE;
73 }
74 }
75 }
76
77 void ParticleDraw()
78 {
79 for (int i = 0; i < particleCount; i++)
80 {
81 Particle particle = particles[i];
82 if (particle.particleType == PARTICLE_TYPE_NONE)
83 {
84 continue;
85 }
86
87 float age = gameTime.time - particle.spawnTime;
88 float transition = age / particle.lifetime;
89 switch (particle.particleType)
90 {
91 case PARTICLE_TYPE_EXPLOSION:
92 DrawExplosionParticle(&particle, transition);
93 break;
94 default:
95 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
96 break;
97 }
98 }
99 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 Tower towers[TOWER_MAX_COUNT];
5 int towerCount = 0;
6
7 Model towerModels[TOWER_TYPE_COUNT];
8 Texture2D palette;
9
10 void TowerInit()
11 {
12 for (int i = 0; i < TOWER_MAX_COUNT; i++)
13 {
14 towers[i] = (Tower){0};
15 }
16 towerCount = 0;
17
18 // we'll use a palette texture to colorize the all buildings and environment art
19 palette = LoadTexture("data/palette.png");
20 // The texture uses gradients on very small space, so we'll enable bilinear filtering
21 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
22
23 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
24
25 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
26 {
27 if (towerModels[i].materials)
28 {
29 // assign the palette texture to the material of the model (0 is not used afaik)
30 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
31 }
32 }
33 }
34
35 static void TowerGunUpdate(Tower *tower)
36 {
37 if (tower->cooldown <= 0)
38 {
39 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
40 if (enemy)
41 {
42 tower->cooldown = 0.5f;
43 // shoot the enemy; determine future position of the enemy
44 float bulletSpeed = 1.0f;
45 float bulletDamage = 3.0f;
46 Vector2 velocity = enemy->simVelocity;
47 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
48 Vector2 towerPosition = {tower->x, tower->y};
49 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
50 for (int i = 0; i < 8; i++) {
51 velocity = enemy->simVelocity;
52 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
53 float distance = Vector2Distance(towerPosition, futurePosition);
54 float eta2 = distance / bulletSpeed;
55 if (fabs(eta - eta2) < 0.01f) {
56 break;
57 }
58 eta = (eta2 + eta) * 0.5f;
59 }
60 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
61 bulletSpeed, bulletDamage);
62 enemy->futureDamage += bulletDamage;
63 }
64 }
65 else
66 {
67 tower->cooldown -= gameTime.deltaTime;
68 }
69 }
70
71 Tower *TowerGetAt(int16_t x, int16_t y)
72 {
73 for (int i = 0; i < towerCount; i++)
74 {
75 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
76 {
77 return &towers[i];
78 }
79 }
80 return 0;
81 }
82
83 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
84 {
85 if (towerCount >= TOWER_MAX_COUNT)
86 {
87 return 0;
88 }
89
90 Tower *tower = TowerGetAt(x, y);
91 if (tower)
92 {
93 return 0;
94 }
95
96 tower = &towers[towerCount++];
97 tower->x = x;
98 tower->y = y;
99 tower->towerType = towerType;
100 tower->cooldown = 0.0f;
101 tower->damage = 0.0f;
102 return tower;
103 }
104
105 Tower *GetTowerByType(uint8_t towerType)
106 {
107 for (int i = 0; i < towerCount; i++)
108 {
109 if (towers[i].towerType == towerType)
110 {
111 return &towers[i];
112 }
113 }
114 return 0;
115 }
116
117 int GetTowerCosts(uint8_t towerType)
118 {
119 switch (towerType)
120 {
121 case TOWER_TYPE_BASE:
122 return 0;
123 case TOWER_TYPE_GUN:
124 return 6;
125 case TOWER_TYPE_WALL:
126 return 2;
127 }
128 return 0;
129 }
130
131 float TowerGetMaxHealth(Tower *tower)
132 {
133 switch (tower->towerType)
134 {
135 case TOWER_TYPE_BASE:
136 return 10.0f;
137 case TOWER_TYPE_GUN:
138 return 3.0f;
139 case TOWER_TYPE_WALL:
140 return 5.0f;
141 }
142 return 0.0f;
143 }
144
145 void TowerDraw()
146 {
147 for (int i = 0; i < towerCount; i++)
148 {
149 Tower tower = towers[i];
150 if (tower.towerType == TOWER_TYPE_NONE)
151 {
152 continue;
153 }
154
155 switch (tower.towerType)
156 {
157 case TOWER_TYPE_BASE:
158 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
159 break;
160 case TOWER_TYPE_GUN:
161 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
162 break;
163 default:
164 if (towerModels[tower.towerType].materials)
165 {
166 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
167 } else {
168 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
169 }
170 break;
171 }
172 }
173 }
174
175 void TowerUpdate()
176 {
177 for (int i = 0; i < towerCount; i++)
178 {
179 Tower *tower = &towers[i];
180 switch (tower->towerType)
181 {
182 case TOWER_TYPE_GUN:
183 TowerGunUpdate(tower);
184 break;
185 }
186 }
187 }
188
189 void TowerDrawHealthBars(Camera3D camera)
190 {
191 for (int i = 0; i < towerCount; i++)
192 {
193 Tower *tower = &towers[i];
194 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
195 {
196 continue;
197 }
198
199 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
200 float maxHealth = TowerGetMaxHealth(tower);
201 float health = maxHealth - tower->damage;
202 float healthRatio = health / maxHealth;
203
204 DrawHealthBar(camera, position, healthRatio, GREEN);
205 }
206 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
Place some walls to see the graphics:
There is now only a model for the walls; everything else is untouched:
Typically, prototyping assets are simple and not very detailed, but my weakness is that I tend to look at things from an artistic perspective and prefer to have things looking a little bit more polished.
So the setting is now good old medieval or fantasy; So the enemies should be some kind of orcs or goblins and the shooter ... how about adding archers to the towers, also as sprites? This means we need to combine 3D models with 2D sprites. Let's experiment a little
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Variables
7 GUIState guiState = {0};
8 GameTime gameTime = {0};
9
10 Level levels[] = {
11 [0] = {
12 .state = LEVEL_STATE_BUILDING,
13 .initialGold = 20,
14 .waves[0] = {
15 .enemyType = ENEMY_TYPE_MINION,
16 .wave = 0,
17 .count = 10,
18 .interval = 2.5f,
19 .delay = 1.0f,
20 .spawnPosition = {0, 6},
21 },
22 .waves[1] = {
23 .enemyType = ENEMY_TYPE_MINION,
24 .wave = 1,
25 .count = 20,
26 .interval = 1.5f,
27 .delay = 1.0f,
28 .spawnPosition = {0, 6},
29 },
30 .waves[2] = {
31 .enemyType = ENEMY_TYPE_MINION,
32 .wave = 2,
33 .count = 30,
34 .interval = 1.2f,
35 .delay = 1.0f,
36 .spawnPosition = {0, 6},
37 }
38 },
39 };
40
41 Level *currentLevel = levels;
42
43 //# Game
44
45 void InitLevel(Level *level)
46 {
47 TowerInit();
48 EnemyInit();
49 ProjectileInit();
50 ParticleInit();
51 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
52
53 level->placementMode = 0;
54 level->state = LEVEL_STATE_BUILDING;
55 level->nextState = LEVEL_STATE_NONE;
56 level->playerGold = level->initialGold;
57
58 Camera *camera = &level->camera;
59 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
60 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
61 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
62 camera->fovy = 45.0f;
63 camera->projection = CAMERA_PERSPECTIVE;
64 }
65
66 void DrawLevelHud(Level *level)
67 {
68 const char *text = TextFormat("Gold: %d", level->playerGold);
69 Font font = GetFontDefault();
70 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
71 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
72 }
73
74 void DrawLevelReportLostWave(Level *level)
75 {
76 BeginMode3D(level->camera);
77 DrawGrid(10, 1.0f);
78 TowerDraw();
79 EnemyDraw();
80 ProjectileDraw();
81 ParticleDraw();
82 guiState.isBlocked = 0;
83 EndMode3D();
84
85 TowerDrawHealthBars(level->camera);
86
87 const char *text = "Wave lost";
88 int textWidth = MeasureText(text, 20);
89 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
90
91 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
92 {
93 level->nextState = LEVEL_STATE_RESET;
94 }
95 }
96
97 int HasLevelNextWave(Level *level)
98 {
99 for (int i = 0; i < 10; i++)
100 {
101 EnemyWave *wave = &level->waves[i];
102 if (wave->wave == level->currentWave)
103 {
104 return 1;
105 }
106 }
107 return 0;
108 }
109
110 void DrawLevelReportWonWave(Level *level)
111 {
112 BeginMode3D(level->camera);
113 DrawGrid(10, 1.0f);
114 TowerDraw();
115 EnemyDraw();
116 ProjectileDraw();
117 ParticleDraw();
118 guiState.isBlocked = 0;
119 EndMode3D();
120
121 TowerDrawHealthBars(level->camera);
122
123 const char *text = "Wave won";
124 int textWidth = MeasureText(text, 20);
125 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
126
127
128 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
129 {
130 level->nextState = LEVEL_STATE_RESET;
131 }
132
133 if (HasLevelNextWave(level))
134 {
135 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
136 {
137 level->nextState = LEVEL_STATE_BUILDING;
138 }
139 }
140 else {
141 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
142 {
143 level->nextState = LEVEL_STATE_WON_LEVEL;
144 }
145 }
146 }
147
148 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
149 {
150 static ButtonState buttonStates[8] = {0};
151 int cost = GetTowerCosts(towerType);
152 const char *text = TextFormat("%s: %d", name, cost);
153 buttonStates[towerType].isSelected = level->placementMode == towerType;
154 buttonStates[towerType].isDisabled = level->playerGold < cost;
155 if (Button(text, x, y, width, height, &buttonStates[towerType]))
156 {
157 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
158 }
159 }
160
161 void DrawLevelBuildingState(Level *level)
162 {
163 BeginMode3D(level->camera);
164 DrawGrid(10, 1.0f);
165 TowerDraw();
166 EnemyDraw();
167 ProjectileDraw();
168 ParticleDraw();
169
170 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
171 float planeDistance = ray.position.y / -ray.direction.y;
172 float planeX = ray.direction.x * planeDistance + ray.position.x;
173 float planeY = ray.direction.z * planeDistance + ray.position.z;
174 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
175 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
176 if (level->placementMode && !guiState.isBlocked)
177 {
178 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
179 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
180 {
181 if (TowerTryAdd(level->placementMode, mapX, mapY))
182 {
183 level->playerGold -= GetTowerCosts(level->placementMode);
184 level->placementMode = TOWER_TYPE_NONE;
185 }
186 }
187 }
188
189 guiState.isBlocked = 0;
190
191 EndMode3D();
192
193 TowerDrawHealthBars(level->camera);
194
195 static ButtonState buildWallButtonState = {0};
196 static ButtonState buildGunButtonState = {0};
197 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
198 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
199
200 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
201 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
202
203 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
204 {
205 level->nextState = LEVEL_STATE_RESET;
206 }
207
208 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
209 {
210 level->nextState = LEVEL_STATE_BATTLE;
211 }
212
213 const char *text = "Building phase";
214 int textWidth = MeasureText(text, 20);
215 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
216 }
217
218 void InitBattleStateConditions(Level *level)
219 {
220 level->state = LEVEL_STATE_BATTLE;
221 level->nextState = LEVEL_STATE_NONE;
222 level->waveEndTimer = 0.0f;
223 for (int i = 0; i < 10; i++)
224 {
225 EnemyWave *wave = &level->waves[i];
226 wave->spawned = 0;
227 wave->timeToSpawnNext = wave->delay;
228 }
229 }
230
231 void DrawLevelBattleState(Level *level)
232 {
233 BeginMode3D(level->camera);
234 DrawGrid(10, 1.0f);
235 TowerDraw();
236 EnemyDraw();
237 ProjectileDraw();
238 ParticleDraw();
239 guiState.isBlocked = 0;
240 EndMode3D();
241
242 EnemyDrawHealthbars(level->camera);
243 TowerDrawHealthBars(level->camera);
244
245 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
246 {
247 level->nextState = LEVEL_STATE_RESET;
248 }
249
250 int maxCount = 0;
251 int remainingCount = 0;
252 for (int i = 0; i < 10; i++)
253 {
254 EnemyWave *wave = &level->waves[i];
255 if (wave->wave != level->currentWave)
256 {
257 continue;
258 }
259 maxCount += wave->count;
260 remainingCount += wave->count - wave->spawned;
261 }
262 int aliveCount = EnemyCount();
263 remainingCount += aliveCount;
264
265 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
266 int textWidth = MeasureText(text, 20);
267 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
268 }
269
270 void DrawLevel(Level *level)
271 {
272 switch (level->state)
273 {
274 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
275 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
276 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
277 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
278 default: break;
279 }
280
281 DrawLevelHud(level);
282 }
283
284 void UpdateLevel(Level *level)
285 {
286 if (level->state == LEVEL_STATE_BATTLE)
287 {
288 int activeWaves = 0;
289 for (int i = 0; i < 10; i++)
290 {
291 EnemyWave *wave = &level->waves[i];
292 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
293 {
294 continue;
295 }
296 activeWaves++;
297 wave->timeToSpawnNext -= gameTime.deltaTime;
298 if (wave->timeToSpawnNext <= 0.0f)
299 {
300 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
301 if (enemy)
302 {
303 wave->timeToSpawnNext = wave->interval;
304 wave->spawned++;
305 }
306 }
307 }
308 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
309 level->waveEndTimer += gameTime.deltaTime;
310 if (level->waveEndTimer >= 2.0f)
311 {
312 level->nextState = LEVEL_STATE_LOST_WAVE;
313 }
314 }
315 else if (activeWaves == 0 && EnemyCount() == 0)
316 {
317 level->waveEndTimer += gameTime.deltaTime;
318 if (level->waveEndTimer >= 2.0f)
319 {
320 level->nextState = LEVEL_STATE_WON_WAVE;
321 }
322 }
323 }
324
325 PathFindingMapUpdate();
326 EnemyUpdate();
327 TowerUpdate();
328 ProjectileUpdate();
329 ParticleUpdate();
330
331 if (level->nextState == LEVEL_STATE_RESET)
332 {
333 InitLevel(level);
334 }
335
336 if (level->nextState == LEVEL_STATE_BATTLE)
337 {
338 InitBattleStateConditions(level);
339 }
340
341 if (level->nextState == LEVEL_STATE_WON_WAVE)
342 {
343 level->currentWave++;
344 level->state = LEVEL_STATE_WON_WAVE;
345 }
346
347 if (level->nextState == LEVEL_STATE_LOST_WAVE)
348 {
349 level->state = LEVEL_STATE_LOST_WAVE;
350 }
351
352 if (level->nextState == LEVEL_STATE_BUILDING)
353 {
354 level->state = LEVEL_STATE_BUILDING;
355 }
356
357 if (level->nextState == LEVEL_STATE_WON_LEVEL)
358 {
359 // make something of this later
360 InitLevel(level);
361 }
362
363 level->nextState = LEVEL_STATE_NONE;
364 }
365
366 float nextSpawnTime = 0.0f;
367
368 void ResetGame()
369 {
370 InitLevel(currentLevel);
371 }
372
373 void InitGame()
374 {
375 TowerInit();
376 EnemyInit();
377 ProjectileInit();
378 ParticleInit();
379 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
380
381 currentLevel = levels;
382 InitLevel(currentLevel);
383 }
384
385 //# Immediate GUI functions
386
387 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor)
388 {
389 const float healthBarWidth = 40.0f;
390 const float healthBarHeight = 6.0f;
391 const float healthBarOffset = 15.0f;
392 const float inset = 2.0f;
393 const float innerWidth = healthBarWidth - inset * 2;
394 const float innerHeight = healthBarHeight - inset * 2;
395
396 Vector2 screenPos = GetWorldToScreen(position, camera);
397 float centerX = screenPos.x - healthBarWidth * 0.5f;
398 float topY = screenPos.y - healthBarOffset;
399 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
400 float healthWidth = innerWidth * healthRatio;
401 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
402 }
403
404 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
405 {
406 Rectangle bounds = {x, y, width, height};
407 int isPressed = 0;
408 int isSelected = state && state->isSelected;
409 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
410 {
411 Color color = isSelected ? DARKGRAY : GRAY;
412 DrawRectangle(x, y, width, height, color);
413 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
414 {
415 isPressed = 1;
416 }
417 guiState.isBlocked = 1;
418 }
419 else
420 {
421 Color color = isSelected ? WHITE : LIGHTGRAY;
422 DrawRectangle(x, y, width, height, color);
423 }
424 Font font = GetFontDefault();
425 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
426 Color textColor = state->isDisabled ? GRAY : BLACK;
427 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
428 return isPressed;
429 }
430
431 //# Main game loop
432
433 void GameUpdate()
434 {
435 float dt = GetFrameTime();
436 // cap maximum delta time to 0.1 seconds to prevent large time steps
437 if (dt > 0.1f) dt = 0.1f;
438 gameTime.time += dt;
439 gameTime.deltaTime = dt;
440
441 UpdateLevel(currentLevel);
442 }
443
444 int main(void)
445 {
446 int screenWidth, screenHeight;
447 GetPreferredSize(&screenWidth, &screenHeight);
448 InitWindow(screenWidth, screenHeight, "Tower defense");
449 SetTargetFPS(30);
450
451 InitGame();
452
453 while (!WindowShouldClose())
454 {
455 if (IsPaused()) {
456 // canvas is not visible in browser - do nothing
457 continue;
458 }
459
460 BeginDrawing();
461 ClearBackground(DARKBLUE);
462
463 GameUpdate();
464 DrawLevel(currentLevel);
465
466 EndDrawing();
467 }
468
469 CloseWindow();
470
471 return 0;
472 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 } Particle;
28
29 #define TOWER_MAX_COUNT 400
30 #define TOWER_TYPE_NONE 0
31 #define TOWER_TYPE_BASE 1
32 #define TOWER_TYPE_GUN 2
33 #define TOWER_TYPE_WALL 3
34 #define TOWER_TYPE_COUNT 4
35
36 typedef struct Tower
37 {
38 int16_t x, y;
39 uint8_t towerType;
40 float cooldown;
41 float damage;
42 } Tower;
43
44 typedef struct GameTime
45 {
46 float time;
47 float deltaTime;
48 } GameTime;
49
50 typedef struct ButtonState {
51 char isSelected;
52 char isDisabled;
53 } ButtonState;
54
55 typedef struct GUIState {
56 int isBlocked;
57 } GUIState;
58
59 typedef enum LevelState
60 {
61 LEVEL_STATE_NONE,
62 LEVEL_STATE_BUILDING,
63 LEVEL_STATE_BATTLE,
64 LEVEL_STATE_WON_WAVE,
65 LEVEL_STATE_LOST_WAVE,
66 LEVEL_STATE_WON_LEVEL,
67 LEVEL_STATE_RESET,
68 } LevelState;
69
70 typedef struct EnemyWave {
71 uint8_t enemyType;
72 uint8_t wave;
73 uint16_t count;
74 float interval;
75 float delay;
76 Vector2 spawnPosition;
77
78 uint16_t spawned;
79 float timeToSpawnNext;
80 } EnemyWave;
81
82 typedef struct Level
83 {
84 LevelState state;
85 LevelState nextState;
86 Camera3D camera;
87 int placementMode;
88
89 int initialGold;
90 int playerGold;
91
92 EnemyWave waves[10];
93 int currentWave;
94 float waveEndTimer;
95 } Level;
96
97 typedef struct DeltaSrc
98 {
99 char x, y;
100 } DeltaSrc;
101
102 typedef struct PathfindingMap
103 {
104 int width, height;
105 float scale;
106 float *distances;
107 long *towerIndex;
108 DeltaSrc *deltaSrc;
109 float maxDistance;
110 Matrix toMapSpace;
111 Matrix toWorldSpace;
112 } PathfindingMap;
113
114 // when we execute the pathfinding algorithm, we need to store the active nodes
115 // in a queue. Each node has a position, a distance from the start, and the
116 // position of the node that we came from.
117 typedef struct PathfindingNode
118 {
119 int16_t x, y, fromX, fromY;
120 float distance;
121 } PathfindingNode;
122
123 typedef struct EnemyId
124 {
125 uint16_t index;
126 uint16_t generation;
127 } EnemyId;
128
129 typedef struct EnemyClassConfig
130 {
131 float speed;
132 float health;
133 float radius;
134 float maxAcceleration;
135 float requiredContactTime;
136 float explosionDamage;
137 float explosionRange;
138 float explosionPushbackPower;
139 int goldValue;
140 } EnemyClassConfig;
141
142 typedef struct Enemy
143 {
144 int16_t currentX, currentY;
145 int16_t nextX, nextY;
146 Vector2 simPosition;
147 Vector2 simVelocity;
148 uint16_t generation;
149 float startMovingTime;
150 float damage, futureDamage;
151 float contactTime;
152 uint8_t enemyType;
153 uint8_t movePathCount;
154 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
155 } Enemy;
156
157 #define PROJECTILE_MAX_COUNT 1200
158 #define PROJECTILE_TYPE_NONE 0
159 #define PROJECTILE_TYPE_BULLET 1
160
161 typedef struct Projectile
162 {
163 uint8_t projectileType;
164 float shootTime;
165 float arrivalTime;
166 float damage;
167 Vector2 position;
168 Vector2 target;
169 Vector2 directionNormal;
170 EnemyId targetEnemy;
171 } Projectile;
172
173 //# Function declarations
174 float TowerGetMaxHealth(Tower *tower);
175 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
176 int EnemyAddDamage(Enemy *enemy, float damage);
177
178 //# Enemy functions
179 void EnemyInit();
180 void EnemyDraw();
181 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
182 void EnemyUpdate();
183 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
184 float EnemyGetMaxHealth(Enemy *enemy);
185 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
186 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
187 EnemyId EnemyGetId(Enemy *enemy);
188 Enemy *EnemyTryResolve(EnemyId enemyId);
189 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
190 int EnemyAddDamage(Enemy *enemy, float damage);
191 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
192 int EnemyCount();
193 void EnemyDrawHealthbars(Camera3D camera);
194
195 //# Tower functions
196 void TowerInit();
197 Tower *TowerGetAt(int16_t x, int16_t y);
198 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
199 Tower *GetTowerByType(uint8_t towerType);
200 int GetTowerCosts(uint8_t towerType);
201 float TowerGetMaxHealth(Tower *tower);
202 void TowerDraw();
203 void TowerUpdate();
204 void TowerDrawHealthBars(Camera3D camera);
205
206 //# Particles
207 void ParticleInit();
208 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
209 void ParticleUpdate();
210 void ParticleDraw();
211
212 //# Projectiles
213 void ProjectileInit();
214 void ProjectileDraw();
215 void ProjectileUpdate();
216 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage);
217
218 //# Pathfinding map
219 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
220 float PathFindingGetDistance(int mapX, int mapY);
221 Vector2 PathFindingGetGradient(Vector3 world);
222 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
223 void PathFindingMapUpdate();
224 void PathFindingMapDraw();
225
226 //# UI
227 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor);
228
229 //# variables
230 extern Level *currentLevel;
231 extern Enemy enemies[ENEMY_MAX_COUNT];
232 extern int enemyCount;
233 extern EnemyClassConfig enemyClassConfigs[];
234
235 extern GUIState guiState;
236 extern GameTime gameTime;
237 extern Tower towers[TOWER_MAX_COUNT];
238 extern int towerCount;
239
240 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 void EnemyInit()
24 {
25 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
26 {
27 enemies[i] = (Enemy){0};
28 }
29 enemyCount = 0;
30 }
31
32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
33 {
34 return enemyClassConfigs[enemy->enemyType].speed;
35 }
36
37 float EnemyGetMaxHealth(Enemy *enemy)
38 {
39 return enemyClassConfigs[enemy->enemyType].health;
40 }
41
42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
43 {
44 int16_t castleX = 0;
45 int16_t castleY = 0;
46 int16_t dx = castleX - currentX;
47 int16_t dy = castleY - currentY;
48 if (dx == 0 && dy == 0)
49 {
50 *nextX = currentX;
51 *nextY = currentY;
52 return 1;
53 }
54 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
55
56 if (gradient.x == 0 && gradient.y == 0)
57 {
58 *nextX = currentX;
59 *nextY = currentY;
60 return 1;
61 }
62
63 if (fabsf(gradient.x) > fabsf(gradient.y))
64 {
65 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
66 *nextY = currentY;
67 return 0;
68 }
69 *nextX = currentX;
70 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
71 return 0;
72 }
73
74
75 // this function predicts the movement of the unit for the next deltaT seconds
76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
77 {
78 const float pointReachedDistance = 0.25f;
79 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
80 const float maxSimStepTime = 0.015625f;
81
82 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
83 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
84 int16_t nextX = enemy->nextX;
85 int16_t nextY = enemy->nextY;
86 Vector2 position = enemy->simPosition;
87 int passedCount = 0;
88 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
89 {
90 float stepTime = fminf(deltaT - t, maxSimStepTime);
91 Vector2 target = (Vector2){nextX, nextY};
92 float speed = Vector2Length(*velocity);
93 // draw the target position for debugging
94 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
95 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
96 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
97 {
98 // we reached the target position, let's move to the next waypoint
99 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100 target = (Vector2){nextX, nextY};
101 // track how many waypoints we passed
102 passedCount++;
103 }
104
105 // acceleration towards the target
106 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108 *velocity = Vector2Add(*velocity, acceleration);
109
110 // limit the speed to the maximum speed
111 if (speed > maxSpeed)
112 {
113 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114 }
115
116 // move the enemy
117 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118 }
119
120 if (waypointPassedCount)
121 {
122 (*waypointPassedCount) = passedCount;
123 }
124
125 return position;
126 }
127
128 void EnemyDraw()
129 {
130 for (int i = 0; i < enemyCount; i++)
131 {
132 Enemy enemy = enemies[i];
133 if (enemy.enemyType == ENEMY_TYPE_NONE)
134 {
135 continue;
136 }
137
138 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139
140 if (enemy.movePathCount > 0)
141 {
142 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144 }
145 for (int j = 1; j < enemy.movePathCount; j++)
146 {
147 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149 DrawLine3D(p, q, GREEN);
150 }
151
152 switch (enemy.enemyType)
153 {
154 case ENEMY_TYPE_MINION:
155 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156 break;
157 }
158 }
159 }
160
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163 // damage the tower
164 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167 float explosionRange2 = explosionRange * explosionRange;
168 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169 // explode the enemy
170 if (tower->damage >= TowerGetMaxHealth(tower))
171 {
172 tower->towerType = TOWER_TYPE_NONE;
173 }
174
175 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
176 explosionSource,
177 (Vector3){0, 0.1f, 0}, 1.0f);
178
179 enemy->enemyType = ENEMY_TYPE_NONE;
180
181 // push back enemies & dealing damage
182 for (int i = 0; i < enemyCount; i++)
183 {
184 Enemy *other = &enemies[i];
185 if (other->enemyType == ENEMY_TYPE_NONE)
186 {
187 continue;
188 }
189 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190 if (distanceSqr > 0 && distanceSqr < explosionRange2)
191 {
192 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194 EnemyAddDamage(other, explosionDamge);
195 }
196 }
197 }
198
199 void EnemyUpdate()
200 {
201 const float castleX = 0;
202 const float castleY = 0;
203 const float maxPathDistance2 = 0.25f * 0.25f;
204
205 for (int i = 0; i < enemyCount; i++)
206 {
207 Enemy *enemy = &enemies[i];
208 if (enemy->enemyType == ENEMY_TYPE_NONE)
209 {
210 continue;
211 }
212
213 int waypointPassedCount = 0;
214 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215 enemy->startMovingTime = gameTime.time;
216 // track path of unit
217 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218 {
219 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220 {
221 enemy->movePath[j] = enemy->movePath[j - 1];
222 }
223 enemy->movePath[0] = enemy->simPosition;
224 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225 {
226 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227 }
228 }
229
230 if (waypointPassedCount > 0)
231 {
232 enemy->currentX = enemy->nextX;
233 enemy->currentY = enemy->nextY;
234 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236 {
237 // enemy reached the castle; remove it
238 enemy->enemyType = ENEMY_TYPE_NONE;
239 continue;
240 }
241 }
242 }
243
244 // handle collisions between enemies
245 for (int i = 0; i < enemyCount - 1; i++)
246 {
247 Enemy *enemyA = &enemies[i];
248 if (enemyA->enemyType == ENEMY_TYPE_NONE)
249 {
250 continue;
251 }
252 for (int j = i + 1; j < enemyCount; j++)
253 {
254 Enemy *enemyB = &enemies[j];
255 if (enemyB->enemyType == ENEMY_TYPE_NONE)
256 {
257 continue;
258 }
259 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262 float radiusSum = radiusA + radiusB;
263 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264 {
265 // collision
266 float distance = sqrtf(distanceSqr);
267 float overlap = radiusSum - distance;
268 // move the enemies apart, but softly; if we have a clog of enemies,
269 // moving them perfectly apart can cause them to jitter
270 float positionCorrection = overlap / 5.0f;
271 Vector2 direction = (Vector2){
272 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276 }
277 }
278 }
279
280 // handle collisions between enemies and towers
281 for (int i = 0; i < enemyCount; i++)
282 {
283 Enemy *enemy = &enemies[i];
284 if (enemy->enemyType == ENEMY_TYPE_NONE)
285 {
286 continue;
287 }
288 enemy->contactTime -= gameTime.deltaTime;
289 if (enemy->contactTime < 0.0f)
290 {
291 enemy->contactTime = 0.0f;
292 }
293
294 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295 // linear search over towers; could be optimized by using path finding tower map,
296 // but for now, we keep it simple
297 for (int j = 0; j < towerCount; j++)
298 {
299 Tower *tower = &towers[j];
300 if (tower->towerType == TOWER_TYPE_NONE)
301 {
302 continue;
303 }
304 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306 if (distanceSqr > combinedRadius * combinedRadius)
307 {
308 continue;
309 }
310 // potential collision; square / circle intersection
311 float dx = tower->x - enemy->simPosition.x;
312 float dy = tower->y - enemy->simPosition.y;
313 float absDx = fabsf(dx);
314 float absDy = fabsf(dy);
315 Vector3 contactPoint = {0};
316 if (absDx <= 0.5f && absDx <= absDy) {
317 // vertical collision; push the enemy out horizontally
318 float overlap = enemyRadius + 0.5f - absDy;
319 if (overlap < 0.0f)
320 {
321 continue;
322 }
323 float direction = dy > 0.0f ? -1.0f : 1.0f;
324 enemy->simPosition.y += direction * overlap;
325 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
326 }
327 else if (absDy <= 0.5f && absDy <= absDx)
328 {
329 // horizontal collision; push the enemy out vertically
330 float overlap = enemyRadius + 0.5f - absDx;
331 if (overlap < 0.0f)
332 {
333 continue;
334 }
335 float direction = dx > 0.0f ? -1.0f : 1.0f;
336 enemy->simPosition.x += direction * overlap;
337 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
338 }
339 else
340 {
341 // possible collision with a corner
342 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
343 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
344 float cornerX = tower->x + cornerDX;
345 float cornerY = tower->y + cornerDY;
346 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
347 if (cornerDistanceSqr > enemyRadius * enemyRadius)
348 {
349 continue;
350 }
351 // push the enemy out along the diagonal
352 float cornerDistance = sqrtf(cornerDistanceSqr);
353 float overlap = enemyRadius - cornerDistance;
354 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
355 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
356 enemy->simPosition.x -= directionX * overlap;
357 enemy->simPosition.y -= directionY * overlap;
358 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
359 }
360
361 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
362 {
363 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
364 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
365 {
366 EnemyTriggerExplode(enemy, tower, contactPoint);
367 }
368 }
369 }
370 }
371 }
372
373 EnemyId EnemyGetId(Enemy *enemy)
374 {
375 return (EnemyId){enemy - enemies, enemy->generation};
376 }
377
378 Enemy *EnemyTryResolve(EnemyId enemyId)
379 {
380 if (enemyId.index >= ENEMY_MAX_COUNT)
381 {
382 return 0;
383 }
384 Enemy *enemy = &enemies[enemyId.index];
385 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
386 {
387 return 0;
388 }
389 return enemy;
390 }
391
392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
393 {
394 Enemy *spawn = 0;
395 for (int i = 0; i < enemyCount; i++)
396 {
397 Enemy *enemy = &enemies[i];
398 if (enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 spawn = enemy;
401 break;
402 }
403 }
404
405 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
406 {
407 spawn = &enemies[enemyCount++];
408 }
409
410 if (spawn)
411 {
412 spawn->currentX = currentX;
413 spawn->currentY = currentY;
414 spawn->nextX = currentX;
415 spawn->nextY = currentY;
416 spawn->simPosition = (Vector2){currentX, currentY};
417 spawn->simVelocity = (Vector2){0, 0};
418 spawn->enemyType = enemyType;
419 spawn->startMovingTime = gameTime.time;
420 spawn->damage = 0.0f;
421 spawn->futureDamage = 0.0f;
422 spawn->generation++;
423 spawn->movePathCount = 0;
424 }
425
426 return spawn;
427 }
428
429 int EnemyAddDamage(Enemy *enemy, float damage)
430 {
431 enemy->damage += damage;
432 if (enemy->damage >= EnemyGetMaxHealth(enemy))
433 {
434 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
435 enemy->enemyType = ENEMY_TYPE_NONE;
436 return 1;
437 }
438
439 return 0;
440 }
441
442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
443 {
444 int16_t castleX = 0;
445 int16_t castleY = 0;
446 Enemy* closest = 0;
447 int16_t closestDistance = 0;
448 float range2 = range * range;
449 for (int i = 0; i < enemyCount; i++)
450 {
451 Enemy* enemy = &enemies[i];
452 if (enemy->enemyType == ENEMY_TYPE_NONE)
453 {
454 continue;
455 }
456 float maxHealth = EnemyGetMaxHealth(enemy);
457 if (enemy->futureDamage >= maxHealth)
458 {
459 // ignore enemies that will die soon
460 continue;
461 }
462 int16_t dx = castleX - enemy->currentX;
463 int16_t dy = castleY - enemy->currentY;
464 int16_t distance = abs(dx) + abs(dy);
465 if (!closest || distance < closestDistance)
466 {
467 float tdx = towerX - enemy->currentX;
468 float tdy = towerY - enemy->currentY;
469 float tdistance2 = tdx * tdx + tdy * tdy;
470 if (tdistance2 <= range2)
471 {
472 closest = enemy;
473 closestDistance = distance;
474 }
475 }
476 }
477 return closest;
478 }
479
480 int EnemyCount()
481 {
482 int count = 0;
483 for (int i = 0; i < enemyCount; i++)
484 {
485 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
486 {
487 count++;
488 }
489 }
490 return count;
491 }
492
493 void EnemyDrawHealthbars(Camera3D camera)
494 {
495 for (int i = 0; i < enemyCount; i++)
496 {
497 Enemy *enemy = &enemies[i];
498 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
499 {
500 continue;
501 }
502 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
503 float maxHealth = EnemyGetMaxHealth(enemy);
504 float health = maxHealth - enemy->damage;
505 float healthRatio = health / maxHealth;
506
507 DrawHealthBar(camera, position, healthRatio, GREEN);
508 }
509 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate()
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < towerCount; i++)
131 {
132 Tower *tower = &towers[i];
133 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134 {
135 continue;
136 }
137 int16_t mapX, mapY;
138 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139 // this would not work correctly and needs to be refined to allow towers covering multiple cells
140 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141 // one cell. For now.
142 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143 {
144 continue;
145 }
146 int index = mapY * width + mapX;
147 pathfindingMap.towerIndex[index] = i;
148 }
149
150 // we start at the castle and add the castle to the queue
151 pathfindingMap.maxDistance = 0.0f;
152 pathfindingNodeQueueCount = 0;
153 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154 PathfindingNode *node = 0;
155 while ((node = PathFindingNodePop()))
156 {
157 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158 {
159 continue;
160 }
161 int index = node->y * width + node->x;
162 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163 {
164 continue;
165 }
166
167 int deltaX = node->x - node->fromX;
168 int deltaY = node->y - node->fromY;
169 // even if the cell is blocked by a tower, we still may want to store the direction
170 // (though this might not be needed, IDK right now)
171 pathfindingMap.deltaSrc[index].x = (char) deltaX;
172 pathfindingMap.deltaSrc[index].y = (char) deltaY;
173
174 // we skip nodes that are blocked by towers
175 if (pathfindingMap.towerIndex[index] >= 0)
176 {
177 node->distance += 8.0f;
178 }
179 pathfindingMap.distances[index] = node->distance;
180 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185 }
186 }
187
188 void PathFindingMapDraw()
189 {
190 float cellSize = pathfindingMap.scale * 0.9f;
191 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192 for (int x = 0; x < pathfindingMap.width; x++)
193 {
194 for (int y = 0; y < pathfindingMap.height; y++)
195 {
196 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200 // animate the distance "wave" to show how the pathfinding algorithm expands
201 // from the castle
202 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203 {
204 color = BLACK;
205 }
206 DrawCube(position, cellSize, 0.1f, cellSize, color);
207 }
208 }
209 }
210
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213 int16_t mapX, mapY;
214 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215 {
216 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217 return (Vector2){(float)-delta.x, (float)-delta.y};
218 }
219 // fallback to a simple gradient calculation
220 float n = PathFindingGetDistance(mapX, mapY - 1);
221 float s = PathFindingGetDistance(mapX, mapY + 1);
222 float w = PathFindingGetDistance(mapX - 1, mapY);
223 float e = PathFindingGetDistance(mapX + 1, mapY);
224 return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 void ProjectileInit()
8 {
9 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
10 {
11 projectiles[i] = (Projectile){0};
12 }
13 }
14
15 void ProjectileDraw()
16 {
17 for (int i = 0; i < projectileCount; i++)
18 {
19 Projectile projectile = projectiles[i];
20 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
21 {
22 continue;
23 }
24 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
25 if (transition >= 1.0f)
26 {
27 continue;
28 }
29 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
30 float x = position.x;
31 float y = position.y;
32 float dx = projectile.directionNormal.x;
33 float dy = projectile.directionNormal.y;
34 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
35 {
36 x -= dx * 0.1f;
37 y -= dy * 0.1f;
38 float size = 0.1f * d;
39 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
40 }
41 }
42 }
43
44 void ProjectileUpdate()
45 {
46 for (int i = 0; i < projectileCount; i++)
47 {
48 Projectile *projectile = &projectiles[i];
49 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
50 {
51 continue;
52 }
53 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
54 if (transition >= 1.0f)
55 {
56 projectile->projectileType = PROJECTILE_TYPE_NONE;
57 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
58 if (enemy)
59 {
60 EnemyAddDamage(enemy, projectile->damage);
61 }
62 continue;
63 }
64 }
65 }
66
67 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
68 {
69 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
70 {
71 Projectile *projectile = &projectiles[i];
72 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
73 {
74 projectile->projectileType = projectileType;
75 projectile->shootTime = gameTime.time;
76 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
77 projectile->damage = damage;
78 projectile->position = position;
79 projectile->target = target;
80 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
81 projectile->targetEnemy = EnemyGetId(enemy);
82 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
83 return projectile;
84 }
85 }
86 return 0;
87 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Particle particles[PARTICLE_MAX_COUNT];
5 static int particleCount = 0;
6
7 void ParticleInit()
8 {
9 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
10 {
11 particles[i] = (Particle){0};
12 }
13 particleCount = 0;
14 }
15
16 static void DrawExplosionParticle(Particle *particle, float transition)
17 {
18 float size = 1.2f * (1.0f - transition);
19 Color startColor = WHITE;
20 Color endColor = RED;
21 Color color = ColorLerp(startColor, endColor, transition);
22 DrawCube(particle->position, size, size, size, color);
23 }
24
25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
26 {
27 if (particleCount >= PARTICLE_MAX_COUNT)
28 {
29 return;
30 }
31
32 int index = -1;
33 for (int i = 0; i < particleCount; i++)
34 {
35 if (particles[i].particleType == PARTICLE_TYPE_NONE)
36 {
37 index = i;
38 break;
39 }
40 }
41
42 if (index == -1)
43 {
44 index = particleCount++;
45 }
46
47 Particle *particle = &particles[index];
48 particle->particleType = particleType;
49 particle->spawnTime = gameTime.time;
50 particle->lifetime = lifetime;
51 particle->position = position;
52 particle->velocity = velocity;
53 }
54
55 void ParticleUpdate()
56 {
57 for (int i = 0; i < particleCount; i++)
58 {
59 Particle *particle = &particles[i];
60 if (particle->particleType == PARTICLE_TYPE_NONE)
61 {
62 continue;
63 }
64
65 float age = gameTime.time - particle->spawnTime;
66
67 if (particle->lifetime > age)
68 {
69 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
70 }
71 else {
72 particle->particleType = PARTICLE_TYPE_NONE;
73 }
74 }
75 }
76
77 void ParticleDraw()
78 {
79 for (int i = 0; i < particleCount; i++)
80 {
81 Particle particle = particles[i];
82 if (particle.particleType == PARTICLE_TYPE_NONE)
83 {
84 continue;
85 }
86
87 float age = gameTime.time - particle.spawnTime;
88 float transition = age / particle.lifetime;
89 switch (particle.particleType)
90 {
91 case PARTICLE_TYPE_EXPLOSION:
92 DrawExplosionParticle(&particle, transition);
93 break;
94 default:
95 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
96 break;
97 }
98 }
99 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 Tower towers[TOWER_MAX_COUNT];
5 int towerCount = 0;
6
7 Model towerModels[TOWER_TYPE_COUNT];
8 Texture2D palette, spriteSheet;
9
10 // a unit that uses sprites to be drawn
11 typedef struct SpriteUnit
12 {
13 Rectangle srcRect;
14 Vector2 offset;
15 } SpriteUnit;
16
17 // definition of our archer unit
18 SpriteUnit archerUnit = {
19 .srcRect = {0, 0, 16, 16},
20 .offset = {7, 1},
21 };
22
23 void DrawSpriteUnit(SpriteUnit unit, Vector3 position)
24 {
25 Camera3D camera = currentLevel->camera;
26 float size = 0.5f;
27 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size };
28 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
29 // we want the sprite to face the camera, so we need to calculate the up vector
30 Vector3 forward = Vector3Subtract(camera.target, camera.position);
31 Vector3 up = {0, 1, 0};
32 Vector3 right = Vector3CrossProduct(forward, up);
33 up = Vector3Normalize(Vector3CrossProduct(right, forward));
34 DrawBillboardPro(camera, spriteSheet, unit.srcRect, position, up, scale, offset, 0, WHITE);
35 }
36
37 void TowerInit()
38 {
39 for (int i = 0; i < TOWER_MAX_COUNT; i++)
40 {
41 towers[i] = (Tower){0};
42 }
43 towerCount = 0;
44
45 // load a sprite sheet that contains all units
46 spriteSheet = LoadTexture("data/spritesheet.png");
47
48 // we'll use a palette texture to colorize the all buildings and environment art
49 palette = LoadTexture("data/palette.png");
50 // The texture uses gradients on very small space, so we'll enable bilinear filtering
51 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
52
53 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
54
55 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
56 {
57 if (towerModels[i].materials)
58 {
59 // assign the palette texture to the material of the model (0 is not used afaik)
60 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
61 }
62 }
63 }
64
65 static void TowerGunUpdate(Tower *tower)
66 {
67 if (tower->cooldown <= 0)
68 {
69 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
70 if (enemy)
71 {
72 tower->cooldown = 0.5f;
73 // shoot the enemy; determine future position of the enemy
74 float bulletSpeed = 1.0f;
75 float bulletDamage = 3.0f;
76 Vector2 velocity = enemy->simVelocity;
77 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
78 Vector2 towerPosition = {tower->x, tower->y};
79 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
80 for (int i = 0; i < 8; i++) {
81 velocity = enemy->simVelocity;
82 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
83 float distance = Vector2Distance(towerPosition, futurePosition);
84 float eta2 = distance / bulletSpeed;
85 if (fabs(eta - eta2) < 0.01f) {
86 break;
87 }
88 eta = (eta2 + eta) * 0.5f;
89 }
90 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
91 bulletSpeed, bulletDamage);
92 enemy->futureDamage += bulletDamage;
93 }
94 }
95 else
96 {
97 tower->cooldown -= gameTime.deltaTime;
98 }
99 }
100
101 Tower *TowerGetAt(int16_t x, int16_t y)
102 {
103 for (int i = 0; i < towerCount; i++)
104 {
105 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
106 {
107 return &towers[i];
108 }
109 }
110 return 0;
111 }
112
113 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
114 {
115 if (towerCount >= TOWER_MAX_COUNT)
116 {
117 return 0;
118 }
119
120 Tower *tower = TowerGetAt(x, y);
121 if (tower)
122 {
123 return 0;
124 }
125
126 tower = &towers[towerCount++];
127 tower->x = x;
128 tower->y = y;
129 tower->towerType = towerType;
130 tower->cooldown = 0.0f;
131 tower->damage = 0.0f;
132 return tower;
133 }
134
135 Tower *GetTowerByType(uint8_t towerType)
136 {
137 for (int i = 0; i < towerCount; i++)
138 {
139 if (towers[i].towerType == towerType)
140 {
141 return &towers[i];
142 }
143 }
144 return 0;
145 }
146
147 int GetTowerCosts(uint8_t towerType)
148 {
149 switch (towerType)
150 {
151 case TOWER_TYPE_BASE:
152 return 0;
153 case TOWER_TYPE_GUN:
154 return 6;
155 case TOWER_TYPE_WALL:
156 return 2;
157 }
158 return 0;
159 }
160
161 float TowerGetMaxHealth(Tower *tower)
162 {
163 switch (tower->towerType)
164 {
165 case TOWER_TYPE_BASE:
166 return 10.0f;
167 case TOWER_TYPE_GUN:
168 return 3.0f;
169 case TOWER_TYPE_WALL:
170 return 5.0f;
171 }
172 return 0.0f;
173 }
174
175 void TowerDraw()
176 {
177 for (int i = 0; i < towerCount; i++)
178 {
179 Tower tower = towers[i];
180 if (tower.towerType == TOWER_TYPE_NONE)
181 {
182 continue;
183 }
184
185 switch (tower.towerType)
186 {
187 case TOWER_TYPE_BASE:
188 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
189 break;
190 case TOWER_TYPE_GUN:
191 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
192 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y});
193 break;
194 default:
195 if (towerModels[tower.towerType].materials)
196 {
197 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
198 } else {
199 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
200 }
201 break;
202 }
203 }
204 }
205
206 void TowerUpdate()
207 {
208 for (int i = 0; i < towerCount; i++)
209 {
210 Tower *tower = &towers[i];
211 switch (tower->towerType)
212 {
213 case TOWER_TYPE_GUN:
214 TowerGunUpdate(tower);
215 break;
216 }
217 }
218 }
219
220 void TowerDrawHealthBars(Camera3D camera)
221 {
222 for (int i = 0; i < towerCount; i++)
223 {
224 Tower *tower = &towers[i];
225 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
226 {
227 continue;
228 }
229
230 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
231 float maxHealth = TowerGetMaxHealth(tower);
232 float health = maxHealth - tower->damage;
233 float healthRatio = health / maxHealth;
234
235 DrawHealthBar(camera, position, healthRatio, GREEN);
236 }
237 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
Place a gun to see a tower with the sprite on top:
So we have now a tower on which a ... person is standing. It's not an archer yet, but looking at the result...
... maybe we should try an orthogonal view for the game. This way, the sprites should mix better with the 3D models. Let's try this:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Variables
7 GUIState guiState = {0};
8 GameTime gameTime = {0};
9
10 Level levels[] = {
11 [0] = {
12 .state = LEVEL_STATE_BUILDING,
13 .initialGold = 20,
14 .waves[0] = {
15 .enemyType = ENEMY_TYPE_MINION,
16 .wave = 0,
17 .count = 10,
18 .interval = 2.5f,
19 .delay = 1.0f,
20 .spawnPosition = {0, 6},
21 },
22 .waves[1] = {
23 .enemyType = ENEMY_TYPE_MINION,
24 .wave = 1,
25 .count = 20,
26 .interval = 1.5f,
27 .delay = 1.0f,
28 .spawnPosition = {0, 6},
29 },
30 .waves[2] = {
31 .enemyType = ENEMY_TYPE_MINION,
32 .wave = 2,
33 .count = 30,
34 .interval = 1.2f,
35 .delay = 1.0f,
36 .spawnPosition = {0, 6},
37 }
38 },
39 };
40
41 Level *currentLevel = levels;
42
43 //# Game
44
45 void InitLevel(Level *level)
46 {
47 TowerInit();
48 EnemyInit();
49 ProjectileInit();
50 ParticleInit();
51 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
52
53 level->placementMode = 0;
54 level->state = LEVEL_STATE_BUILDING;
55 level->nextState = LEVEL_STATE_NONE;
56 level->playerGold = level->initialGold;
57
58 Camera *camera = &level->camera;
59 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
60 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
61 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
62 camera->fovy = 10.0f;
63 camera->projection = CAMERA_ORTHOGRAPHIC;
64 }
65
66 void DrawLevelHud(Level *level)
67 {
68 const char *text = TextFormat("Gold: %d", level->playerGold);
69 Font font = GetFontDefault();
70 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
71 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
72 }
73
74 void DrawLevelReportLostWave(Level *level)
75 {
76 BeginMode3D(level->camera);
77 DrawGrid(10, 1.0f);
78 TowerDraw();
79 EnemyDraw();
80 ProjectileDraw();
81 ParticleDraw();
82 guiState.isBlocked = 0;
83 EndMode3D();
84
85 TowerDrawHealthBars(level->camera);
86
87 const char *text = "Wave lost";
88 int textWidth = MeasureText(text, 20);
89 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
90
91 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
92 {
93 level->nextState = LEVEL_STATE_RESET;
94 }
95 }
96
97 int HasLevelNextWave(Level *level)
98 {
99 for (int i = 0; i < 10; i++)
100 {
101 EnemyWave *wave = &level->waves[i];
102 if (wave->wave == level->currentWave)
103 {
104 return 1;
105 }
106 }
107 return 0;
108 }
109
110 void DrawLevelReportWonWave(Level *level)
111 {
112 BeginMode3D(level->camera);
113 DrawGrid(10, 1.0f);
114 TowerDraw();
115 EnemyDraw();
116 ProjectileDraw();
117 ParticleDraw();
118 guiState.isBlocked = 0;
119 EndMode3D();
120
121 TowerDrawHealthBars(level->camera);
122
123 const char *text = "Wave won";
124 int textWidth = MeasureText(text, 20);
125 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
126
127
128 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
129 {
130 level->nextState = LEVEL_STATE_RESET;
131 }
132
133 if (HasLevelNextWave(level))
134 {
135 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
136 {
137 level->nextState = LEVEL_STATE_BUILDING;
138 }
139 }
140 else {
141 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
142 {
143 level->nextState = LEVEL_STATE_WON_LEVEL;
144 }
145 }
146 }
147
148 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
149 {
150 static ButtonState buttonStates[8] = {0};
151 int cost = GetTowerCosts(towerType);
152 const char *text = TextFormat("%s: %d", name, cost);
153 buttonStates[towerType].isSelected = level->placementMode == towerType;
154 buttonStates[towerType].isDisabled = level->playerGold < cost;
155 if (Button(text, x, y, width, height, &buttonStates[towerType]))
156 {
157 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
158 }
159 }
160
161 void DrawLevelBuildingState(Level *level)
162 {
163 BeginMode3D(level->camera);
164 DrawGrid(10, 1.0f);
165 TowerDraw();
166 EnemyDraw();
167 ProjectileDraw();
168 ParticleDraw();
169
170 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
171 float planeDistance = ray.position.y / -ray.direction.y;
172 float planeX = ray.direction.x * planeDistance + ray.position.x;
173 float planeY = ray.direction.z * planeDistance + ray.position.z;
174 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
175 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
176 if (level->placementMode && !guiState.isBlocked)
177 {
178 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
179 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
180 {
181 if (TowerTryAdd(level->placementMode, mapX, mapY))
182 {
183 level->playerGold -= GetTowerCosts(level->placementMode);
184 level->placementMode = TOWER_TYPE_NONE;
185 }
186 }
187 }
188
189 guiState.isBlocked = 0;
190
191 EndMode3D();
192
193 TowerDrawHealthBars(level->camera);
194
195 static ButtonState buildWallButtonState = {0};
196 static ButtonState buildGunButtonState = {0};
197 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
198 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
199
200 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
201 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
202
203 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
204 {
205 level->nextState = LEVEL_STATE_RESET;
206 }
207
208 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
209 {
210 level->nextState = LEVEL_STATE_BATTLE;
211 }
212
213 const char *text = "Building phase";
214 int textWidth = MeasureText(text, 20);
215 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
216 }
217
218 void InitBattleStateConditions(Level *level)
219 {
220 level->state = LEVEL_STATE_BATTLE;
221 level->nextState = LEVEL_STATE_NONE;
222 level->waveEndTimer = 0.0f;
223 for (int i = 0; i < 10; i++)
224 {
225 EnemyWave *wave = &level->waves[i];
226 wave->spawned = 0;
227 wave->timeToSpawnNext = wave->delay;
228 }
229 }
230
231 void DrawLevelBattleState(Level *level)
232 {
233 BeginMode3D(level->camera);
234 DrawGrid(10, 1.0f);
235 TowerDraw();
236 EnemyDraw();
237 ProjectileDraw();
238 ParticleDraw();
239 guiState.isBlocked = 0;
240 EndMode3D();
241
242 EnemyDrawHealthbars(level->camera);
243 TowerDrawHealthBars(level->camera);
244
245 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
246 {
247 level->nextState = LEVEL_STATE_RESET;
248 }
249
250 int maxCount = 0;
251 int remainingCount = 0;
252 for (int i = 0; i < 10; i++)
253 {
254 EnemyWave *wave = &level->waves[i];
255 if (wave->wave != level->currentWave)
256 {
257 continue;
258 }
259 maxCount += wave->count;
260 remainingCount += wave->count - wave->spawned;
261 }
262 int aliveCount = EnemyCount();
263 remainingCount += aliveCount;
264
265 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
266 int textWidth = MeasureText(text, 20);
267 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
268 }
269
270 void DrawLevel(Level *level)
271 {
272 switch (level->state)
273 {
274 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
275 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
276 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
277 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
278 default: break;
279 }
280
281 DrawLevelHud(level);
282 }
283
284 void UpdateLevel(Level *level)
285 {
286 if (level->state == LEVEL_STATE_BATTLE)
287 {
288 int activeWaves = 0;
289 for (int i = 0; i < 10; i++)
290 {
291 EnemyWave *wave = &level->waves[i];
292 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
293 {
294 continue;
295 }
296 activeWaves++;
297 wave->timeToSpawnNext -= gameTime.deltaTime;
298 if (wave->timeToSpawnNext <= 0.0f)
299 {
300 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
301 if (enemy)
302 {
303 wave->timeToSpawnNext = wave->interval;
304 wave->spawned++;
305 }
306 }
307 }
308 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
309 level->waveEndTimer += gameTime.deltaTime;
310 if (level->waveEndTimer >= 2.0f)
311 {
312 level->nextState = LEVEL_STATE_LOST_WAVE;
313 }
314 }
315 else if (activeWaves == 0 && EnemyCount() == 0)
316 {
317 level->waveEndTimer += gameTime.deltaTime;
318 if (level->waveEndTimer >= 2.0f)
319 {
320 level->nextState = LEVEL_STATE_WON_WAVE;
321 }
322 }
323 }
324
325 PathFindingMapUpdate();
326 EnemyUpdate();
327 TowerUpdate();
328 ProjectileUpdate();
329 ParticleUpdate();
330
331 if (level->nextState == LEVEL_STATE_RESET)
332 {
333 InitLevel(level);
334 }
335
336 if (level->nextState == LEVEL_STATE_BATTLE)
337 {
338 InitBattleStateConditions(level);
339 }
340
341 if (level->nextState == LEVEL_STATE_WON_WAVE)
342 {
343 level->currentWave++;
344 level->state = LEVEL_STATE_WON_WAVE;
345 }
346
347 if (level->nextState == LEVEL_STATE_LOST_WAVE)
348 {
349 level->state = LEVEL_STATE_LOST_WAVE;
350 }
351
352 if (level->nextState == LEVEL_STATE_BUILDING)
353 {
354 level->state = LEVEL_STATE_BUILDING;
355 }
356
357 if (level->nextState == LEVEL_STATE_WON_LEVEL)
358 {
359 // make something of this later
360 InitLevel(level);
361 }
362
363 level->nextState = LEVEL_STATE_NONE;
364 }
365
366 float nextSpawnTime = 0.0f;
367
368 void ResetGame()
369 {
370 InitLevel(currentLevel);
371 }
372
373 void InitGame()
374 {
375 TowerInit();
376 EnemyInit();
377 ProjectileInit();
378 ParticleInit();
379 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
380
381 currentLevel = levels;
382 InitLevel(currentLevel);
383 }
384
385 //# Immediate GUI functions
386
387 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor)
388 {
389 const float healthBarWidth = 40.0f;
390 const float healthBarHeight = 6.0f;
391 const float healthBarOffset = 15.0f;
392 const float inset = 2.0f;
393 const float innerWidth = healthBarWidth - inset * 2;
394 const float innerHeight = healthBarHeight - inset * 2;
395
396 Vector2 screenPos = GetWorldToScreen(position, camera);
397 float centerX = screenPos.x - healthBarWidth * 0.5f;
398 float topY = screenPos.y - healthBarOffset;
399 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
400 float healthWidth = innerWidth * healthRatio;
401 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
402 }
403
404 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
405 {
406 Rectangle bounds = {x, y, width, height};
407 int isPressed = 0;
408 int isSelected = state && state->isSelected;
409 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
410 {
411 Color color = isSelected ? DARKGRAY : GRAY;
412 DrawRectangle(x, y, width, height, color);
413 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
414 {
415 isPressed = 1;
416 }
417 guiState.isBlocked = 1;
418 }
419 else
420 {
421 Color color = isSelected ? WHITE : LIGHTGRAY;
422 DrawRectangle(x, y, width, height, color);
423 }
424 Font font = GetFontDefault();
425 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
426 Color textColor = state->isDisabled ? GRAY : BLACK;
427 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
428 return isPressed;
429 }
430
431 //# Main game loop
432
433 void GameUpdate()
434 {
435 float dt = GetFrameTime();
436 // cap maximum delta time to 0.1 seconds to prevent large time steps
437 if (dt > 0.1f) dt = 0.1f;
438 gameTime.time += dt;
439 gameTime.deltaTime = dt;
440
441 UpdateLevel(currentLevel);
442 }
443
444 int main(void)
445 {
446 int screenWidth, screenHeight;
447 GetPreferredSize(&screenWidth, &screenHeight);
448 InitWindow(screenWidth, screenHeight, "Tower defense");
449 SetTargetFPS(30);
450
451 InitGame();
452
453 while (!WindowShouldClose())
454 {
455 if (IsPaused()) {
456 // canvas is not visible in browser - do nothing
457 continue;
458 }
459
460 BeginDrawing();
461 ClearBackground(DARKBLUE);
462
463 GameUpdate();
464 DrawLevel(currentLevel);
465
466 EndDrawing();
467 }
468
469 CloseWindow();
470
471 return 0;
472 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 } Particle;
28
29 #define TOWER_MAX_COUNT 400
30 #define TOWER_TYPE_NONE 0
31 #define TOWER_TYPE_BASE 1
32 #define TOWER_TYPE_GUN 2
33 #define TOWER_TYPE_WALL 3
34 #define TOWER_TYPE_COUNT 4
35
36 typedef struct Tower
37 {
38 int16_t x, y;
39 uint8_t towerType;
40 float cooldown;
41 float damage;
42 } Tower;
43
44 typedef struct GameTime
45 {
46 float time;
47 float deltaTime;
48 } GameTime;
49
50 typedef struct ButtonState {
51 char isSelected;
52 char isDisabled;
53 } ButtonState;
54
55 typedef struct GUIState {
56 int isBlocked;
57 } GUIState;
58
59 typedef enum LevelState
60 {
61 LEVEL_STATE_NONE,
62 LEVEL_STATE_BUILDING,
63 LEVEL_STATE_BATTLE,
64 LEVEL_STATE_WON_WAVE,
65 LEVEL_STATE_LOST_WAVE,
66 LEVEL_STATE_WON_LEVEL,
67 LEVEL_STATE_RESET,
68 } LevelState;
69
70 typedef struct EnemyWave {
71 uint8_t enemyType;
72 uint8_t wave;
73 uint16_t count;
74 float interval;
75 float delay;
76 Vector2 spawnPosition;
77
78 uint16_t spawned;
79 float timeToSpawnNext;
80 } EnemyWave;
81
82 typedef struct Level
83 {
84 LevelState state;
85 LevelState nextState;
86 Camera3D camera;
87 int placementMode;
88
89 int initialGold;
90 int playerGold;
91
92 EnemyWave waves[10];
93 int currentWave;
94 float waveEndTimer;
95 } Level;
96
97 typedef struct DeltaSrc
98 {
99 char x, y;
100 } DeltaSrc;
101
102 typedef struct PathfindingMap
103 {
104 int width, height;
105 float scale;
106 float *distances;
107 long *towerIndex;
108 DeltaSrc *deltaSrc;
109 float maxDistance;
110 Matrix toMapSpace;
111 Matrix toWorldSpace;
112 } PathfindingMap;
113
114 // when we execute the pathfinding algorithm, we need to store the active nodes
115 // in a queue. Each node has a position, a distance from the start, and the
116 // position of the node that we came from.
117 typedef struct PathfindingNode
118 {
119 int16_t x, y, fromX, fromY;
120 float distance;
121 } PathfindingNode;
122
123 typedef struct EnemyId
124 {
125 uint16_t index;
126 uint16_t generation;
127 } EnemyId;
128
129 typedef struct EnemyClassConfig
130 {
131 float speed;
132 float health;
133 float radius;
134 float maxAcceleration;
135 float requiredContactTime;
136 float explosionDamage;
137 float explosionRange;
138 float explosionPushbackPower;
139 int goldValue;
140 } EnemyClassConfig;
141
142 typedef struct Enemy
143 {
144 int16_t currentX, currentY;
145 int16_t nextX, nextY;
146 Vector2 simPosition;
147 Vector2 simVelocity;
148 uint16_t generation;
149 float startMovingTime;
150 float damage, futureDamage;
151 float contactTime;
152 uint8_t enemyType;
153 uint8_t movePathCount;
154 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
155 } Enemy;
156
157 #define PROJECTILE_MAX_COUNT 1200
158 #define PROJECTILE_TYPE_NONE 0
159 #define PROJECTILE_TYPE_BULLET 1
160
161 typedef struct Projectile
162 {
163 uint8_t projectileType;
164 float shootTime;
165 float arrivalTime;
166 float damage;
167 Vector2 position;
168 Vector2 target;
169 Vector2 directionNormal;
170 EnemyId targetEnemy;
171 } Projectile;
172
173 //# Function declarations
174 float TowerGetMaxHealth(Tower *tower);
175 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
176 int EnemyAddDamage(Enemy *enemy, float damage);
177
178 //# Enemy functions
179 void EnemyInit();
180 void EnemyDraw();
181 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
182 void EnemyUpdate();
183 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
184 float EnemyGetMaxHealth(Enemy *enemy);
185 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
186 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
187 EnemyId EnemyGetId(Enemy *enemy);
188 Enemy *EnemyTryResolve(EnemyId enemyId);
189 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
190 int EnemyAddDamage(Enemy *enemy, float damage);
191 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
192 int EnemyCount();
193 void EnemyDrawHealthbars(Camera3D camera);
194
195 //# Tower functions
196 void TowerInit();
197 Tower *TowerGetAt(int16_t x, int16_t y);
198 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
199 Tower *GetTowerByType(uint8_t towerType);
200 int GetTowerCosts(uint8_t towerType);
201 float TowerGetMaxHealth(Tower *tower);
202 void TowerDraw();
203 void TowerUpdate();
204 void TowerDrawHealthBars(Camera3D camera);
205
206 //# Particles
207 void ParticleInit();
208 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
209 void ParticleUpdate();
210 void ParticleDraw();
211
212 //# Projectiles
213 void ProjectileInit();
214 void ProjectileDraw();
215 void ProjectileUpdate();
216 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage);
217
218 //# Pathfinding map
219 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
220 float PathFindingGetDistance(int mapX, int mapY);
221 Vector2 PathFindingGetGradient(Vector3 world);
222 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
223 void PathFindingMapUpdate();
224 void PathFindingMapDraw();
225
226 //# UI
227 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor);
228
229 //# variables
230 extern Level *currentLevel;
231 extern Enemy enemies[ENEMY_MAX_COUNT];
232 extern int enemyCount;
233 extern EnemyClassConfig enemyClassConfigs[];
234
235 extern GUIState guiState;
236 extern GameTime gameTime;
237 extern Tower towers[TOWER_MAX_COUNT];
238 extern int towerCount;
239
240 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 10.0f,
9 .speed = 0.6f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 void EnemyInit()
24 {
25 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
26 {
27 enemies[i] = (Enemy){0};
28 }
29 enemyCount = 0;
30 }
31
32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
33 {
34 return enemyClassConfigs[enemy->enemyType].speed;
35 }
36
37 float EnemyGetMaxHealth(Enemy *enemy)
38 {
39 return enemyClassConfigs[enemy->enemyType].health;
40 }
41
42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
43 {
44 int16_t castleX = 0;
45 int16_t castleY = 0;
46 int16_t dx = castleX - currentX;
47 int16_t dy = castleY - currentY;
48 if (dx == 0 && dy == 0)
49 {
50 *nextX = currentX;
51 *nextY = currentY;
52 return 1;
53 }
54 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
55
56 if (gradient.x == 0 && gradient.y == 0)
57 {
58 *nextX = currentX;
59 *nextY = currentY;
60 return 1;
61 }
62
63 if (fabsf(gradient.x) > fabsf(gradient.y))
64 {
65 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
66 *nextY = currentY;
67 return 0;
68 }
69 *nextX = currentX;
70 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
71 return 0;
72 }
73
74
75 // this function predicts the movement of the unit for the next deltaT seconds
76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
77 {
78 const float pointReachedDistance = 0.25f;
79 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
80 const float maxSimStepTime = 0.015625f;
81
82 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
83 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
84 int16_t nextX = enemy->nextX;
85 int16_t nextY = enemy->nextY;
86 Vector2 position = enemy->simPosition;
87 int passedCount = 0;
88 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
89 {
90 float stepTime = fminf(deltaT - t, maxSimStepTime);
91 Vector2 target = (Vector2){nextX, nextY};
92 float speed = Vector2Length(*velocity);
93 // draw the target position for debugging
94 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
95 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
96 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
97 {
98 // we reached the target position, let's move to the next waypoint
99 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100 target = (Vector2){nextX, nextY};
101 // track how many waypoints we passed
102 passedCount++;
103 }
104
105 // acceleration towards the target
106 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108 *velocity = Vector2Add(*velocity, acceleration);
109
110 // limit the speed to the maximum speed
111 if (speed > maxSpeed)
112 {
113 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114 }
115
116 // move the enemy
117 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118 }
119
120 if (waypointPassedCount)
121 {
122 (*waypointPassedCount) = passedCount;
123 }
124
125 return position;
126 }
127
128 void EnemyDraw()
129 {
130 for (int i = 0; i < enemyCount; i++)
131 {
132 Enemy enemy = enemies[i];
133 if (enemy.enemyType == ENEMY_TYPE_NONE)
134 {
135 continue;
136 }
137
138 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139
140 if (enemy.movePathCount > 0)
141 {
142 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144 }
145 for (int j = 1; j < enemy.movePathCount; j++)
146 {
147 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149 DrawLine3D(p, q, GREEN);
150 }
151
152 switch (enemy.enemyType)
153 {
154 case ENEMY_TYPE_MINION:
155 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156 break;
157 }
158 }
159 }
160
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163 // damage the tower
164 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167 float explosionRange2 = explosionRange * explosionRange;
168 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169 // explode the enemy
170 if (tower->damage >= TowerGetMaxHealth(tower))
171 {
172 tower->towerType = TOWER_TYPE_NONE;
173 }
174
175 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
176 explosionSource,
177 (Vector3){0, 0.1f, 0}, 1.0f);
178
179 enemy->enemyType = ENEMY_TYPE_NONE;
180
181 // push back enemies & dealing damage
182 for (int i = 0; i < enemyCount; i++)
183 {
184 Enemy *other = &enemies[i];
185 if (other->enemyType == ENEMY_TYPE_NONE)
186 {
187 continue;
188 }
189 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190 if (distanceSqr > 0 && distanceSqr < explosionRange2)
191 {
192 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194 EnemyAddDamage(other, explosionDamge);
195 }
196 }
197 }
198
199 void EnemyUpdate()
200 {
201 const float castleX = 0;
202 const float castleY = 0;
203 const float maxPathDistance2 = 0.25f * 0.25f;
204
205 for (int i = 0; i < enemyCount; i++)
206 {
207 Enemy *enemy = &enemies[i];
208 if (enemy->enemyType == ENEMY_TYPE_NONE)
209 {
210 continue;
211 }
212
213 int waypointPassedCount = 0;
214 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215 enemy->startMovingTime = gameTime.time;
216 // track path of unit
217 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218 {
219 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220 {
221 enemy->movePath[j] = enemy->movePath[j - 1];
222 }
223 enemy->movePath[0] = enemy->simPosition;
224 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225 {
226 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227 }
228 }
229
230 if (waypointPassedCount > 0)
231 {
232 enemy->currentX = enemy->nextX;
233 enemy->currentY = enemy->nextY;
234 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236 {
237 // enemy reached the castle; remove it
238 enemy->enemyType = ENEMY_TYPE_NONE;
239 continue;
240 }
241 }
242 }
243
244 // handle collisions between enemies
245 for (int i = 0; i < enemyCount - 1; i++)
246 {
247 Enemy *enemyA = &enemies[i];
248 if (enemyA->enemyType == ENEMY_TYPE_NONE)
249 {
250 continue;
251 }
252 for (int j = i + 1; j < enemyCount; j++)
253 {
254 Enemy *enemyB = &enemies[j];
255 if (enemyB->enemyType == ENEMY_TYPE_NONE)
256 {
257 continue;
258 }
259 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262 float radiusSum = radiusA + radiusB;
263 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264 {
265 // collision
266 float distance = sqrtf(distanceSqr);
267 float overlap = radiusSum - distance;
268 // move the enemies apart, but softly; if we have a clog of enemies,
269 // moving them perfectly apart can cause them to jitter
270 float positionCorrection = overlap / 5.0f;
271 Vector2 direction = (Vector2){
272 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276 }
277 }
278 }
279
280 // handle collisions between enemies and towers
281 for (int i = 0; i < enemyCount; i++)
282 {
283 Enemy *enemy = &enemies[i];
284 if (enemy->enemyType == ENEMY_TYPE_NONE)
285 {
286 continue;
287 }
288 enemy->contactTime -= gameTime.deltaTime;
289 if (enemy->contactTime < 0.0f)
290 {
291 enemy->contactTime = 0.0f;
292 }
293
294 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295 // linear search over towers; could be optimized by using path finding tower map,
296 // but for now, we keep it simple
297 for (int j = 0; j < towerCount; j++)
298 {
299 Tower *tower = &towers[j];
300 if (tower->towerType == TOWER_TYPE_NONE)
301 {
302 continue;
303 }
304 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306 if (distanceSqr > combinedRadius * combinedRadius)
307 {
308 continue;
309 }
310 // potential collision; square / circle intersection
311 float dx = tower->x - enemy->simPosition.x;
312 float dy = tower->y - enemy->simPosition.y;
313 float absDx = fabsf(dx);
314 float absDy = fabsf(dy);
315 Vector3 contactPoint = {0};
316 if (absDx <= 0.5f && absDx <= absDy) {
317 // vertical collision; push the enemy out horizontally
318 float overlap = enemyRadius + 0.5f - absDy;
319 if (overlap < 0.0f)
320 {
321 continue;
322 }
323 float direction = dy > 0.0f ? -1.0f : 1.0f;
324 enemy->simPosition.y += direction * overlap;
325 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->y + direction * 0.5f};
326 }
327 else if (absDy <= 0.5f && absDy <= absDx)
328 {
329 // horizontal collision; push the enemy out vertically
330 float overlap = enemyRadius + 0.5f - absDx;
331 if (overlap < 0.0f)
332 {
333 continue;
334 }
335 float direction = dx > 0.0f ? -1.0f : 1.0f;
336 enemy->simPosition.x += direction * overlap;
337 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
338 }
339 else
340 {
341 // possible collision with a corner
342 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
343 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
344 float cornerX = tower->x + cornerDX;
345 float cornerY = tower->y + cornerDY;
346 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
347 if (cornerDistanceSqr > enemyRadius * enemyRadius)
348 {
349 continue;
350 }
351 // push the enemy out along the diagonal
352 float cornerDistance = sqrtf(cornerDistanceSqr);
353 float overlap = enemyRadius - cornerDistance;
354 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
355 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
356 enemy->simPosition.x -= directionX * overlap;
357 enemy->simPosition.y -= directionY * overlap;
358 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
359 }
360
361 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
362 {
363 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
364 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
365 {
366 EnemyTriggerExplode(enemy, tower, contactPoint);
367 }
368 }
369 }
370 }
371 }
372
373 EnemyId EnemyGetId(Enemy *enemy)
374 {
375 return (EnemyId){enemy - enemies, enemy->generation};
376 }
377
378 Enemy *EnemyTryResolve(EnemyId enemyId)
379 {
380 if (enemyId.index >= ENEMY_MAX_COUNT)
381 {
382 return 0;
383 }
384 Enemy *enemy = &enemies[enemyId.index];
385 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
386 {
387 return 0;
388 }
389 return enemy;
390 }
391
392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
393 {
394 Enemy *spawn = 0;
395 for (int i = 0; i < enemyCount; i++)
396 {
397 Enemy *enemy = &enemies[i];
398 if (enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 spawn = enemy;
401 break;
402 }
403 }
404
405 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
406 {
407 spawn = &enemies[enemyCount++];
408 }
409
410 if (spawn)
411 {
412 spawn->currentX = currentX;
413 spawn->currentY = currentY;
414 spawn->nextX = currentX;
415 spawn->nextY = currentY;
416 spawn->simPosition = (Vector2){currentX, currentY};
417 spawn->simVelocity = (Vector2){0, 0};
418 spawn->enemyType = enemyType;
419 spawn->startMovingTime = gameTime.time;
420 spawn->damage = 0.0f;
421 spawn->futureDamage = 0.0f;
422 spawn->generation++;
423 spawn->movePathCount = 0;
424 }
425
426 return spawn;
427 }
428
429 int EnemyAddDamage(Enemy *enemy, float damage)
430 {
431 enemy->damage += damage;
432 if (enemy->damage >= EnemyGetMaxHealth(enemy))
433 {
434 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
435 enemy->enemyType = ENEMY_TYPE_NONE;
436 return 1;
437 }
438
439 return 0;
440 }
441
442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
443 {
444 int16_t castleX = 0;
445 int16_t castleY = 0;
446 Enemy* closest = 0;
447 int16_t closestDistance = 0;
448 float range2 = range * range;
449 for (int i = 0; i < enemyCount; i++)
450 {
451 Enemy* enemy = &enemies[i];
452 if (enemy->enemyType == ENEMY_TYPE_NONE)
453 {
454 continue;
455 }
456 float maxHealth = EnemyGetMaxHealth(enemy);
457 if (enemy->futureDamage >= maxHealth)
458 {
459 // ignore enemies that will die soon
460 continue;
461 }
462 int16_t dx = castleX - enemy->currentX;
463 int16_t dy = castleY - enemy->currentY;
464 int16_t distance = abs(dx) + abs(dy);
465 if (!closest || distance < closestDistance)
466 {
467 float tdx = towerX - enemy->currentX;
468 float tdy = towerY - enemy->currentY;
469 float tdistance2 = tdx * tdx + tdy * tdy;
470 if (tdistance2 <= range2)
471 {
472 closest = enemy;
473 closestDistance = distance;
474 }
475 }
476 }
477 return closest;
478 }
479
480 int EnemyCount()
481 {
482 int count = 0;
483 for (int i = 0; i < enemyCount; i++)
484 {
485 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
486 {
487 count++;
488 }
489 }
490 return count;
491 }
492
493 void EnemyDrawHealthbars(Camera3D camera)
494 {
495 for (int i = 0; i < enemyCount; i++)
496 {
497 Enemy *enemy = &enemies[i];
498 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
499 {
500 continue;
501 }
502 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
503 float maxHealth = EnemyGetMaxHealth(enemy);
504 float health = maxHealth - enemy->damage;
505 float healthRatio = health / maxHealth;
506
507 DrawHealthBar(camera, position, healthRatio, GREEN);
508 }
509 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate()
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < towerCount; i++)
131 {
132 Tower *tower = &towers[i];
133 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134 {
135 continue;
136 }
137 int16_t mapX, mapY;
138 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139 // this would not work correctly and needs to be refined to allow towers covering multiple cells
140 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141 // one cell. For now.
142 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143 {
144 continue;
145 }
146 int index = mapY * width + mapX;
147 pathfindingMap.towerIndex[index] = i;
148 }
149
150 // we start at the castle and add the castle to the queue
151 pathfindingMap.maxDistance = 0.0f;
152 pathfindingNodeQueueCount = 0;
153 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154 PathfindingNode *node = 0;
155 while ((node = PathFindingNodePop()))
156 {
157 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158 {
159 continue;
160 }
161 int index = node->y * width + node->x;
162 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163 {
164 continue;
165 }
166
167 int deltaX = node->x - node->fromX;
168 int deltaY = node->y - node->fromY;
169 // even if the cell is blocked by a tower, we still may want to store the direction
170 // (though this might not be needed, IDK right now)
171 pathfindingMap.deltaSrc[index].x = (char) deltaX;
172 pathfindingMap.deltaSrc[index].y = (char) deltaY;
173
174 // we skip nodes that are blocked by towers
175 if (pathfindingMap.towerIndex[index] >= 0)
176 {
177 node->distance += 8.0f;
178 }
179 pathfindingMap.distances[index] = node->distance;
180 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185 }
186 }
187
188 void PathFindingMapDraw()
189 {
190 float cellSize = pathfindingMap.scale * 0.9f;
191 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192 for (int x = 0; x < pathfindingMap.width; x++)
193 {
194 for (int y = 0; y < pathfindingMap.height; y++)
195 {
196 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200 // animate the distance "wave" to show how the pathfinding algorithm expands
201 // from the castle
202 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203 {
204 color = BLACK;
205 }
206 DrawCube(position, cellSize, 0.1f, cellSize, color);
207 }
208 }
209 }
210
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213 int16_t mapX, mapY;
214 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215 {
216 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217 return (Vector2){(float)-delta.x, (float)-delta.y};
218 }
219 // fallback to a simple gradient calculation
220 float n = PathFindingGetDistance(mapX, mapY - 1);
221 float s = PathFindingGetDistance(mapX, mapY + 1);
222 float w = PathFindingGetDistance(mapX - 1, mapY);
223 float e = PathFindingGetDistance(mapX + 1, mapY);
224 return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 void ProjectileInit()
8 {
9 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
10 {
11 projectiles[i] = (Projectile){0};
12 }
13 }
14
15 void ProjectileDraw()
16 {
17 for (int i = 0; i < projectileCount; i++)
18 {
19 Projectile projectile = projectiles[i];
20 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
21 {
22 continue;
23 }
24 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
25 if (transition >= 1.0f)
26 {
27 continue;
28 }
29 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
30 float x = position.x;
31 float y = position.y;
32 float dx = projectile.directionNormal.x;
33 float dy = projectile.directionNormal.y;
34 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
35 {
36 x -= dx * 0.1f;
37 y -= dy * 0.1f;
38 float size = 0.1f * d;
39 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
40 }
41 }
42 }
43
44 void ProjectileUpdate()
45 {
46 for (int i = 0; i < projectileCount; i++)
47 {
48 Projectile *projectile = &projectiles[i];
49 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
50 {
51 continue;
52 }
53 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
54 if (transition >= 1.0f)
55 {
56 projectile->projectileType = PROJECTILE_TYPE_NONE;
57 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
58 if (enemy)
59 {
60 EnemyAddDamage(enemy, projectile->damage);
61 }
62 continue;
63 }
64 }
65 }
66
67 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
68 {
69 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
70 {
71 Projectile *projectile = &projectiles[i];
72 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
73 {
74 projectile->projectileType = projectileType;
75 projectile->shootTime = gameTime.time;
76 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
77 projectile->damage = damage;
78 projectile->position = position;
79 projectile->target = target;
80 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
81 projectile->targetEnemy = EnemyGetId(enemy);
82 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
83 return projectile;
84 }
85 }
86 return 0;
87 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Particle particles[PARTICLE_MAX_COUNT];
5 static int particleCount = 0;
6
7 void ParticleInit()
8 {
9 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
10 {
11 particles[i] = (Particle){0};
12 }
13 particleCount = 0;
14 }
15
16 static void DrawExplosionParticle(Particle *particle, float transition)
17 {
18 float size = 1.2f * (1.0f - transition);
19 Color startColor = WHITE;
20 Color endColor = RED;
21 Color color = ColorLerp(startColor, endColor, transition);
22 DrawCube(particle->position, size, size, size, color);
23 }
24
25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
26 {
27 if (particleCount >= PARTICLE_MAX_COUNT)
28 {
29 return;
30 }
31
32 int index = -1;
33 for (int i = 0; i < particleCount; i++)
34 {
35 if (particles[i].particleType == PARTICLE_TYPE_NONE)
36 {
37 index = i;
38 break;
39 }
40 }
41
42 if (index == -1)
43 {
44 index = particleCount++;
45 }
46
47 Particle *particle = &particles[index];
48 particle->particleType = particleType;
49 particle->spawnTime = gameTime.time;
50 particle->lifetime = lifetime;
51 particle->position = position;
52 particle->velocity = velocity;
53 }
54
55 void ParticleUpdate()
56 {
57 for (int i = 0; i < particleCount; i++)
58 {
59 Particle *particle = &particles[i];
60 if (particle->particleType == PARTICLE_TYPE_NONE)
61 {
62 continue;
63 }
64
65 float age = gameTime.time - particle->spawnTime;
66
67 if (particle->lifetime > age)
68 {
69 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
70 }
71 else {
72 particle->particleType = PARTICLE_TYPE_NONE;
73 }
74 }
75 }
76
77 void ParticleDraw()
78 {
79 for (int i = 0; i < particleCount; i++)
80 {
81 Particle particle = particles[i];
82 if (particle.particleType == PARTICLE_TYPE_NONE)
83 {
84 continue;
85 }
86
87 float age = gameTime.time - particle.spawnTime;
88 float transition = age / particle.lifetime;
89 switch (particle.particleType)
90 {
91 case PARTICLE_TYPE_EXPLOSION:
92 DrawExplosionParticle(&particle, transition);
93 break;
94 default:
95 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
96 break;
97 }
98 }
99 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 Tower towers[TOWER_MAX_COUNT];
5 int towerCount = 0;
6
7 Model towerModels[TOWER_TYPE_COUNT];
8 Texture2D palette, spriteSheet;
9
10 // a unit that uses sprites to be drawn
11 typedef struct SpriteUnit
12 {
13 Rectangle srcRect;
14 Vector2 offset;
15 } SpriteUnit;
16
17 // definition of our archer unit
18 SpriteUnit archerUnit = {
19 .srcRect = {0, 0, 16, 16},
20 .offset = {7, 1},
21 };
22
23 void DrawSpriteUnit(SpriteUnit unit, Vector3 position)
24 {
25 Camera3D camera = currentLevel->camera;
26 float size = 0.5f;
27 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size };
28 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
29 // we want the sprite to face the camera, so we need to calculate the up vector
30 Vector3 forward = Vector3Subtract(camera.target, camera.position);
31 Vector3 up = {0, 1, 0};
32 Vector3 right = Vector3CrossProduct(forward, up);
33 up = Vector3Normalize(Vector3CrossProduct(right, forward));
34 DrawBillboardPro(camera, spriteSheet, unit.srcRect, position, up, scale, offset, 0, WHITE);
35 }
36
37 void TowerInit()
38 {
39 for (int i = 0; i < TOWER_MAX_COUNT; i++)
40 {
41 towers[i] = (Tower){0};
42 }
43 towerCount = 0;
44
45 // load a sprite sheet that contains all units
46 spriteSheet = LoadTexture("data/spritesheet.png");
47
48 // we'll use a palette texture to colorize the all buildings and environment art
49 palette = LoadTexture("data/palette.png");
50 // The texture uses gradients on very small space, so we'll enable bilinear filtering
51 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
52
53 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
54
55 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
56 {
57 if (towerModels[i].materials)
58 {
59 // assign the palette texture to the material of the model (0 is not used afaik)
60 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
61 }
62 }
63 }
64
65 static void TowerGunUpdate(Tower *tower)
66 {
67 if (tower->cooldown <= 0)
68 {
69 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
70 if (enemy)
71 {
72 tower->cooldown = 0.5f;
73 // shoot the enemy; determine future position of the enemy
74 float bulletSpeed = 1.0f;
75 float bulletDamage = 3.0f;
76 Vector2 velocity = enemy->simVelocity;
77 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
78 Vector2 towerPosition = {tower->x, tower->y};
79 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
80 for (int i = 0; i < 8; i++) {
81 velocity = enemy->simVelocity;
82 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
83 float distance = Vector2Distance(towerPosition, futurePosition);
84 float eta2 = distance / bulletSpeed;
85 if (fabs(eta - eta2) < 0.01f) {
86 break;
87 }
88 eta = (eta2 + eta) * 0.5f;
89 }
90 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
91 bulletSpeed, bulletDamage);
92 enemy->futureDamage += bulletDamage;
93 }
94 }
95 else
96 {
97 tower->cooldown -= gameTime.deltaTime;
98 }
99 }
100
101 Tower *TowerGetAt(int16_t x, int16_t y)
102 {
103 for (int i = 0; i < towerCount; i++)
104 {
105 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
106 {
107 return &towers[i];
108 }
109 }
110 return 0;
111 }
112
113 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
114 {
115 if (towerCount >= TOWER_MAX_COUNT)
116 {
117 return 0;
118 }
119
120 Tower *tower = TowerGetAt(x, y);
121 if (tower)
122 {
123 return 0;
124 }
125
126 tower = &towers[towerCount++];
127 tower->x = x;
128 tower->y = y;
129 tower->towerType = towerType;
130 tower->cooldown = 0.0f;
131 tower->damage = 0.0f;
132 return tower;
133 }
134
135 Tower *GetTowerByType(uint8_t towerType)
136 {
137 for (int i = 0; i < towerCount; i++)
138 {
139 if (towers[i].towerType == towerType)
140 {
141 return &towers[i];
142 }
143 }
144 return 0;
145 }
146
147 int GetTowerCosts(uint8_t towerType)
148 {
149 switch (towerType)
150 {
151 case TOWER_TYPE_BASE:
152 return 0;
153 case TOWER_TYPE_GUN:
154 return 6;
155 case TOWER_TYPE_WALL:
156 return 2;
157 }
158 return 0;
159 }
160
161 float TowerGetMaxHealth(Tower *tower)
162 {
163 switch (tower->towerType)
164 {
165 case TOWER_TYPE_BASE:
166 return 10.0f;
167 case TOWER_TYPE_GUN:
168 return 3.0f;
169 case TOWER_TYPE_WALL:
170 return 5.0f;
171 }
172 return 0.0f;
173 }
174
175 void TowerDraw()
176 {
177 for (int i = 0; i < towerCount; i++)
178 {
179 Tower tower = towers[i];
180 if (tower.towerType == TOWER_TYPE_NONE)
181 {
182 continue;
183 }
184
185 switch (tower.towerType)
186 {
187 case TOWER_TYPE_BASE:
188 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
189 break;
190 case TOWER_TYPE_GUN:
191 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
192 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y});
193 break;
194 default:
195 if (towerModels[tower.towerType].materials)
196 {
197 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
198 } else {
199 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
200 }
201 break;
202 }
203 }
204 }
205
206 void TowerUpdate()
207 {
208 for (int i = 0; i < towerCount; i++)
209 {
210 Tower *tower = &towers[i];
211 switch (tower->towerType)
212 {
213 case TOWER_TYPE_GUN:
214 TowerGunUpdate(tower);
215 break;
216 }
217 }
218 }
219
220 void TowerDrawHealthBars(Camera3D camera)
221 {
222 for (int i = 0; i < towerCount; i++)
223 {
224 Tower *tower = &towers[i];
225 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
226 {
227 continue;
228 }
229
230 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
231 float maxHealth = TowerGetMaxHealth(tower);
232 float health = maxHealth - tower->damage;
233 float healthRatio = health / maxHealth;
234
235 DrawHealthBar(camera, position, healthRatio, GREEN);
236 }
237 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
The result looks like this:
This starts looking decent! The orthogonal view makes the sprites fit better with the 3D models. There is still much to do, but this tutorial step has grown quite long already, so I will pause here.
Wrap up
We have now added healthbars that are aligned with the enemy positions and added some basic graphics for the wall and tower buildings. The camera is now in orthogonal view, which makes the sprites fit better with the 3D models. The graphics are simple and low poly, and we have a few more to add in the next steps!
So in the next part, we will continue with tweaking the graphics and adding more enemy types and towers. There's lots of details to add and improve, so next week, we will continue with this. Until then, have fun!