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