Simple tower defense tutorial, part 12: Building placement UX
Since touch screen interactions are working now (it wasn't working before I fixed some issues with the raylib web bindings that were specific to how I use the canvas integration), it would be nice if building placement on touch screens was working in a proper way.
Since I do get occasional feedback (thanks!) that the game does not properly work on touch screens, I want to address this issue now.
Draft how the building placement UX should work
Currently, you can choose a building to place and on the next tap on the map, the preview is shown at that location and the next tap places the building on whichever point you tapped on the map. This qualifies as "working", but it isn't something that is very nice to use.
UX (user experience) as a topic is complex and subjective. The complexity comes from the different input systems (mouse, keyboard, touch, gamepad) and what works in one environment might feel very awkward in another. Balancing this out is what makes this a subjective topic.
In this case, I will choose a pattern that I am familiar with from other games:
- When building a tower, the tower appears in a default location and the screen UI shows an OK button to confirm the placement on the bottom right of the screen. A cancel button is also shown on the bottom left of the screen.
- The player can drag the tower around the map by touching and dragging the tower.
- The player can also just tap on the map to place the tower at that location.
It would also be nice if the movement of the preview tower was smooth and playful. Also, the tower could be moved using the arrow keys on the keyboard this way.
Let's see how this will feel in practice!
Implementing the new UX
The first things to do is to establish the build mode with the cancel and OK buttons.
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4 #include <stdlib.h>
5 #include <math.h>
6
7 //# Variables
8 GUIState guiState = {0};
9 GameTime gameTime = {0};
10
11 Model floorTileAModel = {0};
12 Model floorTileBModel = {0};
13 Model treeModel[2] = {0};
14 Model firTreeModel[2] = {0};
15 Model rockModels[5] = {0};
16 Model grassPatchModel[1] = {0};
17
18 Model pathArrowModel = {0};
19
20 Texture2D palette, spriteSheet;
21
22 Level levels[] = {
23 [0] = {
24 .state = LEVEL_STATE_BUILDING,
25 .initialGold = 20,
26 .waves[0] = {
27 .enemyType = ENEMY_TYPE_MINION,
28 .wave = 0,
29 .count = 5,
30 .interval = 2.5f,
31 .delay = 1.0f,
32 .spawnPosition = {2, 6},
33 },
34 .waves[1] = {
35 .enemyType = ENEMY_TYPE_MINION,
36 .wave = 0,
37 .count = 5,
38 .interval = 2.5f,
39 .delay = 1.0f,
40 .spawnPosition = {-2, 6},
41 },
42 .waves[2] = {
43 .enemyType = ENEMY_TYPE_MINION,
44 .wave = 1,
45 .count = 20,
46 .interval = 1.5f,
47 .delay = 1.0f,
48 .spawnPosition = {0, 6},
49 },
50 .waves[3] = {
51 .enemyType = ENEMY_TYPE_MINION,
52 .wave = 2,
53 .count = 30,
54 .interval = 1.2f,
55 .delay = 1.0f,
56 .spawnPosition = {0, 6},
57 }
58 },
59 };
60
61 Level *currentLevel = levels;
62
63 //# Game
64
65 static Model LoadGLBModel(char *filename)
66 {
67 Model model = LoadModel(TextFormat("data/%s.glb",filename));
68 for (int i = 0; i < model.materialCount; i++)
69 {
70 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
71 }
72 return model;
73 }
74
75 void LoadAssets()
76 {
77 // load a sprite sheet that contains all units
78 spriteSheet = LoadTexture("data/spritesheet.png");
79 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
80
81 // we'll use a palette texture to colorize the all buildings and environment art
82 palette = LoadTexture("data/palette.png");
83 // The texture uses gradients on very small space, so we'll enable bilinear filtering
84 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
85
86 floorTileAModel = LoadGLBModel("floor-tile-a");
87 floorTileBModel = LoadGLBModel("floor-tile-b");
88 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
89 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
90 firTreeModel[0] = LoadGLBModel("firtree-1-a");
91 firTreeModel[1] = LoadGLBModel("firtree-1-b");
92 rockModels[0] = LoadGLBModel("rock-1");
93 rockModels[1] = LoadGLBModel("rock-2");
94 rockModels[2] = LoadGLBModel("rock-3");
95 rockModels[3] = LoadGLBModel("rock-4");
96 rockModels[4] = LoadGLBModel("rock-5");
97 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
98
99 pathArrowModel = LoadGLBModel("direction-arrow-x");
100 }
101
102 void InitLevel(Level *level)
103 {
104 level->seed = (int)(GetTime() * 100.0f);
105
106 TowerInit();
107 EnemyInit();
108 ProjectileInit();
109 ParticleInit();
110 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
111
112 level->placementMode = 0;
113 level->state = LEVEL_STATE_BUILDING;
114 level->nextState = LEVEL_STATE_NONE;
115 level->playerGold = level->initialGold;
116 level->currentWave = 0;
117
118 Camera *camera = &level->camera;
119 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
120 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
121 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
122 camera->fovy = 10.0f;
123 camera->projection = CAMERA_ORTHOGRAPHIC;
124 }
125
126 void DrawLevelHud(Level *level)
127 {
128 const char *text = TextFormat("Gold: %d", level->playerGold);
129 Font font = GetFontDefault();
130 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
131 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
132 }
133
134 void DrawLevelReportLostWave(Level *level)
135 {
136 BeginMode3D(level->camera);
137 DrawLevelGround(level);
138 TowerDraw();
139 EnemyDraw();
140 ProjectileDraw();
141 ParticleDraw();
142 guiState.isBlocked = 0;
143 EndMode3D();
144
145 TowerDrawHealthBars(level->camera);
146
147 const char *text = "Wave lost";
148 int textWidth = MeasureText(text, 20);
149 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
150
151 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
152 {
153 level->nextState = LEVEL_STATE_RESET;
154 }
155 }
156
157 int HasLevelNextWave(Level *level)
158 {
159 for (int i = 0; i < 10; i++)
160 {
161 EnemyWave *wave = &level->waves[i];
162 if (wave->wave == level->currentWave)
163 {
164 return 1;
165 }
166 }
167 return 0;
168 }
169
170 void DrawLevelReportWonWave(Level *level)
171 {
172 BeginMode3D(level->camera);
173 DrawLevelGround(level);
174 TowerDraw();
175 EnemyDraw();
176 ProjectileDraw();
177 ParticleDraw();
178 guiState.isBlocked = 0;
179 EndMode3D();
180
181 TowerDrawHealthBars(level->camera);
182
183 const char *text = "Wave won";
184 int textWidth = MeasureText(text, 20);
185 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
186
187
188 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
189 {
190 level->nextState = LEVEL_STATE_RESET;
191 }
192
193 if (HasLevelNextWave(level))
194 {
195 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
196 {
197 level->nextState = LEVEL_STATE_BUILDING;
198 }
199 }
200 else {
201 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
202 {
203 level->nextState = LEVEL_STATE_WON_LEVEL;
204 }
205 }
206 }
207
208 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
209 {
210 static ButtonState buttonStates[8] = {0};
211 int cost = GetTowerCosts(towerType);
212 const char *text = TextFormat("%s: %d", name, cost);
213 buttonStates[towerType].isSelected = level->placementMode == towerType;
214 buttonStates[towerType].isDisabled = level->playerGold < cost;
215 if (Button(text, x, y, width, height, &buttonStates[towerType]))
216 {
217 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
218 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
219 }
220 }
221
222 float GetRandomFloat(float min, float max)
223 {
224 int random = GetRandomValue(0, 0xfffffff);
225 return ((float)random / (float)0xfffffff) * (max - min) + min;
226 }
227
228 void DrawLevelGround(Level *level)
229 {
230 // draw checkerboard ground pattern
231 for (int x = -5; x <= 5; x += 1)
232 {
233 for (int y = -5; y <= 5; y += 1)
234 {
235 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
236 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
237 }
238 }
239
240 int oldSeed = GetRandomValue(0, 0xfffffff);
241 SetRandomSeed(level->seed);
242 // increase probability for trees via duplicated entries
243 Model borderModels[64];
244 int maxRockCount = GetRandomValue(2, 6);
245 int maxTreeCount = GetRandomValue(10, 20);
246 int maxFirTreeCount = GetRandomValue(5, 10);
247 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
248 int grassPatchCount = GetRandomValue(5, 30);
249
250 int modelCount = 0;
251 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
252 {
253 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
254 }
255 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
256 {
257 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
258 }
259 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
260 {
261 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
262 }
263 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
264 {
265 borderModels[modelCount++] = grassPatchModel[0];
266 }
267
268 // draw some objects around the border of the map
269 Vector3 up = {0, 1, 0};
270 // a pseudo random number generator to get the same result every time
271 const float wiggle = 0.75f;
272 const int layerCount = 3;
273 for (int layer = 0; layer < layerCount; layer++)
274 {
275 int layerPos = 6 + layer;
276 for (int x = -6 + layer; x <= 6 + layer; x += 1)
277 {
278 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
279 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
280 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
281 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
282 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
283 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
284 }
285
286 for (int z = -5 + layer; z <= 5 + layer; z += 1)
287 {
288 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
289 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
290 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
291 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
292 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
293 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
294 }
295 }
296
297 SetRandomSeed(oldSeed);
298 }
299
300 void DrawEnemyPath(Level *level, Color arrowColor)
301 {
302 const int castleX = 0, castleY = 0;
303 const int maxWaypointCount = 200;
304 const float timeStep = 1.0f;
305 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
306
307 // we start with a time offset to simulate the path,
308 // this way the arrows are animated in a forward moving direction
309 // The time is wrapped around the time step to get a smooth animation
310 float timeOffset = fmodf(GetTime(), timeStep);
311
312 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
313 {
314 EnemyWave *wave = &level->waves[i];
315 if (wave->wave != level->currentWave)
316 {
317 continue;
318 }
319
320 // use this dummy enemy to simulate the path
321 Enemy dummy = {
322 .enemyType = ENEMY_TYPE_MINION,
323 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
324 .nextX = wave->spawnPosition.x,
325 .nextY = wave->spawnPosition.y,
326 .currentX = wave->spawnPosition.x,
327 .currentY = wave->spawnPosition.y,
328 };
329
330 float deltaTime = timeOffset;
331 for (int j = 0; j < maxWaypointCount; j++)
332 {
333 int waypointPassedCount = 0;
334 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
335 // after the initial variable starting offset, we use a fixed time step
336 deltaTime = timeStep;
337 dummy.simPosition = pos;
338
339 // Update the dummy's position just like we do in the regular enemy update loop
340 for (int k = 0; k < waypointPassedCount; k++)
341 {
342 dummy.currentX = dummy.nextX;
343 dummy.currentY = dummy.nextY;
344 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
345 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
346 {
347 break;
348 }
349 }
350 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
351 {
352 break;
353 }
354
355 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
356 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
357 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
358 }
359 }
360 }
361
362 void DrawEnemyPaths(Level *level)
363 {
364 // disable depth testing for the path arrows
365 // flush the 3D batch to draw the arrows on top of everything
366 rlDrawRenderBatchActive();
367 rlDisableDepthTest();
368 DrawEnemyPath(level, (Color){64, 64, 64, 160});
369
370 rlDrawRenderBatchActive();
371 rlEnableDepthTest();
372 DrawEnemyPath(level, WHITE);
373 }
374
375 void DrawLevelBuildingPlacementState(Level *level)
376 {
377 BeginMode3D(level->camera);
378 DrawLevelGround(level);
379
380 int blockedCellCount = 0;
381 Vector2 blockedCells[1];
382 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
383 float planeDistance = ray.position.y / -ray.direction.y;
384 float planeX = ray.direction.x * planeDistance + ray.position.x;
385 float planeY = ray.direction.z * planeDistance + ray.position.z;
386 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
387 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
388 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
389 {
390 level->placementX = mapX;
391 level->placementY = mapY;
392 }
393 else
394 {
395 mapX = level->placementX;
396 mapY = level->placementY;
397 }
398 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
399 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
400 PathFindingMapUpdate(blockedCellCount, blockedCells);
401
402 TowerDraw();
403 EnemyDraw();
404 ProjectileDraw();
405 ParticleDraw();
406 DrawEnemyPaths(level);
407
408 guiState.isBlocked = 0;
409
410 EndMode3D();
411
412 TowerDrawHealthBars(level->camera);
413
414 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
415 {
416 level->nextState = LEVEL_STATE_BUILDING;
417 level->placementMode = TOWER_TYPE_NONE;
418 TraceLog(LOG_INFO, "Cancel building");
419 }
420
421 if (Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
422 {
423 level->nextState = LEVEL_STATE_BUILDING;
424 if (TowerTryAdd(level->placementMode, mapX, mapY))
425 {
426 level->playerGold -= GetTowerCosts(level->placementMode);
427 level->placementMode = TOWER_TYPE_NONE;
428 }
429 }
430 }
431
432 void DrawLevelBuildingState(Level *level)
433 {
434 BeginMode3D(level->camera);
435 DrawLevelGround(level);
436
437 PathFindingMapUpdate(0, 0);
438 TowerDraw();
439 EnemyDraw();
440 ProjectileDraw();
441 ParticleDraw();
442 DrawEnemyPaths(level);
443
444 guiState.isBlocked = 0;
445
446 EndMode3D();
447
448 TowerDrawHealthBars(level->camera);
449
450 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
451 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
452 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
453 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
454
455 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
456 {
457 level->nextState = LEVEL_STATE_RESET;
458 }
459
460 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
461 {
462 level->nextState = LEVEL_STATE_BATTLE;
463 }
464
465 const char *text = "Building phase";
466 int textWidth = MeasureText(text, 20);
467 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
468 }
469
470 void InitBattleStateConditions(Level *level)
471 {
472 level->state = LEVEL_STATE_BATTLE;
473 level->nextState = LEVEL_STATE_NONE;
474 level->waveEndTimer = 0.0f;
475 for (int i = 0; i < 10; i++)
476 {
477 EnemyWave *wave = &level->waves[i];
478 wave->spawned = 0;
479 wave->timeToSpawnNext = wave->delay;
480 }
481 }
482
483 void DrawLevelBattleState(Level *level)
484 {
485 BeginMode3D(level->camera);
486 DrawLevelGround(level);
487 TowerDraw();
488 EnemyDraw();
489 ProjectileDraw();
490 ParticleDraw();
491 guiState.isBlocked = 0;
492 EndMode3D();
493
494 EnemyDrawHealthbars(level->camera);
495 TowerDrawHealthBars(level->camera);
496
497 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
498 {
499 level->nextState = LEVEL_STATE_RESET;
500 }
501
502 int maxCount = 0;
503 int remainingCount = 0;
504 for (int i = 0; i < 10; i++)
505 {
506 EnemyWave *wave = &level->waves[i];
507 if (wave->wave != level->currentWave)
508 {
509 continue;
510 }
511 maxCount += wave->count;
512 remainingCount += wave->count - wave->spawned;
513 }
514 int aliveCount = EnemyCount();
515 remainingCount += aliveCount;
516
517 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
518 int textWidth = MeasureText(text, 20);
519 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
520 }
521
522 void DrawLevel(Level *level)
523 {
524 switch (level->state)
525 {
526 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
527 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
528 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
529 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
530 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
531 default: break;
532 }
533
534 DrawLevelHud(level);
535 }
536
537 void UpdateLevel(Level *level)
538 {
539 if (level->state == LEVEL_STATE_BATTLE)
540 {
541 int activeWaves = 0;
542 for (int i = 0; i < 10; i++)
543 {
544 EnemyWave *wave = &level->waves[i];
545 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
546 {
547 continue;
548 }
549 activeWaves++;
550 wave->timeToSpawnNext -= gameTime.deltaTime;
551 if (wave->timeToSpawnNext <= 0.0f)
552 {
553 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
554 if (enemy)
555 {
556 wave->timeToSpawnNext = wave->interval;
557 wave->spawned++;
558 }
559 }
560 }
561 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
562 level->waveEndTimer += gameTime.deltaTime;
563 if (level->waveEndTimer >= 2.0f)
564 {
565 level->nextState = LEVEL_STATE_LOST_WAVE;
566 }
567 }
568 else if (activeWaves == 0 && EnemyCount() == 0)
569 {
570 level->waveEndTimer += gameTime.deltaTime;
571 if (level->waveEndTimer >= 2.0f)
572 {
573 level->nextState = LEVEL_STATE_WON_WAVE;
574 }
575 }
576 }
577
578 PathFindingMapUpdate(0, 0);
579 EnemyUpdate();
580 TowerUpdate();
581 ProjectileUpdate();
582 ParticleUpdate();
583
584 if (level->nextState == LEVEL_STATE_RESET)
585 {
586 InitLevel(level);
587 }
588
589 if (level->nextState == LEVEL_STATE_BATTLE)
590 {
591 InitBattleStateConditions(level);
592 }
593
594 if (level->nextState == LEVEL_STATE_WON_WAVE)
595 {
596 level->currentWave++;
597 level->state = LEVEL_STATE_WON_WAVE;
598 }
599
600 if (level->nextState == LEVEL_STATE_LOST_WAVE)
601 {
602 level->state = LEVEL_STATE_LOST_WAVE;
603 }
604
605 if (level->nextState == LEVEL_STATE_BUILDING)
606 {
607 level->state = LEVEL_STATE_BUILDING;
608 }
609
610 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
611 {
612 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
613 }
614
615 if (level->nextState == LEVEL_STATE_WON_LEVEL)
616 {
617 // make something of this later
618 InitLevel(level);
619 }
620
621 level->nextState = LEVEL_STATE_NONE;
622 }
623
624 float nextSpawnTime = 0.0f;
625
626 void ResetGame()
627 {
628 InitLevel(currentLevel);
629 }
630
631 void InitGame()
632 {
633 TowerInit();
634 EnemyInit();
635 ProjectileInit();
636 ParticleInit();
637 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
638
639 currentLevel = levels;
640 InitLevel(currentLevel);
641 }
642
643 //# Immediate GUI functions
644
645 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
646 {
647 const float healthBarHeight = 6.0f;
648 const float healthBarOffset = 15.0f;
649 const float inset = 2.0f;
650 const float innerWidth = healthBarWidth - inset * 2;
651 const float innerHeight = healthBarHeight - inset * 2;
652
653 Vector2 screenPos = GetWorldToScreen(position, camera);
654 float centerX = screenPos.x - healthBarWidth * 0.5f;
655 float topY = screenPos.y - healthBarOffset;
656 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
657 float healthWidth = innerWidth * healthRatio;
658 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
659 }
660
661 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
662 {
663 Rectangle bounds = {x, y, width, height};
664 int isPressed = 0;
665 int isSelected = state && state->isSelected;
666 int isDisabled = state && state->isDisabled;
667 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
668 {
669 Color color = isSelected ? DARKGRAY : GRAY;
670 DrawRectangle(x, y, width, height, color);
671 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
672 {
673 isPressed = 1;
674 }
675 guiState.isBlocked = 1;
676 }
677 else
678 {
679 Color color = isSelected ? WHITE : LIGHTGRAY;
680 DrawRectangle(x, y, width, height, color);
681 }
682 Font font = GetFontDefault();
683 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
684 Color textColor = isDisabled ? GRAY : BLACK;
685 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
686 return isPressed;
687 }
688
689 //# Main game loop
690
691 void GameUpdate()
692 {
693 float dt = GetFrameTime();
694 // cap maximum delta time to 0.1 seconds to prevent large time steps
695 if (dt > 0.1f) dt = 0.1f;
696 gameTime.time += dt;
697 gameTime.deltaTime = dt;
698
699 UpdateLevel(currentLevel);
700 }
701
702 int main(void)
703 {
704 int screenWidth, screenHeight;
705 GetPreferredSize(&screenWidth, &screenHeight);
706 InitWindow(screenWidth, screenHeight, "Tower defense");
707 SetTargetFPS(30);
708
709 LoadAssets();
710 InitGame();
711
712 while (!WindowShouldClose())
713 {
714 if (IsPaused()) {
715 // canvas is not visible in browser - do nothing
716 continue;
717 }
718
719 BeginDrawing();
720 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
721
722 GameUpdate();
723 DrawLevel(currentLevel);
724
725 EndDrawing();
726 }
727
728 CloseWindow();
729
730 return 0;
731 }
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 Vector3 scale;
28 } Particle;
29
30 #define TOWER_MAX_COUNT 400
31 enum TowerType
32 {
33 TOWER_TYPE_NONE,
34 TOWER_TYPE_BASE,
35 TOWER_TYPE_ARCHER,
36 TOWER_TYPE_BALLISTA,
37 TOWER_TYPE_CATAPULT,
38 TOWER_TYPE_WALL,
39 TOWER_TYPE_COUNT
40 };
41
42 typedef struct HitEffectConfig
43 {
44 float damage;
45 float areaDamageRadius;
46 float pushbackPowerDistance;
47 } HitEffectConfig;
48
49 typedef struct TowerTypeConfig
50 {
51 float cooldown;
52 float range;
53 float projectileSpeed;
54
55 uint8_t cost;
56 uint8_t projectileType;
57 uint16_t maxHealth;
58
59 HitEffectConfig hitEffect;
60 } TowerTypeConfig;
61
62 typedef struct Tower
63 {
64 int16_t x, y;
65 uint8_t towerType;
66 Vector2 lastTargetPosition;
67 float cooldown;
68 float damage;
69 } Tower;
70
71 typedef struct GameTime
72 {
73 float time;
74 float deltaTime;
75 } GameTime;
76
77 typedef struct ButtonState {
78 char isSelected;
79 char isDisabled;
80 } ButtonState;
81
82 typedef struct GUIState {
83 int isBlocked;
84 } GUIState;
85
86 typedef enum LevelState
87 {
88 LEVEL_STATE_NONE,
89 LEVEL_STATE_BUILDING,
90 LEVEL_STATE_BUILDING_PLACEMENT,
91 LEVEL_STATE_BATTLE,
92 LEVEL_STATE_WON_WAVE,
93 LEVEL_STATE_LOST_WAVE,
94 LEVEL_STATE_WON_LEVEL,
95 LEVEL_STATE_RESET,
96 } LevelState;
97
98 typedef struct EnemyWave {
99 uint8_t enemyType;
100 uint8_t wave;
101 uint16_t count;
102 float interval;
103 float delay;
104 Vector2 spawnPosition;
105
106 uint16_t spawned;
107 float timeToSpawnNext;
108 } EnemyWave;
109
110 #define ENEMY_MAX_WAVE_COUNT 10
111
112 typedef struct Level
113 {
114 int seed;
115 LevelState state;
116 LevelState nextState;
117 Camera3D camera;
118 int placementMode;
119 int16_t placementX;
120 int16_t placementY;
121
122 int initialGold;
123 int playerGold;
124
125 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
126 int currentWave;
127 float waveEndTimer;
128 } Level;
129
130 typedef struct DeltaSrc
131 {
132 char x, y;
133 } DeltaSrc;
134
135 typedef struct PathfindingMap
136 {
137 int width, height;
138 float scale;
139 float *distances;
140 long *towerIndex;
141 DeltaSrc *deltaSrc;
142 float maxDistance;
143 Matrix toMapSpace;
144 Matrix toWorldSpace;
145 } PathfindingMap;
146
147 // when we execute the pathfinding algorithm, we need to store the active nodes
148 // in a queue. Each node has a position, a distance from the start, and the
149 // position of the node that we came from.
150 typedef struct PathfindingNode
151 {
152 int16_t x, y, fromX, fromY;
153 float distance;
154 } PathfindingNode;
155
156 typedef struct EnemyId
157 {
158 uint16_t index;
159 uint16_t generation;
160 } EnemyId;
161
162 typedef struct EnemyClassConfig
163 {
164 float speed;
165 float health;
166 float radius;
167 float maxAcceleration;
168 float requiredContactTime;
169 float explosionDamage;
170 float explosionRange;
171 float explosionPushbackPower;
172 int goldValue;
173 } EnemyClassConfig;
174
175 typedef struct Enemy
176 {
177 int16_t currentX, currentY;
178 int16_t nextX, nextY;
179 Vector2 simPosition;
180 Vector2 simVelocity;
181 uint16_t generation;
182 float walkedDistance;
183 float startMovingTime;
184 float damage, futureDamage;
185 float contactTime;
186 uint8_t enemyType;
187 uint8_t movePathCount;
188 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
189 } Enemy;
190
191 // a unit that uses sprites to be drawn
192 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
193 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
194 typedef struct SpriteUnit
195 {
196 Rectangle srcRect;
197 Vector2 offset;
198 int frameCount;
199 float frameDuration;
200 Rectangle srcWeaponIdleRect;
201 Vector2 srcWeaponIdleOffset;
202 Rectangle srcWeaponCooldownRect;
203 Vector2 srcWeaponCooldownOffset;
204 } SpriteUnit;
205
206 #define PROJECTILE_MAX_COUNT 1200
207 #define PROJECTILE_TYPE_NONE 0
208 #define PROJECTILE_TYPE_ARROW 1
209 #define PROJECTILE_TYPE_CATAPULT 2
210 #define PROJECTILE_TYPE_BALLISTA 3
211
212 typedef struct Projectile
213 {
214 uint8_t projectileType;
215 float shootTime;
216 float arrivalTime;
217 float distance;
218 Vector3 position;
219 Vector3 target;
220 Vector3 directionNormal;
221 EnemyId targetEnemy;
222 HitEffectConfig hitEffectConfig;
223 } Projectile;
224
225 //# Function declarations
226 float TowerGetMaxHealth(Tower *tower);
227 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
228 int EnemyAddDamageRange(Vector2 position, float range, float damage);
229 int EnemyAddDamage(Enemy *enemy, float damage);
230
231 //# Enemy functions
232 void EnemyInit();
233 void EnemyDraw();
234 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
235 void EnemyUpdate();
236 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
237 float EnemyGetMaxHealth(Enemy *enemy);
238 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
239 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
240 EnemyId EnemyGetId(Enemy *enemy);
241 Enemy *EnemyTryResolve(EnemyId enemyId);
242 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
243 int EnemyAddDamage(Enemy *enemy, float damage);
244 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
245 int EnemyCount();
246 void EnemyDrawHealthbars(Camera3D camera);
247
248 //# Tower functions
249 void TowerInit();
250 Tower *TowerGetAt(int16_t x, int16_t y);
251 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
252 Tower *GetTowerByType(uint8_t towerType);
253 int GetTowerCosts(uint8_t towerType);
254 float TowerGetMaxHealth(Tower *tower);
255 void TowerDraw();
256 void TowerUpdate();
257 void TowerDrawHealthBars(Camera3D camera);
258 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
259
260 //# Particles
261 void ParticleInit();
262 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
263 void ParticleUpdate();
264 void ParticleDraw();
265
266 //# Projectiles
267 void ProjectileInit();
268 void ProjectileDraw();
269 void ProjectileUpdate();
270 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
271
272 //# Pathfinding map
273 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
274 float PathFindingGetDistance(int mapX, int mapY);
275 Vector2 PathFindingGetGradient(Vector3 world);
276 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
277 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
278 void PathFindingMapDraw();
279
280 //# UI
281 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
282
283 //# Level
284 void DrawLevelGround(Level *level);
285 void DrawEnemyPath(Level *level, Color arrowColor);
286
287 //# variables
288 extern Level *currentLevel;
289 extern Enemy enemies[ENEMY_MAX_COUNT];
290 extern int enemyCount;
291 extern EnemyClassConfig enemyClassConfigs[];
292
293 extern GUIState guiState;
294 extern GameTime gameTime;
295 extern Tower towers[TOWER_MAX_COUNT];
296 extern int towerCount;
297
298 extern Texture2D palette, spriteSheet;
299
300 #endif
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(int blockedCellCount, Vector2 *blockedCells)
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 < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 6.0f,
28 .pushbackPowerDistance = 0.25f,
29 }
30 },
31 [TOWER_TYPE_CATAPULT] = {
32 .cooldown = 1.7f,
33 .range = 5.0f,
34 .cost = 10,
35 .maxHealth = 10,
36 .projectileSpeed = 3.0f,
37 .projectileType = PROJECTILE_TYPE_CATAPULT,
38 .hitEffect = {
39 .damage = 2.0f,
40 .areaDamageRadius = 1.75f,
41 }
42 },
43 [TOWER_TYPE_WALL] = {
44 .cost = 2,
45 .maxHealth = 10,
46 },
47 };
48
49 Tower towers[TOWER_MAX_COUNT];
50 int towerCount = 0;
51
52 Model towerModels[TOWER_TYPE_COUNT];
53
54 // definition of our archer unit
55 SpriteUnit archerUnit = {
56 .srcRect = {0, 0, 16, 16},
57 .offset = {7, 1},
58 .frameCount = 1,
59 .frameDuration = 0.0f,
60 .srcWeaponIdleRect = {16, 0, 6, 16},
61 .srcWeaponIdleOffset = {8, 0},
62 .srcWeaponCooldownRect = {22, 0, 11, 16},
63 .srcWeaponCooldownOffset = {10, 0},
64 };
65
66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
67 {
68 float xScale = flip ? -1.0f : 1.0f;
69 Camera3D camera = currentLevel->camera;
70 float size = 0.5f;
71 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
72 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
73 // we want the sprite to face the camera, so we need to calculate the up vector
74 Vector3 forward = Vector3Subtract(camera.target, camera.position);
75 Vector3 up = {0, 1, 0};
76 Vector3 right = Vector3CrossProduct(forward, up);
77 up = Vector3Normalize(Vector3CrossProduct(right, forward));
78
79 Rectangle srcRect = unit.srcRect;
80 if (unit.frameCount > 1)
81 {
82 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
83 }
84 if (flip)
85 {
86 srcRect.x += srcRect.width;
87 srcRect.width = -srcRect.width;
88 }
89 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
90
91 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
92 {
93 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
94 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
95 srcRect = unit.srcWeaponCooldownRect;
96 if (flip)
97 {
98 // position.x = flip * scale.x * 0.5f;
99 srcRect.x += srcRect.width;
100 srcRect.width = -srcRect.width;
101 offset.x = scale.x - offset.x;
102 }
103 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104 }
105 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106 {
107 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109 srcRect = unit.srcWeaponIdleRect;
110 if (flip)
111 {
112 // position.x = flip * scale.x * 0.5f;
113 srcRect.x += srcRect.width;
114 srcRect.width = -srcRect.width;
115 offset.x = scale.x - offset.x;
116 }
117 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118 }
119 }
120
121 void TowerInit()
122 {
123 for (int i = 0; i < TOWER_MAX_COUNT; i++)
124 {
125 towers[i] = (Tower){0};
126 }
127 towerCount = 0;
128
129 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131
132 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133 {
134 if (towerModels[i].materials)
135 {
136 // assign the palette texture to the material of the model (0 is not used afaik)
137 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138 }
139 }
140 }
141
142 static void TowerGunUpdate(Tower *tower)
143 {
144 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145 if (tower->cooldown <= 0.0f)
146 {
147 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148 if (enemy)
149 {
150 tower->cooldown = config.cooldown;
151 // shoot the enemy; determine future position of the enemy
152 float bulletSpeed = config.projectileSpeed;
153 Vector2 velocity = enemy->simVelocity;
154 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155 Vector2 towerPosition = {tower->x, tower->y};
156 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157 for (int i = 0; i < 8; i++) {
158 velocity = enemy->simVelocity;
159 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160 float distance = Vector2Distance(towerPosition, futurePosition);
161 float eta2 = distance / bulletSpeed;
162 if (fabs(eta - eta2) < 0.01f) {
163 break;
164 }
165 eta = (eta2 + eta) * 0.5f;
166 }
167
168 ProjectileTryAdd(config.projectileType, enemy,
169 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
170 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171 bulletSpeed, config.hitEffect);
172 enemy->futureDamage += config.hitEffect.damage;
173 tower->lastTargetPosition = futurePosition;
174 }
175 }
176 else
177 {
178 tower->cooldown -= gameTime.deltaTime;
179 }
180 }
181
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184 for (int i = 0; i < towerCount; i++)
185 {
186 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187 {
188 return &towers[i];
189 }
190 }
191 return 0;
192 }
193
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196 if (towerCount >= TOWER_MAX_COUNT)
197 {
198 return 0;
199 }
200
201 Tower *tower = TowerGetAt(x, y);
202 if (tower)
203 {
204 return 0;
205 }
206
207 tower = &towers[towerCount++];
208 tower->x = x;
209 tower->y = y;
210 tower->towerType = towerType;
211 tower->cooldown = 0.0f;
212 tower->damage = 0.0f;
213 return tower;
214 }
215
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218 for (int i = 0; i < towerCount; i++)
219 {
220 if (towers[i].towerType == towerType)
221 {
222 return &towers[i];
223 }
224 }
225 return 0;
226 }
227
228 int GetTowerCosts(uint8_t towerType)
229 {
230 return towerTypeConfigs[towerType].cost;
231 }
232
233 float TowerGetMaxHealth(Tower *tower)
234 {
235 return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237
238 void TowerDraw()
239 {
240 for (int i = 0; i < towerCount; i++)
241 {
242 Tower tower = towers[i];
243 if (tower.towerType == TOWER_TYPE_NONE)
244 {
245 continue;
246 }
247
248 switch (tower.towerType)
249 {
250 case TOWER_TYPE_ARCHER:
251 {
252 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
253 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
254 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
255 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
256 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
257 }
258 break;
259 case TOWER_TYPE_BALLISTA:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
261 break;
262 case TOWER_TYPE_CATAPULT:
263 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
264 break;
265 default:
266 if (towerModels[tower.towerType].materials)
267 {
268 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
269 } else {
270 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
271 }
272 break;
273 }
274 }
275 }
276
277 void TowerUpdate()
278 {
279 for (int i = 0; i < towerCount; i++)
280 {
281 Tower *tower = &towers[i];
282 switch (tower->towerType)
283 {
284 case TOWER_TYPE_CATAPULT:
285 case TOWER_TYPE_BALLISTA:
286 case TOWER_TYPE_ARCHER:
287 TowerGunUpdate(tower);
288 break;
289 }
290 }
291 }
292
293 void TowerDrawHealthBars(Camera3D camera)
294 {
295 for (int i = 0; i < towerCount; i++)
296 {
297 Tower *tower = &towers[i];
298 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
299 {
300 continue;
301 }
302
303 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
304 float maxHealth = TowerGetMaxHealth(tower);
305 float health = maxHealth - tower->damage;
306 float healthRatio = health / maxHealth;
307
308 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
309 }
310 }
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}, (Vector3){1.0f, 1.0f, 1.0f}, 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 EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445 int count = 0;
446 float range2 = range * range;
447 for (int i = 0; i < enemyCount; i++)
448 {
449 Enemy *enemy = &enemies[i];
450 if (enemy->enemyType == ENEMY_TYPE_NONE)
451 {
452 continue;
453 }
454 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455 if (distance2 <= range2)
456 {
457 EnemyAddDamage(enemy, damage);
458 count++;
459 }
460 }
461 return count;
462 }
463
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466 enemy->damage += damage;
467 if (enemy->damage >= EnemyGetMaxHealth(enemy))
468 {
469 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470 enemy->enemyType = ENEMY_TYPE_NONE;
471 return 1;
472 }
473
474 return 0;
475 }
476
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479 int16_t castleX = 0;
480 int16_t castleY = 0;
481 Enemy* closest = 0;
482 int16_t closestDistance = 0;
483 float range2 = range * range;
484 for (int i = 0; i < enemyCount; i++)
485 {
486 Enemy* enemy = &enemies[i];
487 if (enemy->enemyType == ENEMY_TYPE_NONE)
488 {
489 continue;
490 }
491 float maxHealth = EnemyGetMaxHealth(enemy);
492 if (enemy->futureDamage >= maxHealth)
493 {
494 // ignore enemies that will die soon
495 continue;
496 }
497 int16_t dx = castleX - enemy->currentX;
498 int16_t dy = castleY - enemy->currentY;
499 int16_t distance = abs(dx) + abs(dy);
500 if (!closest || distance < closestDistance)
501 {
502 float tdx = towerX - enemy->currentX;
503 float tdy = towerY - enemy->currentY;
504 float tdistance2 = tdx * tdx + tdy * tdy;
505 if (tdistance2 <= range2)
506 {
507 closest = enemy;
508 closestDistance = distance;
509 }
510 }
511 }
512 return closest;
513 }
514
515 int EnemyCount()
516 {
517 int count = 0;
518 for (int i = 0; i < enemyCount; i++)
519 {
520 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521 {
522 count++;
523 }
524 }
525 return count;
526 }
527
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530 for (int i = 0; i < enemyCount; i++)
531 {
532 Enemy *enemy = &enemies[i];
533 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534 {
535 continue;
536 }
537 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538 float maxHealth = EnemyGetMaxHealth(enemy);
539 float health = maxHealth - enemy->damage;
540 float healthRatio = health / maxHealth;
541
542 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543 }
544 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 typedef struct ProjectileConfig
8 {
9 float arcFactor;
10 Color color;
11 Color trailColor;
12 } ProjectileConfig;
13
14 ProjectileConfig projectileConfigs[] = {
15 [PROJECTILE_TYPE_ARROW] = {
16 .arcFactor = 0.15f,
17 .color = RED,
18 .trailColor = BROWN,
19 },
20 [PROJECTILE_TYPE_CATAPULT] = {
21 .arcFactor = 0.5f,
22 .color = RED,
23 .trailColor = GRAY,
24 },
25 [PROJECTILE_TYPE_BALLISTA] = {
26 .arcFactor = 0.025f,
27 .color = RED,
28 .trailColor = BROWN,
29 },
30 };
31
32 void ProjectileInit()
33 {
34 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
35 {
36 projectiles[i] = (Projectile){0};
37 }
38 }
39
40 void ProjectileDraw()
41 {
42 for (int i = 0; i < projectileCount; i++)
43 {
44 Projectile projectile = projectiles[i];
45 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
46 {
47 continue;
48 }
49 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
50 if (transition >= 1.0f)
51 {
52 continue;
53 }
54
55 ProjectileConfig config = projectileConfigs[projectile.projectileType];
56 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
57 {
58 float t = transition + transitionOffset * 0.3f;
59 if (t > 1.0f)
60 {
61 break;
62 }
63 Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
64 Color color = config.color;
65 color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
66 // fake a ballista flight path using parabola equation
67 float parabolaT = t - 0.5f;
68 parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
69 position.y += config.arcFactor * parabolaT * projectile.distance;
70
71 float size = 0.06f * (transitionOffset + 0.25f);
72 DrawCube(position, size, size, size, color);
73 }
74 }
75 }
76
77 void ProjectileUpdate()
78 {
79 for (int i = 0; i < projectileCount; i++)
80 {
81 Projectile *projectile = &projectiles[i];
82 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
83 {
84 continue;
85 }
86 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
87 if (transition >= 1.0f)
88 {
89 projectile->projectileType = PROJECTILE_TYPE_NONE;
90 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
91 if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
92 {
93 Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
94 enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
95 }
96
97 if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
98 {
99 EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100 // pancaked sphere explosion
101 float r = projectile->hitEffectConfig.areaDamageRadius;
102 ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103 }
104 else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105 {
106 EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107 }
108 continue;
109 }
110 }
111 }
112
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116 {
117 Projectile *projectile = &projectiles[i];
118 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119 {
120 projectile->projectileType = projectileType;
121 projectile->shootTime = gameTime.time;
122 float distance = Vector3Distance(position, target);
123 projectile->arrivalTime = gameTime.time + distance / speed;
124 projectile->position = position;
125 projectile->target = target;
126 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127 projectile->distance = distance;
128 projectile->targetEnemy = EnemyGetId(enemy);
129 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130 projectile->hitEffectConfig = hitEffectConfig;
131 return projectile;
132 }
133 }
134 return 0;
135 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4
5 static Particle particles[PARTICLE_MAX_COUNT];
6 static int particleCount = 0;
7
8 void ParticleInit()
9 {
10 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
11 {
12 particles[i] = (Particle){0};
13 }
14 particleCount = 0;
15 }
16
17 static void DrawExplosionParticle(Particle *particle, float transition)
18 {
19 Vector3 scale = particle->scale;
20 float size = 1.0f * (1.0f - transition);
21 Color startColor = WHITE;
22 Color endColor = RED;
23 Color color = ColorLerp(startColor, endColor, transition);
24
25 rlPushMatrix();
26 rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
27 rlScalef(scale.x, scale.y, scale.z);
28 DrawSphere(Vector3Zero(), size, color);
29 rlPopMatrix();
30 }
31
32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
33 {
34 if (particleCount >= PARTICLE_MAX_COUNT)
35 {
36 return;
37 }
38
39 int index = -1;
40 for (int i = 0; i < particleCount; i++)
41 {
42 if (particles[i].particleType == PARTICLE_TYPE_NONE)
43 {
44 index = i;
45 break;
46 }
47 }
48
49 if (index == -1)
50 {
51 index = particleCount++;
52 }
53
54 Particle *particle = &particles[index];
55 particle->particleType = particleType;
56 particle->spawnTime = gameTime.time;
57 particle->lifetime = lifetime;
58 particle->position = position;
59 particle->velocity = velocity;
60 particle->scale = scale;
61 }
62
63 void ParticleUpdate()
64 {
65 for (int i = 0; i < particleCount; i++)
66 {
67 Particle *particle = &particles[i];
68 if (particle->particleType == PARTICLE_TYPE_NONE)
69 {
70 continue;
71 }
72
73 float age = gameTime.time - particle->spawnTime;
74
75 if (particle->lifetime > age)
76 {
77 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
78 }
79 else {
80 particle->particleType = PARTICLE_TYPE_NONE;
81 }
82 }
83 }
84
85 void ParticleDraw()
86 {
87 for (int i = 0; i < particleCount; i++)
88 {
89 Particle particle = particles[i];
90 if (particle.particleType == PARTICLE_TYPE_NONE)
91 {
92 continue;
93 }
94
95 float age = gameTime.time - particle.spawnTime;
96 float transition = age / particle.lifetime;
97 switch (particle.particleType)
98 {
99 case PARTICLE_TYPE_EXPLOSION:
100 DrawExplosionParticle(&particle, transition);
101 break;
102 default:
103 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104 break;
105 }
106 }
107 }
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 build mode is entered when selecting a tower to build. The OK and Cancel buttons are shown on the screen and we can drag and click on the map to move the wireframe tower around as long as we want. Accidental builds are prevented by the OK button.
Prototyping-wise this could be considered done. But let's polish this!
Polishing the tower placement
The first thing to improve is to show the tower that is to be built at the current location. To communicate that the tower can be moved around, we will add a set of animated arrows that bounce around the tower.
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4 #include <stdlib.h>
5 #include <math.h>
6
7 //# Variables
8 GUIState guiState = {0};
9 GameTime gameTime = {0};
10
11 Model floorTileAModel = {0};
12 Model floorTileBModel = {0};
13 Model treeModel[2] = {0};
14 Model firTreeModel[2] = {0};
15 Model rockModels[5] = {0};
16 Model grassPatchModel[1] = {0};
17
18 Model pathArrowModel = {0};
19 Model greenArrowModel = {0};
20
21 Texture2D palette, spriteSheet;
22
23 Level levels[] = {
24 [0] = {
25 .state = LEVEL_STATE_BUILDING,
26 .initialGold = 20,
27 .waves[0] = {
28 .enemyType = ENEMY_TYPE_MINION,
29 .wave = 0,
30 .count = 5,
31 .interval = 2.5f,
32 .delay = 1.0f,
33 .spawnPosition = {2, 6},
34 },
35 .waves[1] = {
36 .enemyType = ENEMY_TYPE_MINION,
37 .wave = 0,
38 .count = 5,
39 .interval = 2.5f,
40 .delay = 1.0f,
41 .spawnPosition = {-2, 6},
42 },
43 .waves[2] = {
44 .enemyType = ENEMY_TYPE_MINION,
45 .wave = 1,
46 .count = 20,
47 .interval = 1.5f,
48 .delay = 1.0f,
49 .spawnPosition = {0, 6},
50 },
51 .waves[3] = {
52 .enemyType = ENEMY_TYPE_MINION,
53 .wave = 2,
54 .count = 30,
55 .interval = 1.2f,
56 .delay = 1.0f,
57 .spawnPosition = {0, 6},
58 }
59 },
60 };
61
62 Level *currentLevel = levels;
63
64 //# Game
65
66 static Model LoadGLBModel(char *filename)
67 {
68 Model model = LoadModel(TextFormat("data/%s.glb",filename));
69 for (int i = 0; i < model.materialCount; i++)
70 {
71 model.materials[i].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
72 }
73 return model;
74 }
75
76 void LoadAssets()
77 {
78 // load a sprite sheet that contains all units
79 spriteSheet = LoadTexture("data/spritesheet.png");
80 SetTextureFilter(spriteSheet, TEXTURE_FILTER_BILINEAR);
81
82 // we'll use a palette texture to colorize the all buildings and environment art
83 palette = LoadTexture("data/palette.png");
84 // The texture uses gradients on very small space, so we'll enable bilinear filtering
85 SetTextureFilter(palette, TEXTURE_FILTER_BILINEAR);
86
87 floorTileAModel = LoadGLBModel("floor-tile-a");
88 floorTileBModel = LoadGLBModel("floor-tile-b");
89 treeModel[0] = LoadGLBModel("leaftree-large-1-a");
90 treeModel[1] = LoadGLBModel("leaftree-large-1-b");
91 firTreeModel[0] = LoadGLBModel("firtree-1-a");
92 firTreeModel[1] = LoadGLBModel("firtree-1-b");
93 rockModels[0] = LoadGLBModel("rock-1");
94 rockModels[1] = LoadGLBModel("rock-2");
95 rockModels[2] = LoadGLBModel("rock-3");
96 rockModels[3] = LoadGLBModel("rock-4");
97 rockModels[4] = LoadGLBModel("rock-5");
98 grassPatchModel[0] = LoadGLBModel("grass-patch-1");
99
100 pathArrowModel = LoadGLBModel("direction-arrow-x");
101 greenArrowModel = LoadGLBModel("green-arrow");
102 }
103
104 void InitLevel(Level *level)
105 {
106 level->seed = (int)(GetTime() * 100.0f);
107
108 TowerInit();
109 EnemyInit();
110 ProjectileInit();
111 ParticleInit();
112 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
113
114 level->placementMode = 0;
115 level->state = LEVEL_STATE_BUILDING;
116 level->nextState = LEVEL_STATE_NONE;
117 level->playerGold = level->initialGold;
118 level->currentWave = 0;
119
120 Camera *camera = &level->camera;
121 camera->position = (Vector3){4.0f, 8.0f, 8.0f};
122 camera->target = (Vector3){0.0f, 0.0f, 0.0f};
123 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
124 camera->fovy = 10.0f;
125 camera->projection = CAMERA_ORTHOGRAPHIC;
126 }
127
128 void DrawLevelHud(Level *level)
129 {
130 const char *text = TextFormat("Gold: %d", level->playerGold);
131 Font font = GetFontDefault();
132 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
133 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
134 }
135
136 void DrawLevelReportLostWave(Level *level)
137 {
138 BeginMode3D(level->camera);
139 DrawLevelGround(level);
140 TowerDraw();
141 EnemyDraw();
142 ProjectileDraw();
143 ParticleDraw();
144 guiState.isBlocked = 0;
145 EndMode3D();
146
147 TowerDrawHealthBars(level->camera);
148
149 const char *text = "Wave lost";
150 int textWidth = MeasureText(text, 20);
151 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
152
153 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
154 {
155 level->nextState = LEVEL_STATE_RESET;
156 }
157 }
158
159 int HasLevelNextWave(Level *level)
160 {
161 for (int i = 0; i < 10; i++)
162 {
163 EnemyWave *wave = &level->waves[i];
164 if (wave->wave == level->currentWave)
165 {
166 return 1;
167 }
168 }
169 return 0;
170 }
171
172 void DrawLevelReportWonWave(Level *level)
173 {
174 BeginMode3D(level->camera);
175 DrawLevelGround(level);
176 TowerDraw();
177 EnemyDraw();
178 ProjectileDraw();
179 ParticleDraw();
180 guiState.isBlocked = 0;
181 EndMode3D();
182
183 TowerDrawHealthBars(level->camera);
184
185 const char *text = "Wave won";
186 int textWidth = MeasureText(text, 20);
187 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
188
189
190 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
191 {
192 level->nextState = LEVEL_STATE_RESET;
193 }
194
195 if (HasLevelNextWave(level))
196 {
197 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
198 {
199 level->nextState = LEVEL_STATE_BUILDING;
200 }
201 }
202 else {
203 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
204 {
205 level->nextState = LEVEL_STATE_WON_LEVEL;
206 }
207 }
208 }
209
210 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
211 {
212 static ButtonState buttonStates[8] = {0};
213 int cost = GetTowerCosts(towerType);
214 const char *text = TextFormat("%s: %d", name, cost);
215 buttonStates[towerType].isSelected = level->placementMode == towerType;
216 buttonStates[towerType].isDisabled = level->playerGold < cost;
217 if (Button(text, x, y, width, height, &buttonStates[towerType]))
218 {
219 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
220 level->nextState = LEVEL_STATE_BUILDING_PLACEMENT;
221 }
222 }
223
224 float GetRandomFloat(float min, float max)
225 {
226 int random = GetRandomValue(0, 0xfffffff);
227 return ((float)random / (float)0xfffffff) * (max - min) + min;
228 }
229
230 void DrawLevelGround(Level *level)
231 {
232 // draw checkerboard ground pattern
233 for (int x = -5; x <= 5; x += 1)
234 {
235 for (int y = -5; y <= 5; y += 1)
236 {
237 Model *model = (x + y) % 2 == 0 ? &floorTileAModel : &floorTileBModel;
238 DrawModel(*model, (Vector3){x, 0.0f, y}, 1.0f, WHITE);
239 }
240 }
241
242 int oldSeed = GetRandomValue(0, 0xfffffff);
243 SetRandomSeed(level->seed);
244 // increase probability for trees via duplicated entries
245 Model borderModels[64];
246 int maxRockCount = GetRandomValue(2, 6);
247 int maxTreeCount = GetRandomValue(10, 20);
248 int maxFirTreeCount = GetRandomValue(5, 10);
249 int maxLeafTreeCount = maxTreeCount - maxFirTreeCount;
250 int grassPatchCount = GetRandomValue(5, 30);
251
252 int modelCount = 0;
253 for (int i = 0; i < maxRockCount && modelCount < 63; i++)
254 {
255 borderModels[modelCount++] = rockModels[GetRandomValue(0, 5)];
256 }
257 for (int i = 0; i < maxLeafTreeCount && modelCount < 63; i++)
258 {
259 borderModels[modelCount++] = treeModel[GetRandomValue(0, 1)];
260 }
261 for (int i = 0; i < maxFirTreeCount && modelCount < 63; i++)
262 {
263 borderModels[modelCount++] = firTreeModel[GetRandomValue(0, 1)];
264 }
265 for (int i = 0; i < grassPatchCount && modelCount < 63; i++)
266 {
267 borderModels[modelCount++] = grassPatchModel[0];
268 }
269
270 // draw some objects around the border of the map
271 Vector3 up = {0, 1, 0};
272 // a pseudo random number generator to get the same result every time
273 const float wiggle = 0.75f;
274 const int layerCount = 3;
275 for (int layer = 0; layer < layerCount; layer++)
276 {
277 int layerPos = 6 + layer;
278 for (int x = -6 + layer; x <= 6 + layer; x += 1)
279 {
280 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
281 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, -layerPos + GetRandomFloat(0.0f, wiggle)},
282 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
283 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
284 (Vector3){x + GetRandomFloat(0.0f, wiggle), 0.0f, layerPos + GetRandomFloat(0.0f, wiggle)},
285 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
286 }
287
288 for (int z = -5 + layer; z <= 5 + layer; z += 1)
289 {
290 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
291 (Vector3){-layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
292 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
293 DrawModelEx(borderModels[GetRandomValue(0, modelCount - 1)],
294 (Vector3){layerPos + GetRandomFloat(0.0f, wiggle), 0.0f, z + GetRandomFloat(0.0f, wiggle)},
295 up, GetRandomFloat(0.0f, 360), Vector3One(), WHITE);
296 }
297 }
298
299 SetRandomSeed(oldSeed);
300 }
301
302 void DrawEnemyPath(Level *level, Color arrowColor)
303 {
304 const int castleX = 0, castleY = 0;
305 const int maxWaypointCount = 200;
306 const float timeStep = 1.0f;
307 Vector3 arrowScale = {0.75f, 0.75f, 0.75f};
308
309 // we start with a time offset to simulate the path,
310 // this way the arrows are animated in a forward moving direction
311 // The time is wrapped around the time step to get a smooth animation
312 float timeOffset = fmodf(GetTime(), timeStep);
313
314 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
315 {
316 EnemyWave *wave = &level->waves[i];
317 if (wave->wave != level->currentWave)
318 {
319 continue;
320 }
321
322 // use this dummy enemy to simulate the path
323 Enemy dummy = {
324 .enemyType = ENEMY_TYPE_MINION,
325 .simPosition = (Vector2){wave->spawnPosition.x, wave->spawnPosition.y},
326 .nextX = wave->spawnPosition.x,
327 .nextY = wave->spawnPosition.y,
328 .currentX = wave->spawnPosition.x,
329 .currentY = wave->spawnPosition.y,
330 };
331
332 float deltaTime = timeOffset;
333 for (int j = 0; j < maxWaypointCount; j++)
334 {
335 int waypointPassedCount = 0;
336 Vector2 pos = EnemyGetPosition(&dummy, deltaTime, &dummy.simVelocity, &waypointPassedCount);
337 // after the initial variable starting offset, we use a fixed time step
338 deltaTime = timeStep;
339 dummy.simPosition = pos;
340
341 // Update the dummy's position just like we do in the regular enemy update loop
342 for (int k = 0; k < waypointPassedCount; k++)
343 {
344 dummy.currentX = dummy.nextX;
345 dummy.currentY = dummy.nextY;
346 if (EnemyGetNextPosition(dummy.currentX, dummy.currentY, &dummy.nextX, &dummy.nextY) &&
347 Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
348 {
349 break;
350 }
351 }
352 if (Vector2DistanceSqr(dummy.simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
353 {
354 break;
355 }
356
357 // get the angle we need to rotate the arrow model. The velocity is just fine for this.
358 float angle = atan2f(dummy.simVelocity.x, dummy.simVelocity.y) * RAD2DEG - 90.0f;
359 DrawModelEx(pathArrowModel, (Vector3){pos.x, 0.15f, pos.y}, (Vector3){0, 1, 0}, angle, arrowScale, arrowColor);
360 }
361 }
362 }
363
364 void DrawEnemyPaths(Level *level)
365 {
366 // disable depth testing for the path arrows
367 // flush the 3D batch to draw the arrows on top of everything
368 rlDrawRenderBatchActive();
369 rlDisableDepthTest();
370 DrawEnemyPath(level, (Color){64, 64, 64, 160});
371
372 rlDrawRenderBatchActive();
373 rlEnableDepthTest();
374 DrawEnemyPath(level, WHITE);
375 }
376
377 void DrawLevelBuildingPlacementState(Level *level)
378 {
379 BeginMode3D(level->camera);
380 DrawLevelGround(level);
381
382 int blockedCellCount = 0;
383 Vector2 blockedCells[1];
384 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
385 float planeDistance = ray.position.y / -ray.direction.y;
386 float planeX = ray.direction.x * planeDistance + ray.position.x;
387 float planeY = ray.direction.z * planeDistance + ray.position.z;
388 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
389 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
390 if (level->placementMode && !guiState.isBlocked && mapX >= -5 && mapX <= 5 && mapY >= -5 && mapY <= 5 && IsMouseButtonDown(MOUSE_LEFT_BUTTON))
391 {
392 level->placementX = mapX;
393 level->placementY = mapY;
394 }
395 else
396 {
397 mapX = level->placementX;
398 mapY = level->placementY;
399 }
400 blockedCells[blockedCellCount++] = (Vector2){mapX, mapY};
401 PathFindingMapUpdate(blockedCellCount, blockedCells);
402
403 TowerDraw();
404 EnemyDraw();
405 ProjectileDraw();
406 ParticleDraw();
407 DrawEnemyPaths(level);
408
409 rlPushMatrix();
410 rlTranslatef(mapX, 0, mapY);
411 DrawCubeWires((Vector3){0.0f, 0.0f, 0.0f}, 1.0f, 0.0f, 1.0f, RED);
412 Tower dummy = {
413 .towerType = level->placementMode,
414 };
415 TowerDrawSingle(dummy);
416
417 float bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f;
418 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
419 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
420 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
421
422 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
423 DrawModelEx(greenArrowModel, (Vector3){-offset, 0.0f, 0.0f}, (Vector3){0, 1, 0}, 270, (Vector3){squeeze, 1.0f, stretch}, WHITE);
424 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, offset}, (Vector3){0, 1, 0}, 0, (Vector3){squeeze, 1.0f, stretch}, WHITE);
425 DrawModelEx(greenArrowModel, (Vector3){ 0.0f, 0.0f, -offset}, (Vector3){0, 1, 0}, 180, (Vector3){squeeze, 1.0f, stretch}, WHITE);
426 rlPopMatrix();
427
428 guiState.isBlocked = 0;
429
430 EndMode3D();
431
432 TowerDrawHealthBars(level->camera);
433
434 if (Button("Cancel", 20, GetScreenHeight() - 40, 160, 30, 0))
435 {
436 level->nextState = LEVEL_STATE_BUILDING;
437 level->placementMode = TOWER_TYPE_NONE;
438 TraceLog(LOG_INFO, "Cancel building");
439 }
440
441 if (Button("Build", GetScreenWidth() - 180, GetScreenHeight() - 40, 160, 30, 0))
442 {
443 level->nextState = LEVEL_STATE_BUILDING;
444 if (TowerTryAdd(level->placementMode, mapX, mapY))
445 {
446 level->playerGold -= GetTowerCosts(level->placementMode);
447 level->placementMode = TOWER_TYPE_NONE;
448 }
449 }
450 }
451
452 void DrawLevelBuildingState(Level *level)
453 {
454 BeginMode3D(level->camera);
455 DrawLevelGround(level);
456
457 PathFindingMapUpdate(0, 0);
458 TowerDraw();
459 EnemyDraw();
460 ProjectileDraw();
461 ParticleDraw();
462 DrawEnemyPaths(level);
463
464 guiState.isBlocked = 0;
465
466 EndMode3D();
467
468 TowerDrawHealthBars(level->camera);
469
470 DrawBuildingBuildButton(level, 10, 10, 110, 30, TOWER_TYPE_WALL, "Wall");
471 DrawBuildingBuildButton(level, 10, 50, 110, 30, TOWER_TYPE_ARCHER, "Archer");
472 DrawBuildingBuildButton(level, 10, 90, 110, 30, TOWER_TYPE_BALLISTA, "Ballista");
473 DrawBuildingBuildButton(level, 10, 130, 110, 30, TOWER_TYPE_CATAPULT, "Catapult");
474
475 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
476 {
477 level->nextState = LEVEL_STATE_RESET;
478 }
479
480 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
481 {
482 level->nextState = LEVEL_STATE_BATTLE;
483 }
484
485 const char *text = "Building phase";
486 int textWidth = MeasureText(text, 20);
487 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
488 }
489
490 void InitBattleStateConditions(Level *level)
491 {
492 level->state = LEVEL_STATE_BATTLE;
493 level->nextState = LEVEL_STATE_NONE;
494 level->waveEndTimer = 0.0f;
495 for (int i = 0; i < 10; i++)
496 {
497 EnemyWave *wave = &level->waves[i];
498 wave->spawned = 0;
499 wave->timeToSpawnNext = wave->delay;
500 }
501 }
502
503 void DrawLevelBattleState(Level *level)
504 {
505 BeginMode3D(level->camera);
506 DrawLevelGround(level);
507 TowerDraw();
508 EnemyDraw();
509 ProjectileDraw();
510 ParticleDraw();
511 guiState.isBlocked = 0;
512 EndMode3D();
513
514 EnemyDrawHealthbars(level->camera);
515 TowerDrawHealthBars(level->camera);
516
517 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
518 {
519 level->nextState = LEVEL_STATE_RESET;
520 }
521
522 int maxCount = 0;
523 int remainingCount = 0;
524 for (int i = 0; i < 10; i++)
525 {
526 EnemyWave *wave = &level->waves[i];
527 if (wave->wave != level->currentWave)
528 {
529 continue;
530 }
531 maxCount += wave->count;
532 remainingCount += wave->count - wave->spawned;
533 }
534 int aliveCount = EnemyCount();
535 remainingCount += aliveCount;
536
537 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
538 int textWidth = MeasureText(text, 20);
539 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
540 }
541
542 void DrawLevel(Level *level)
543 {
544 switch (level->state)
545 {
546 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
547 case LEVEL_STATE_BUILDING_PLACEMENT: DrawLevelBuildingPlacementState(level); break;
548 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
549 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
550 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
551 default: break;
552 }
553
554 DrawLevelHud(level);
555 }
556
557 void UpdateLevel(Level *level)
558 {
559 if (level->state == LEVEL_STATE_BATTLE)
560 {
561 int activeWaves = 0;
562 for (int i = 0; i < 10; i++)
563 {
564 EnemyWave *wave = &level->waves[i];
565 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
566 {
567 continue;
568 }
569 activeWaves++;
570 wave->timeToSpawnNext -= gameTime.deltaTime;
571 if (wave->timeToSpawnNext <= 0.0f)
572 {
573 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
574 if (enemy)
575 {
576 wave->timeToSpawnNext = wave->interval;
577 wave->spawned++;
578 }
579 }
580 }
581 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
582 level->waveEndTimer += gameTime.deltaTime;
583 if (level->waveEndTimer >= 2.0f)
584 {
585 level->nextState = LEVEL_STATE_LOST_WAVE;
586 }
587 }
588 else if (activeWaves == 0 && EnemyCount() == 0)
589 {
590 level->waveEndTimer += gameTime.deltaTime;
591 if (level->waveEndTimer >= 2.0f)
592 {
593 level->nextState = LEVEL_STATE_WON_WAVE;
594 }
595 }
596 }
597
598 PathFindingMapUpdate(0, 0);
599 EnemyUpdate();
600 TowerUpdate();
601 ProjectileUpdate();
602 ParticleUpdate();
603
604 if (level->nextState == LEVEL_STATE_RESET)
605 {
606 InitLevel(level);
607 }
608
609 if (level->nextState == LEVEL_STATE_BATTLE)
610 {
611 InitBattleStateConditions(level);
612 }
613
614 if (level->nextState == LEVEL_STATE_WON_WAVE)
615 {
616 level->currentWave++;
617 level->state = LEVEL_STATE_WON_WAVE;
618 }
619
620 if (level->nextState == LEVEL_STATE_LOST_WAVE)
621 {
622 level->state = LEVEL_STATE_LOST_WAVE;
623 }
624
625 if (level->nextState == LEVEL_STATE_BUILDING)
626 {
627 level->state = LEVEL_STATE_BUILDING;
628 }
629
630 if (level->nextState == LEVEL_STATE_BUILDING_PLACEMENT)
631 {
632 level->state = LEVEL_STATE_BUILDING_PLACEMENT;
633 }
634
635 if (level->nextState == LEVEL_STATE_WON_LEVEL)
636 {
637 // make something of this later
638 InitLevel(level);
639 }
640
641 level->nextState = LEVEL_STATE_NONE;
642 }
643
644 float nextSpawnTime = 0.0f;
645
646 void ResetGame()
647 {
648 InitLevel(currentLevel);
649 }
650
651 void InitGame()
652 {
653 TowerInit();
654 EnemyInit();
655 ProjectileInit();
656 ParticleInit();
657 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
658
659 currentLevel = levels;
660 InitLevel(currentLevel);
661 }
662
663 //# Immediate GUI functions
664
665 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth)
666 {
667 const float healthBarHeight = 6.0f;
668 const float healthBarOffset = 15.0f;
669 const float inset = 2.0f;
670 const float innerWidth = healthBarWidth - inset * 2;
671 const float innerHeight = healthBarHeight - inset * 2;
672
673 Vector2 screenPos = GetWorldToScreen(position, camera);
674 float centerX = screenPos.x - healthBarWidth * 0.5f;
675 float topY = screenPos.y - healthBarOffset;
676 DrawRectangle(centerX, topY, healthBarWidth, healthBarHeight, BLACK);
677 float healthWidth = innerWidth * healthRatio;
678 DrawRectangle(centerX + inset, topY + inset, healthWidth, innerHeight, barColor);
679 }
680
681 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
682 {
683 Rectangle bounds = {x, y, width, height};
684 int isPressed = 0;
685 int isSelected = state && state->isSelected;
686 int isDisabled = state && state->isDisabled;
687 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !isDisabled)
688 {
689 Color color = isSelected ? DARKGRAY : GRAY;
690 DrawRectangle(x, y, width, height, color);
691 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
692 {
693 isPressed = 1;
694 }
695 guiState.isBlocked = 1;
696 }
697 else
698 {
699 Color color = isSelected ? WHITE : LIGHTGRAY;
700 DrawRectangle(x, y, width, height, color);
701 }
702 Font font = GetFontDefault();
703 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
704 Color textColor = isDisabled ? GRAY : BLACK;
705 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
706 return isPressed;
707 }
708
709 //# Main game loop
710
711 void GameUpdate()
712 {
713 float dt = GetFrameTime();
714 // cap maximum delta time to 0.1 seconds to prevent large time steps
715 if (dt > 0.1f) dt = 0.1f;
716 gameTime.time += dt;
717 gameTime.deltaTime = dt;
718
719 UpdateLevel(currentLevel);
720 }
721
722 int main(void)
723 {
724 int screenWidth, screenHeight;
725 GetPreferredSize(&screenWidth, &screenHeight);
726 InitWindow(screenWidth, screenHeight, "Tower defense");
727 SetTargetFPS(30);
728
729 LoadAssets();
730 InitGame();
731
732 while (!WindowShouldClose())
733 {
734 if (IsPaused()) {
735 // canvas is not visible in browser - do nothing
736 continue;
737 }
738
739 BeginDrawing();
740 ClearBackground((Color){0x4E, 0x63, 0x26, 0xFF});
741
742 GameUpdate();
743 DrawLevel(currentLevel);
744
745 EndDrawing();
746 }
747
748 CloseWindow();
749
750 return 0;
751 }
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 Vector3 scale;
28 } Particle;
29
30 #define TOWER_MAX_COUNT 400
31 enum TowerType
32 {
33 TOWER_TYPE_NONE,
34 TOWER_TYPE_BASE,
35 TOWER_TYPE_ARCHER,
36 TOWER_TYPE_BALLISTA,
37 TOWER_TYPE_CATAPULT,
38 TOWER_TYPE_WALL,
39 TOWER_TYPE_COUNT
40 };
41
42 typedef struct HitEffectConfig
43 {
44 float damage;
45 float areaDamageRadius;
46 float pushbackPowerDistance;
47 } HitEffectConfig;
48
49 typedef struct TowerTypeConfig
50 {
51 float cooldown;
52 float range;
53 float projectileSpeed;
54
55 uint8_t cost;
56 uint8_t projectileType;
57 uint16_t maxHealth;
58
59 HitEffectConfig hitEffect;
60 } TowerTypeConfig;
61
62 typedef struct Tower
63 {
64 int16_t x, y;
65 uint8_t towerType;
66 Vector2 lastTargetPosition;
67 float cooldown;
68 float damage;
69 } Tower;
70
71 typedef struct GameTime
72 {
73 float time;
74 float deltaTime;
75 } GameTime;
76
77 typedef struct ButtonState {
78 char isSelected;
79 char isDisabled;
80 } ButtonState;
81
82 typedef struct GUIState {
83 int isBlocked;
84 } GUIState;
85
86 typedef enum LevelState
87 {
88 LEVEL_STATE_NONE,
89 LEVEL_STATE_BUILDING,
90 LEVEL_STATE_BUILDING_PLACEMENT,
91 LEVEL_STATE_BATTLE,
92 LEVEL_STATE_WON_WAVE,
93 LEVEL_STATE_LOST_WAVE,
94 LEVEL_STATE_WON_LEVEL,
95 LEVEL_STATE_RESET,
96 } LevelState;
97
98 typedef struct EnemyWave {
99 uint8_t enemyType;
100 uint8_t wave;
101 uint16_t count;
102 float interval;
103 float delay;
104 Vector2 spawnPosition;
105
106 uint16_t spawned;
107 float timeToSpawnNext;
108 } EnemyWave;
109
110 #define ENEMY_MAX_WAVE_COUNT 10
111
112 typedef struct Level
113 {
114 int seed;
115 LevelState state;
116 LevelState nextState;
117 Camera3D camera;
118 int placementMode;
119 int16_t placementX;
120 int16_t placementY;
121 Vector2 placementTransitionPosition;
122
123 int initialGold;
124 int playerGold;
125
126 EnemyWave waves[ENEMY_MAX_WAVE_COUNT];
127 int currentWave;
128 float waveEndTimer;
129 } Level;
130
131 typedef struct DeltaSrc
132 {
133 char x, y;
134 } DeltaSrc;
135
136 typedef struct PathfindingMap
137 {
138 int width, height;
139 float scale;
140 float *distances;
141 long *towerIndex;
142 DeltaSrc *deltaSrc;
143 float maxDistance;
144 Matrix toMapSpace;
145 Matrix toWorldSpace;
146 } PathfindingMap;
147
148 // when we execute the pathfinding algorithm, we need to store the active nodes
149 // in a queue. Each node has a position, a distance from the start, and the
150 // position of the node that we came from.
151 typedef struct PathfindingNode
152 {
153 int16_t x, y, fromX, fromY;
154 float distance;
155 } PathfindingNode;
156
157 typedef struct EnemyId
158 {
159 uint16_t index;
160 uint16_t generation;
161 } EnemyId;
162
163 typedef struct EnemyClassConfig
164 {
165 float speed;
166 float health;
167 float radius;
168 float maxAcceleration;
169 float requiredContactTime;
170 float explosionDamage;
171 float explosionRange;
172 float explosionPushbackPower;
173 int goldValue;
174 } EnemyClassConfig;
175
176 typedef struct Enemy
177 {
178 int16_t currentX, currentY;
179 int16_t nextX, nextY;
180 Vector2 simPosition;
181 Vector2 simVelocity;
182 uint16_t generation;
183 float walkedDistance;
184 float startMovingTime;
185 float damage, futureDamage;
186 float contactTime;
187 uint8_t enemyType;
188 uint8_t movePathCount;
189 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
190 } Enemy;
191
192 // a unit that uses sprites to be drawn
193 #define SPRITE_UNIT_PHASE_WEAPON_IDLE 0
194 #define SPRITE_UNIT_PHASE_WEAPON_COOLDOWN 1
195 typedef struct SpriteUnit
196 {
197 Rectangle srcRect;
198 Vector2 offset;
199 int frameCount;
200 float frameDuration;
201 Rectangle srcWeaponIdleRect;
202 Vector2 srcWeaponIdleOffset;
203 Rectangle srcWeaponCooldownRect;
204 Vector2 srcWeaponCooldownOffset;
205 } SpriteUnit;
206
207 #define PROJECTILE_MAX_COUNT 1200
208 #define PROJECTILE_TYPE_NONE 0
209 #define PROJECTILE_TYPE_ARROW 1
210 #define PROJECTILE_TYPE_CATAPULT 2
211 #define PROJECTILE_TYPE_BALLISTA 3
212
213 typedef struct Projectile
214 {
215 uint8_t projectileType;
216 float shootTime;
217 float arrivalTime;
218 float distance;
219 Vector3 position;
220 Vector3 target;
221 Vector3 directionNormal;
222 EnemyId targetEnemy;
223 HitEffectConfig hitEffectConfig;
224 } Projectile;
225
226 //# Function declarations
227 float TowerGetMaxHealth(Tower *tower);
228 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
229 int EnemyAddDamageRange(Vector2 position, float range, float damage);
230 int EnemyAddDamage(Enemy *enemy, float damage);
231
232 //# Enemy functions
233 void EnemyInit();
234 void EnemyDraw();
235 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
236 void EnemyUpdate();
237 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
238 float EnemyGetMaxHealth(Enemy *enemy);
239 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
240 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
241 EnemyId EnemyGetId(Enemy *enemy);
242 Enemy *EnemyTryResolve(EnemyId enemyId);
243 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
244 int EnemyAddDamage(Enemy *enemy, float damage);
245 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
246 int EnemyCount();
247 void EnemyDrawHealthbars(Camera3D camera);
248
249 //# Tower functions
250 void TowerInit();
251 Tower *TowerGetAt(int16_t x, int16_t y);
252 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
253 Tower *GetTowerByType(uint8_t towerType);
254 int GetTowerCosts(uint8_t towerType);
255 float TowerGetMaxHealth(Tower *tower);
256 void TowerDraw();
257 void TowerDrawSingle(Tower tower);
258 void TowerUpdate();
259 void TowerDrawHealthBars(Camera3D camera);
260 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase);
261
262 //# Particles
263 void ParticleInit();
264 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime);
265 void ParticleUpdate();
266 void ParticleDraw();
267
268 //# Projectiles
269 void ProjectileInit();
270 void ProjectileDraw();
271 void ProjectileUpdate();
272 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig);
273
274 //# Pathfinding map
275 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
276 float PathFindingGetDistance(int mapX, int mapY);
277 Vector2 PathFindingGetGradient(Vector3 world);
278 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
279 void PathFindingMapUpdate(int blockedCellCount, Vector2 *blockedCells);
280 void PathFindingMapDraw();
281
282 //# UI
283 void DrawHealthBar(Camera3D camera, Vector3 position, float healthRatio, Color barColor, float healthBarWidth);
284
285 //# Level
286 void DrawLevelGround(Level *level);
287 void DrawEnemyPath(Level *level, Color arrowColor);
288
289 //# variables
290 extern Level *currentLevel;
291 extern Enemy enemies[ENEMY_MAX_COUNT];
292 extern int enemyCount;
293 extern EnemyClassConfig enemyClassConfigs[];
294
295 extern GUIState guiState;
296 extern GameTime gameTime;
297 extern Tower towers[TOWER_MAX_COUNT];
298 extern int towerCount;
299
300 extern Texture2D palette, spriteSheet;
301
302 #endif
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(int blockedCellCount, Vector2 *blockedCells)
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 < blockedCellCount; i++)
131 {
132 int16_t mapX, mapY;
133 if (!PathFindingFromWorldToMapPosition((Vector3){blockedCells[i].x, 0.0f, blockedCells[i].y}, &mapX, &mapY))
134 {
135 continue;
136 }
137 int index = mapY * width + mapX;
138 pathfindingMap.towerIndex[index] = -2;
139 }
140
141 for (int i = 0; i < towerCount; i++)
142 {
143 Tower *tower = &towers[i];
144 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
145 {
146 continue;
147 }
148 int16_t mapX, mapY;
149 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
150 // this would not work correctly and needs to be refined to allow towers covering multiple cells
151 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
152 // one cell. For now.
153 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
154 {
155 continue;
156 }
157 int index = mapY * width + mapX;
158 pathfindingMap.towerIndex[index] = i;
159 }
160
161 // we start at the castle and add the castle to the queue
162 pathfindingMap.maxDistance = 0.0f;
163 pathfindingNodeQueueCount = 0;
164 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
165 PathfindingNode *node = 0;
166 while ((node = PathFindingNodePop()))
167 {
168 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
169 {
170 continue;
171 }
172 int index = node->y * width + node->x;
173 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
174 {
175 continue;
176 }
177
178 int deltaX = node->x - node->fromX;
179 int deltaY = node->y - node->fromY;
180 // even if the cell is blocked by a tower, we still may want to store the direction
181 // (though this might not be needed, IDK right now)
182 pathfindingMap.deltaSrc[index].x = (char) deltaX;
183 pathfindingMap.deltaSrc[index].y = (char) deltaY;
184
185 // we skip nodes that are blocked by towers or by the provided blocked cells
186 if (pathfindingMap.towerIndex[index] != -1)
187 {
188 node->distance += 8.0f;
189 }
190 pathfindingMap.distances[index] = node->distance;
191 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
192 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
193 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
194 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
195 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
196 }
197 }
198
199 void PathFindingMapDraw()
200 {
201 float cellSize = pathfindingMap.scale * 0.9f;
202 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
203 for (int x = 0; x < pathfindingMap.width; x++)
204 {
205 for (int y = 0; y < pathfindingMap.height; y++)
206 {
207 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
208 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
209 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
210 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
211 // animate the distance "wave" to show how the pathfinding algorithm expands
212 // from the castle
213 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
214 {
215 color = BLACK;
216 }
217 DrawCube(position, cellSize, 0.1f, cellSize, color);
218 }
219 }
220 }
221
222 Vector2 PathFindingGetGradient(Vector3 world)
223 {
224 int16_t mapX, mapY;
225 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
226 {
227 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
228 return (Vector2){(float)-delta.x, (float)-delta.y};
229 }
230 // fallback to a simple gradient calculation
231 float n = PathFindingGetDistance(mapX, mapY - 1);
232 float s = PathFindingGetDistance(mapX, mapY + 1);
233 float w = PathFindingGetDistance(mapX - 1, mapY);
234 float e = PathFindingGetDistance(mapX + 1, mapY);
235 return (Vector2){w - e + 0.25f, n - s + 0.125f};
236 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static TowerTypeConfig towerTypeConfigs[TOWER_TYPE_COUNT] = {
5 [TOWER_TYPE_BASE] = {
6 .maxHealth = 10,
7 },
8 [TOWER_TYPE_ARCHER] = {
9 .cooldown = 0.5f,
10 .range = 3.0f,
11 .cost = 6,
12 .maxHealth = 10,
13 .projectileSpeed = 4.0f,
14 .projectileType = PROJECTILE_TYPE_ARROW,
15 .hitEffect = {
16 .damage = 3.0f,
17 }
18 },
19 [TOWER_TYPE_BALLISTA] = {
20 .cooldown = 1.5f,
21 .range = 6.0f,
22 .cost = 9,
23 .maxHealth = 10,
24 .projectileSpeed = 10.0f,
25 .projectileType = PROJECTILE_TYPE_BALLISTA,
26 .hitEffect = {
27 .damage = 6.0f,
28 .pushbackPowerDistance = 0.25f,
29 }
30 },
31 [TOWER_TYPE_CATAPULT] = {
32 .cooldown = 1.7f,
33 .range = 5.0f,
34 .cost = 10,
35 .maxHealth = 10,
36 .projectileSpeed = 3.0f,
37 .projectileType = PROJECTILE_TYPE_CATAPULT,
38 .hitEffect = {
39 .damage = 2.0f,
40 .areaDamageRadius = 1.75f,
41 }
42 },
43 [TOWER_TYPE_WALL] = {
44 .cost = 2,
45 .maxHealth = 10,
46 },
47 };
48
49 Tower towers[TOWER_MAX_COUNT];
50 int towerCount = 0;
51
52 Model towerModels[TOWER_TYPE_COUNT];
53
54 // definition of our archer unit
55 SpriteUnit archerUnit = {
56 .srcRect = {0, 0, 16, 16},
57 .offset = {7, 1},
58 .frameCount = 1,
59 .frameDuration = 0.0f,
60 .srcWeaponIdleRect = {16, 0, 6, 16},
61 .srcWeaponIdleOffset = {8, 0},
62 .srcWeaponCooldownRect = {22, 0, 11, 16},
63 .srcWeaponCooldownOffset = {10, 0},
64 };
65
66 void DrawSpriteUnit(SpriteUnit unit, Vector3 position, float t, int flip, int phase)
67 {
68 float xScale = flip ? -1.0f : 1.0f;
69 Camera3D camera = currentLevel->camera;
70 float size = 0.5f;
71 Vector2 offset = (Vector2){ unit.offset.x / 16.0f * size, unit.offset.y / 16.0f * size * xScale };
72 Vector2 scale = (Vector2){ unit.srcRect.width / 16.0f * size, unit.srcRect.height / 16.0f * size };
73 // we want the sprite to face the camera, so we need to calculate the up vector
74 Vector3 forward = Vector3Subtract(camera.target, camera.position);
75 Vector3 up = {0, 1, 0};
76 Vector3 right = Vector3CrossProduct(forward, up);
77 up = Vector3Normalize(Vector3CrossProduct(right, forward));
78
79 Rectangle srcRect = unit.srcRect;
80 if (unit.frameCount > 1)
81 {
82 srcRect.x += (int)(t / unit.frameDuration) % unit.frameCount * srcRect.width;
83 }
84 if (flip)
85 {
86 srcRect.x += srcRect.width;
87 srcRect.width = -srcRect.width;
88 }
89 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
90
91 if (phase == SPRITE_UNIT_PHASE_WEAPON_COOLDOWN && unit.srcWeaponCooldownRect.width > 0)
92 {
93 offset = (Vector2){ unit.srcWeaponCooldownOffset.x / 16.0f * size, unit.srcWeaponCooldownOffset.y / 16.0f * size };
94 scale = (Vector2){ unit.srcWeaponCooldownRect.width / 16.0f * size, unit.srcWeaponCooldownRect.height / 16.0f * size };
95 srcRect = unit.srcWeaponCooldownRect;
96 if (flip)
97 {
98 // position.x = flip * scale.x * 0.5f;
99 srcRect.x += srcRect.width;
100 srcRect.width = -srcRect.width;
101 offset.x = scale.x - offset.x;
102 }
103 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
104 }
105 else if (phase == SPRITE_UNIT_PHASE_WEAPON_IDLE && unit.srcWeaponIdleRect.width > 0)
106 {
107 offset = (Vector2){ unit.srcWeaponIdleOffset.x / 16.0f * size, unit.srcWeaponIdleOffset.y / 16.0f * size };
108 scale = (Vector2){ unit.srcWeaponIdleRect.width / 16.0f * size, unit.srcWeaponIdleRect.height / 16.0f * size };
109 srcRect = unit.srcWeaponIdleRect;
110 if (flip)
111 {
112 // position.x = flip * scale.x * 0.5f;
113 srcRect.x += srcRect.width;
114 srcRect.width = -srcRect.width;
115 offset.x = scale.x - offset.x;
116 }
117 DrawBillboardPro(camera, spriteSheet, srcRect, position, up, scale, offset, 0, WHITE);
118 }
119 }
120
121 void TowerInit()
122 {
123 for (int i = 0; i < TOWER_MAX_COUNT; i++)
124 {
125 towers[i] = (Tower){0};
126 }
127 towerCount = 0;
128
129 towerModels[TOWER_TYPE_BASE] = LoadModel("data/keep.glb");
130 towerModels[TOWER_TYPE_WALL] = LoadModel("data/wall-0000.glb");
131
132 for (int i = 0; i < TOWER_TYPE_COUNT; i++)
133 {
134 if (towerModels[i].materials)
135 {
136 // assign the palette texture to the material of the model (0 is not used afaik)
137 towerModels[i].materials[1].maps[MATERIAL_MAP_DIFFUSE].texture = palette;
138 }
139 }
140 }
141
142 static void TowerGunUpdate(Tower *tower)
143 {
144 TowerTypeConfig config = towerTypeConfigs[tower->towerType];
145 if (tower->cooldown <= 0.0f)
146 {
147 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, config.range);
148 if (enemy)
149 {
150 tower->cooldown = config.cooldown;
151 // shoot the enemy; determine future position of the enemy
152 float bulletSpeed = config.projectileSpeed;
153 Vector2 velocity = enemy->simVelocity;
154 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
155 Vector2 towerPosition = {tower->x, tower->y};
156 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
157 for (int i = 0; i < 8; i++) {
158 velocity = enemy->simVelocity;
159 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
160 float distance = Vector2Distance(towerPosition, futurePosition);
161 float eta2 = distance / bulletSpeed;
162 if (fabs(eta - eta2) < 0.01f) {
163 break;
164 }
165 eta = (eta2 + eta) * 0.5f;
166 }
167
168 ProjectileTryAdd(config.projectileType, enemy,
169 (Vector3){towerPosition.x, 1.33f, towerPosition.y},
170 (Vector3){futurePosition.x, 0.25f, futurePosition.y},
171 bulletSpeed, config.hitEffect);
172 enemy->futureDamage += config.hitEffect.damage;
173 tower->lastTargetPosition = futurePosition;
174 }
175 }
176 else
177 {
178 tower->cooldown -= gameTime.deltaTime;
179 }
180 }
181
182 Tower *TowerGetAt(int16_t x, int16_t y)
183 {
184 for (int i = 0; i < towerCount; i++)
185 {
186 if (towers[i].x == x && towers[i].y == y && towers[i].towerType != TOWER_TYPE_NONE)
187 {
188 return &towers[i];
189 }
190 }
191 return 0;
192 }
193
194 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
195 {
196 if (towerCount >= TOWER_MAX_COUNT)
197 {
198 return 0;
199 }
200
201 Tower *tower = TowerGetAt(x, y);
202 if (tower)
203 {
204 return 0;
205 }
206
207 tower = &towers[towerCount++];
208 tower->x = x;
209 tower->y = y;
210 tower->towerType = towerType;
211 tower->cooldown = 0.0f;
212 tower->damage = 0.0f;
213 return tower;
214 }
215
216 Tower *GetTowerByType(uint8_t towerType)
217 {
218 for (int i = 0; i < towerCount; i++)
219 {
220 if (towers[i].towerType == towerType)
221 {
222 return &towers[i];
223 }
224 }
225 return 0;
226 }
227
228 int GetTowerCosts(uint8_t towerType)
229 {
230 return towerTypeConfigs[towerType].cost;
231 }
232
233 float TowerGetMaxHealth(Tower *tower)
234 {
235 return towerTypeConfigs[tower->towerType].maxHealth;
236 }
237
238 void TowerDrawSingle(Tower tower)
239 {
240 if (tower.towerType == TOWER_TYPE_NONE)
241 {
242 return;
243 }
244
245 switch (tower.towerType)
246 {
247 case TOWER_TYPE_ARCHER:
248 {
249 Vector2 screenPosTower = GetWorldToScreen((Vector3){tower.x, 0.0f, tower.y}, currentLevel->camera);
250 Vector2 screenPosTarget = GetWorldToScreen((Vector3){tower.lastTargetPosition.x, 0.0f, tower.lastTargetPosition.y}, currentLevel->camera);
251 DrawModel(towerModels[TOWER_TYPE_WALL], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
252 DrawSpriteUnit(archerUnit, (Vector3){tower.x, 1.0f, tower.y}, 0, screenPosTarget.x > screenPosTower.x,
253 tower.cooldown > 0.2f ? SPRITE_UNIT_PHASE_WEAPON_COOLDOWN : SPRITE_UNIT_PHASE_WEAPON_IDLE);
254 }
255 break;
256 case TOWER_TYPE_BALLISTA:
257 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, BROWN);
258 break;
259 case TOWER_TYPE_CATAPULT:
260 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, DARKGRAY);
261 break;
262 default:
263 if (towerModels[tower.towerType].materials)
264 {
265 DrawModel(towerModels[tower.towerType], (Vector3){tower.x, 0.0f, tower.y}, 1.0f, WHITE);
266 } else {
267 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
268 }
269 break;
270 }
271 }
272
273 void TowerDraw()
274 {
275 for (int i = 0; i < towerCount; i++)
276 {
277 TowerDrawSingle(towers[i]);
278 }
279 }
280
281 void TowerUpdate()
282 {
283 for (int i = 0; i < towerCount; i++)
284 {
285 Tower *tower = &towers[i];
286 switch (tower->towerType)
287 {
288 case TOWER_TYPE_CATAPULT:
289 case TOWER_TYPE_BALLISTA:
290 case TOWER_TYPE_ARCHER:
291 TowerGunUpdate(tower);
292 break;
293 }
294 }
295 }
296
297 void TowerDrawHealthBars(Camera3D camera)
298 {
299 for (int i = 0; i < towerCount; i++)
300 {
301 Tower *tower = &towers[i];
302 if (tower->towerType == TOWER_TYPE_NONE || tower->damage <= 0.0f)
303 {
304 continue;
305 }
306
307 Vector3 position = (Vector3){tower->x, 0.5f, tower->y};
308 float maxHealth = TowerGetMaxHealth(tower);
309 float health = maxHealth - tower->damage;
310 float healthRatio = health / maxHealth;
311
312 DrawHealthBar(camera, position, healthRatio, GREEN, 35.0f);
313 }
314 }
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}, (Vector3){1.0f, 1.0f, 1.0f}, 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 EnemyAddDamageRange(Vector2 position, float range, float damage)
444 {
445 int count = 0;
446 float range2 = range * range;
447 for (int i = 0; i < enemyCount; i++)
448 {
449 Enemy *enemy = &enemies[i];
450 if (enemy->enemyType == ENEMY_TYPE_NONE)
451 {
452 continue;
453 }
454 float distance2 = Vector2DistanceSqr(position, enemy->simPosition);
455 if (distance2 <= range2)
456 {
457 EnemyAddDamage(enemy, damage);
458 count++;
459 }
460 }
461 return count;
462 }
463
464 int EnemyAddDamage(Enemy *enemy, float damage)
465 {
466 enemy->damage += damage;
467 if (enemy->damage >= EnemyGetMaxHealth(enemy))
468 {
469 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
470 enemy->enemyType = ENEMY_TYPE_NONE;
471 return 1;
472 }
473
474 return 0;
475 }
476
477 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
478 {
479 int16_t castleX = 0;
480 int16_t castleY = 0;
481 Enemy* closest = 0;
482 int16_t closestDistance = 0;
483 float range2 = range * range;
484 for (int i = 0; i < enemyCount; i++)
485 {
486 Enemy* enemy = &enemies[i];
487 if (enemy->enemyType == ENEMY_TYPE_NONE)
488 {
489 continue;
490 }
491 float maxHealth = EnemyGetMaxHealth(enemy);
492 if (enemy->futureDamage >= maxHealth)
493 {
494 // ignore enemies that will die soon
495 continue;
496 }
497 int16_t dx = castleX - enemy->currentX;
498 int16_t dy = castleY - enemy->currentY;
499 int16_t distance = abs(dx) + abs(dy);
500 if (!closest || distance < closestDistance)
501 {
502 float tdx = towerX - enemy->currentX;
503 float tdy = towerY - enemy->currentY;
504 float tdistance2 = tdx * tdx + tdy * tdy;
505 if (tdistance2 <= range2)
506 {
507 closest = enemy;
508 closestDistance = distance;
509 }
510 }
511 }
512 return closest;
513 }
514
515 int EnemyCount()
516 {
517 int count = 0;
518 for (int i = 0; i < enemyCount; i++)
519 {
520 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
521 {
522 count++;
523 }
524 }
525 return count;
526 }
527
528 void EnemyDrawHealthbars(Camera3D camera)
529 {
530 for (int i = 0; i < enemyCount; i++)
531 {
532 Enemy *enemy = &enemies[i];
533 if (enemy->enemyType == ENEMY_TYPE_NONE || enemy->damage == 0.0f)
534 {
535 continue;
536 }
537 Vector3 position = (Vector3){enemy->simPosition.x, 0.5f, enemy->simPosition.y};
538 float maxHealth = EnemyGetMaxHealth(enemy);
539 float health = maxHealth - enemy->damage;
540 float healthRatio = health / maxHealth;
541
542 DrawHealthBar(camera, position, healthRatio, GREEN, 15.0f);
543 }
544 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 typedef struct ProjectileConfig
8 {
9 float arcFactor;
10 Color color;
11 Color trailColor;
12 } ProjectileConfig;
13
14 ProjectileConfig projectileConfigs[] = {
15 [PROJECTILE_TYPE_ARROW] = {
16 .arcFactor = 0.15f,
17 .color = RED,
18 .trailColor = BROWN,
19 },
20 [PROJECTILE_TYPE_CATAPULT] = {
21 .arcFactor = 0.5f,
22 .color = RED,
23 .trailColor = GRAY,
24 },
25 [PROJECTILE_TYPE_BALLISTA] = {
26 .arcFactor = 0.025f,
27 .color = RED,
28 .trailColor = BROWN,
29 },
30 };
31
32 void ProjectileInit()
33 {
34 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
35 {
36 projectiles[i] = (Projectile){0};
37 }
38 }
39
40 void ProjectileDraw()
41 {
42 for (int i = 0; i < projectileCount; i++)
43 {
44 Projectile projectile = projectiles[i];
45 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
46 {
47 continue;
48 }
49 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
50 if (transition >= 1.0f)
51 {
52 continue;
53 }
54
55 ProjectileConfig config = projectileConfigs[projectile.projectileType];
56 for (float transitionOffset = 0.0f; transitionOffset < 1.0f; transitionOffset += 0.1f)
57 {
58 float t = transition + transitionOffset * 0.3f;
59 if (t > 1.0f)
60 {
61 break;
62 }
63 Vector3 position = Vector3Lerp(projectile.position, projectile.target, t);
64 Color color = config.color;
65 color = ColorLerp(config.trailColor, config.color, transitionOffset * transitionOffset);
66 // fake a ballista flight path using parabola equation
67 float parabolaT = t - 0.5f;
68 parabolaT = 1.0f - 4.0f * parabolaT * parabolaT;
69 position.y += config.arcFactor * parabolaT * projectile.distance;
70
71 float size = 0.06f * (transitionOffset + 0.25f);
72 DrawCube(position, size, size, size, color);
73 }
74 }
75 }
76
77 void ProjectileUpdate()
78 {
79 for (int i = 0; i < projectileCount; i++)
80 {
81 Projectile *projectile = &projectiles[i];
82 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
83 {
84 continue;
85 }
86 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
87 if (transition >= 1.0f)
88 {
89 projectile->projectileType = PROJECTILE_TYPE_NONE;
90 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
91 if (enemy && projectile->hitEffectConfig.pushbackPowerDistance > 0.0f)
92 {
93 Vector2 direction = Vector2Normalize(Vector2Subtract((Vector2){projectile->target.x, projectile->target.z}, enemy->simPosition));
94 enemy->simPosition = Vector2Add(enemy->simPosition, Vector2Scale(direction, projectile->hitEffectConfig.pushbackPowerDistance));
95 }
96
97 if (projectile->hitEffectConfig.areaDamageRadius > 0.0f)
98 {
99 EnemyAddDamageRange((Vector2){projectile->target.x, projectile->target.z}, projectile->hitEffectConfig.areaDamageRadius, projectile->hitEffectConfig.damage);
100 // pancaked sphere explosion
101 float r = projectile->hitEffectConfig.areaDamageRadius;
102 ParticleAdd(PARTICLE_TYPE_EXPLOSION, projectile->target, (Vector3){0}, (Vector3){r, r * 0.2f, r}, 0.33f);
103 }
104 else if (projectile->hitEffectConfig.damage > 0.0f && enemy)
105 {
106 EnemyAddDamage(enemy, projectile->hitEffectConfig.damage);
107 }
108 continue;
109 }
110 }
111 }
112
113 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector3 position, Vector3 target, float speed, HitEffectConfig hitEffectConfig)
114 {
115 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
116 {
117 Projectile *projectile = &projectiles[i];
118 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
119 {
120 projectile->projectileType = projectileType;
121 projectile->shootTime = gameTime.time;
122 float distance = Vector3Distance(position, target);
123 projectile->arrivalTime = gameTime.time + distance / speed;
124 projectile->position = position;
125 projectile->target = target;
126 projectile->directionNormal = Vector3Scale(Vector3Subtract(target, position), 1.0f / distance);
127 projectile->distance = distance;
128 projectile->targetEnemy = EnemyGetId(enemy);
129 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
130 projectile->hitEffectConfig = hitEffectConfig;
131 return projectile;
132 }
133 }
134 return 0;
135 }
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <rlgl.h>
4
5 static Particle particles[PARTICLE_MAX_COUNT];
6 static int particleCount = 0;
7
8 void ParticleInit()
9 {
10 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
11 {
12 particles[i] = (Particle){0};
13 }
14 particleCount = 0;
15 }
16
17 static void DrawExplosionParticle(Particle *particle, float transition)
18 {
19 Vector3 scale = particle->scale;
20 float size = 1.0f * (1.0f - transition);
21 Color startColor = WHITE;
22 Color endColor = RED;
23 Color color = ColorLerp(startColor, endColor, transition);
24
25 rlPushMatrix();
26 rlTranslatef(particle->position.x, particle->position.y, particle->position.z);
27 rlScalef(scale.x, scale.y, scale.z);
28 DrawSphere(Vector3Zero(), size, color);
29 rlPopMatrix();
30 }
31
32 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, Vector3 scale, float lifetime)
33 {
34 if (particleCount >= PARTICLE_MAX_COUNT)
35 {
36 return;
37 }
38
39 int index = -1;
40 for (int i = 0; i < particleCount; i++)
41 {
42 if (particles[i].particleType == PARTICLE_TYPE_NONE)
43 {
44 index = i;
45 break;
46 }
47 }
48
49 if (index == -1)
50 {
51 index = particleCount++;
52 }
53
54 Particle *particle = &particles[index];
55 particle->particleType = particleType;
56 particle->spawnTime = gameTime.time;
57 particle->lifetime = lifetime;
58 particle->position = position;
59 particle->velocity = velocity;
60 particle->scale = scale;
61 }
62
63 void ParticleUpdate()
64 {
65 for (int i = 0; i < particleCount; i++)
66 {
67 Particle *particle = &particles[i];
68 if (particle->particleType == PARTICLE_TYPE_NONE)
69 {
70 continue;
71 }
72
73 float age = gameTime.time - particle->spawnTime;
74
75 if (particle->lifetime > age)
76 {
77 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
78 }
79 else {
80 particle->particleType = PARTICLE_TYPE_NONE;
81 }
82 }
83 }
84
85 void ParticleDraw()
86 {
87 for (int i = 0; i < particleCount; i++)
88 {
89 Particle particle = particles[i];
90 if (particle.particleType == PARTICLE_TYPE_NONE)
91 {
92 continue;
93 }
94
95 float age = gameTime.time - particle.spawnTime;
96 float transition = age / particle.lifetime;
97 switch (particle.particleType)
98 {
99 case PARTICLE_TYPE_EXPLOSION:
100 DrawExplosionParticle(&particle, transition);
101 break;
102 default:
103 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
104 break;
105 }
106 }
107 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
When selecting a tower for placement, we see the tower graphics drawn in place and 4 animated arrows are bouncing around. The building is rendered by creating a dummy tower struct and calling the function that is drawing the tower.
How the arrow animations work
The arrows are animated by using a sine wave function that is mapped to the range [0, 1]. An additional squeeze and stretch effect is added to the arrows to make them look more playful. The formulas for doing this are quite compact and not very readable unless you are familiar with the functions used. The relevant code is here:
1 float bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f;
2 float offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f;
3 float squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f;
4 float stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f;
5
6 DrawModelEx(greenArrowModel, (Vector3){ offset, 0.0f, 0.0f},
7 (Vector3){0, 1, 0}, 90, (Vector3){squeeze, 1.0f, stretch}, WHITE);
The bounce and stretch effect is based both on a single sine wave that is mapped to the range [0, 1]. The offset (bounce movement), the squeeze and stretch are derived from this value.
Animating elements via code/math requires an understanding of the functions that can be used to create the desired effect. Such code is usually not liked by many because it is hard to read and understand, as it requires experience on how these functions work.
Still, let's give it a try and decipher the meanings, I think it is a good exercise:
bounce = sinf(GetTime() * 8.0f) * 0.5f + 0.5f
sinf(GetTime()) would produce a sine wave that oscillates between -1 and 1 within a period of roughly 6.28 seconds (2*π). When multiplying this with 8.0f, the period is shortened to ~0.8 seconds. The rest of the formula ( x * 0.5f + 0.5f ) halves the amplitude (range [-0.5, 0.5]) and adds 0.5 so the range is [0, 1]. So bounce is a value that oscillates between 0 and 1 within a period of 0.8 seconds.
Let's visualize what this code does by plotting a graph of the bounce value over time:
Sine waves are very useful for animating pulses and smooth oscillations, like for positions, scales, or colors of objects.
Let's move on:
offset = fmaxf(0.0f, bounce - 0.4f) * 0.35f + 0.7f
fmaxf(0.0f, bounce - 0.4f) ensures that the value is at least 0. Since the bounce value oscillates between 0 and 1 and we subtract 0.4, the value is staying 0 for 40% of the time. The rest of the time, the value is increasing from 0 to 0.6 with the sine wave oscillation. This capping is responsible for stopping the arrow's movement for a short time when it is on its low. The * 0.35f + 0.7f is mapping the range of the result to a desireable range. 0.35 is the bounce range and 0.7 is the offset.
This is how it looks like:
The squeeze effect works very similar to the offset effect:
squeeze = -fminf(0.0f, bounce - 0.4f) * 0.7f + 0.7f
We want to squeeze the arrow when it is on its low at the resting point. The fminf function ensures that the value is at most 0. The rest is similar to the offset.
Let's add this to the plot:
When looking at the graph you can see that the bumps in the squeeze happen when the offset is at its lowest point and is running flat. So while the offset makes the arrow rest, the squeeze will scale the arrow, which looks as if the arrow is squashed due to its momentum.
To emphasize the movement, the stretch factor is added so that when the arrow is moving to its high point, it is stretched:
stretch = fmaxf(0.0f, bounce - 0.3f) * 0.5f + 0.8f
The stretch is similar to the offset as well, but the arrow is stretched when it is moving to its high point.
Let's add this to the plot, too:
We can see here, that the stretching goes along with the offset function and amplifies its effect.
For the untrained eye, the readability of this code is not great. However, when seeing the formulas on a graph, animators recognize that this is very similar to how animation tools display animation curves. It's essentially the same underlaying principle.
The readability could be somewhat improved by replacing the magic numbers with well named constant. Finding such names is however difficult, as it is hard to describe the individual effects in a single word and thus, in my experience, makes the code not really so much easier to read but certainly more difficult to change when needed. When coming up with the calculations and values, there is a lot of trial and error involved to get things right and when the variables are in a different scope, I find it difficult to understand the effect of a change.
That said, there are also other ways to animate the arrow. For example, the animation could be done in a 3D modeling tool and exported and played back. As said, animators use similar curves. The advantage of using an animation software is of course, that the hand tweaking of the values is much more convenient and the results quickly look better, especially since it is done by actual artists.
Another approach can be to use premade tweening functions and using the formulas as a starting point for the desired effect. A lot of coders prefer tweens since they provide simple premade building blocks to create such animations. If you look inside tweening library functions, you will find that the code looks very similar to the code above. The abstraction into dedicated functions makes it just more approachable for everyone.
The biggest advantage of using code is, that it is possible to adapt the function to other parameters, like the distance of the arrow bounce could depend on whether the tower can be placed at the current location or not. The downside is that artists can't change the animation unless certain values are exposed to them and even then, modifications offer only very limited control.
It is important to note that all approaches are valid here. Personally, I like to implement such effects via code because it allows animating the parameters based on the game state, but similar effects can be achieved with a tweening library or when loading an animation, albeit with less control over the animation itself.
In bigger teams, using animations is typically the way to go, because it is easier to understand and modify for artists and designers. At work, I actually often replace existing tweens in code with animation clips when I work together with artists.
In any case, I would still recommend to anyone to learn doing some basic animations via code to understand how it works and what is possible. Both, tweening and model animations, utilize the same principles, hence I believe that learning how to animate via code is therefore a good foundation, regardless which approach you will use in the end.
In the next parts, this will become more clear, as I will tweak the behavior of the tower when the building is moved, which entirely depends on user inputs.
However, in general it also depends a lot on personal preference. I don't think it makes sense to force oneself to learn animating this way. With this implementation I only wanted to show what can be done with fairly few lines of code and in case you like to learn more about this topic, you can start from here by modifying the values and see what happens.
Conclusion
In this part, the building placement UX works now on touch screens. There is an indicator on the building that can be moved around and the player can confirm the placement with the OK button.
In the next part, I will improve the movement of the tower when dragged around, since it currently snaps to the grid and doesn't feel smooth at all.
I hope you enjoyed this part and see you in the next one!