Simple tower defense tutorial, part 6: Refactoring
I believe some readers may have eagerly awaited this step in the tutorial: Refactoring. Our single main file has grown or maybe better said sprouted into a 1500 line long messy flowerbed of code: Lot's of nice things but also unorganized and no longer easy to understand.
So this part is mostly about splitting the code into multiple files and explaining the thoughts behind it. There won't be any game features added in this part, instead this part will also give some background information about how the C compiler works.
Splitting up the code
There are many opinions on how and when to split functions into files. In this case, I would like to keep the number of files a bit smaller, as the web interface showing the files might become a bit more difficult to follow the changes.
In general, what is more important than the number of lines in a code file is how entangled the cross references within the code are. So our task is to identify code parts that belong together and have few other dependencies and move them into distinct files.
In our case, it should be fairly simple to split the code: We have a some parts that are quite isolated: Enemies, towers, projectiles and particles.
Backgrounds
I will try to keep this short and cut some corners here and there, but I think it's important to understand a few things about how the C compiler works:
When working with multiple files in C, it's helpful to understand how the compiler handles the files. This makes it easier to understand error messages better, so I want to elaborate on this part a bit more, even if this is much more theoretical.
We'll begin with looking at the commandline arguments to build the project. Using GCC, it looks roughly like this:
gcc -o td-tut-32-main td-tut-32-main.c preferred_size.c -lraylib -Iraylib/src
Let's take this apart:
- gcc gets the instruction to output a file called "td-tut-32-main", specified via -o
- The two files called "td-tut-32-main.c" "preferred_size.c" are provided as input source files. This is our program code.
- -lraylib tells the compiler to link against the raylib library. This library provides the functions we use to draw graphics and handle input and window handling.
- -Iraylib/src tells the compiler where to find the raylib header files. When we include raylib.h in our code, the compiler needs to know where to find this file. Any file that is included with #include is searched in the directories specified with -I in the provided order.
What happens upon execution of this command is that the compiler takes the two .c files and compiles them into so called object files. If there's a syntax error in the code or a header that can't be found, the compiler will throw an error in this step.
There are a few steps in between, but in the last step, a linker takes these results to produce the final executable. In the example above, the call to gcc executes the linking step as well, but it's possible to split the compilation and linking steps into two separate calls. This is usually done to avoid recompiling all files when only one file has changed.
To compare this with the makefiles provided by raylib, the coresponding line to the command above would look like this:
$(CC) -o $(PROJECT_NAME)$(EXT) $(OBJS) $(CFLAGS) $(INCLUDE_PATHS) $(LDFLAGS) $(LDLIBS) -D$(PLATFORM)
This looks a little intimidating at first, but let's break it down:
The $(...) elements are variables that are defined in the makefile. The $(CC) variable contains the compiler command, which is usually gcc. The $(PROJECT_NAME) variable contains the name of the project and $(EXT) is the file extension of the executable. The $(CFLAGS) variable contains the compiler flags that are used to compile the code. The $(INCLUDE_PATHS) variable contains the -I flags that tell the compiler where to find the header files. The $(LDFLAGS) variable contains the linker flags and the $(LDLIBS) The -D$(PLATFORM) flag is used to define a preprocessor macro that is used in the code. For raylib, this is something like PLATFORM_DESKTOP, which is used to include the correct platform specific code.
The $(OBJS) variable contains the c file names and is often just looking like this:
OBJS = main.c file1.c file2.c
As said, the compiler compiles each of these files into an object file first. In this step, the source code is checked for syntax errors and is compiled into a binary representation that the later steps can work with. It is not yet in any executable form.
During this step, the compiler takes the header files and includes them in the source code. Naturally, the compiler doesn't know what the functions in the other files are doing, but it registers the functions that the input file is implementing; which leads to this small detail: you probably have noticed that a function can have a "static" in front of its declaration.
It looks like this:
1 static void my_function() { ... }
2 void my_other_function() { ...
The static keyword tells the compiler that this function is only called from within this file. In an object oriented language, you'd probably say that this function is a private method. Variables can also be declared static, which makes them also only visible in this file, or, if they are declared in a function, only within the scope of that function.
When the compiler is compiling the source code, static functions are a bit special: The compiler can do some optimizations based on the knowledge that this function (or variable) is only used within the scope of this file. It is difficult to put a number on how effective this kind of optimization is, but in certain scenarios, it can be significant while in most cases, it doesn't have much of an effect (it should be mentioned that static variables within function scopes are quite different and can have a negative impact on performance, depending on various factors).
- Limiting the scope of functions or variables is usually a good trait, as it reduces the potential dependencies to be considered when reading the code. The potential performance gain could be seen as a side effect.
- If a program is full of performance degrading code, implementing a single optimization like this will result in a significant improvement. For example, if some code runs 10 seconds and a micro optimization like this one reduces its duration by 0.01ms, it is hardly considered to be a worthwhile improvement. Let's say now, we take the code and heavily optimize its runtime so it runs in total for only 0.1ms, a 0.01ms reduction represents suddenly a significant improvement.
Without the static keyword, the function could be also called from other files. In any case, the right signature must be used in order to be able to call a function. What is a signature?
Let's look at this function declaration:
void my_other_function();
It tells the compiler that there is a function with the name "my_other_function", somewhere. Somewhere means, it could be in another object file or it could be part of a library (like raylib). But the signature of a function is not just its name, its also its return type and function arguments. In this case, it's a function that returns nothing and takes no arguments. All this information forms the signature that is used to identify the function so it can be called.
Connecting function calls to the actual function implementation is the job of the linker. During the linking phase, the linker resolves the symbols into actual memory addresses that the program can call. If the linker can't find a function, it will throw an error, which could look like this:
undefined reference to `my_other_function'
This means that the linker couldn't find the function and therefore couldn't resolve the call to this function. It can also happen, that the linker discovers that there are multiple functions with the same name. Since there is no namespace in C, it's up to the programmer to make sure that the function signatures are unique.
When multiple libraries are used, it can happen that two libraries provide a function with the same name and the same signature. This is called a "symbol collision" and the linker will throw an error in this case.
Notice that I say "signature" and not "name". The signature is the function name and the argument types it takes. While I would not recommend to have two functions with the same name in a project, it's possible to have two functions with the same name:
1 #include <stdlib.h>
2 #include <stdio.h>
3
4 void Bar();
5
6 void Foo()
7 {
8 printf("Foo of unit_1\n");
9 }
10
11 int main()
12 {
13 Foo();
14 Bar();
15 return 0;
16 }
1 #include <stdlib.h>
2 #include <stdio.h>
3
4 void Foo(const char *str) {
5 printf("Foo: %s of unit 2\n", str);
6 }
7
8 void Bar() {
9 Foo("Bar");
10 }
The compiler is also throwing a warning, because in general, this is typically a bad idea:
wasm-ld: warning: function signature mismatch: Foo
This is a warning that the linker throws when it discovers that there are two functions with the same name but different signatures. The code however runs just fine. But don't do this.
So enough of the theory, let's split up the code - but I won't do all everything in one go. The first step is to extract the struct and function declarations into the main header:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Declarations
7
8 Level levels[] = {
9 [0] = {
10 .state = LEVEL_STATE_BUILDING,
11 .initialGold = 10,
12 .waves[0] = {
13 .enemyType = ENEMY_TYPE_MINION,
14 .wave = 0,
15 .count = 10,
16 .interval = 1.0f,
17 .delay = 1.0f,
18 .spawnPosition = {0, 6},
19 },
20 .waves[1] = {
21 .enemyType = ENEMY_TYPE_MINION,
22 .wave = 1,
23 .count = 20,
24 .interval = 0.5f,
25 .delay = 1.0f,
26 .spawnPosition = {0, 6},
27 },
28 .waves[2] = {
29 .enemyType = ENEMY_TYPE_MINION,
30 .wave = 2,
31 .count = 30,
32 .interval = 0.25f,
33 .delay = 1.0f,
34 .spawnPosition = {0, 6},
35 }
36 },
37 };
38
39 Level *currentLevel = levels;
40
41 Particle particles[PARTICLE_MAX_COUNT];
42 int particleCount = 0;
43
44 //# Particle system
45
46 void ParticleInit()
47 {
48 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
49 {
50 particles[i] = (Particle){0};
51 }
52 particleCount = 0;
53 }
54
55 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
56 {
57 if (particleCount >= PARTICLE_MAX_COUNT)
58 {
59 return;
60 }
61
62 int index = -1;
63 for (int i = 0; i < particleCount; i++)
64 {
65 if (particles[i].particleType == PARTICLE_TYPE_NONE)
66 {
67 index = i;
68 break;
69 }
70 }
71
72 if (index == -1)
73 {
74 index = particleCount++;
75 }
76
77 Particle *particle = &particles[index];
78 particle->particleType = particleType;
79 particle->spawnTime = gameTime.time;
80 particle->lifetime = lifetime;
81 particle->position = position;
82 particle->velocity = velocity;
83 }
84
85 void ParticleUpdate()
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
97 if (particle->lifetime > age)
98 {
99 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
100 }
101 else {
102 particle->particleType = PARTICLE_TYPE_NONE;
103 }
104 }
105 }
106
107 void DrawExplosionParticle(Particle *particle, float transition)
108 {
109 float size = 1.2f * (1.0f - transition);
110 Color startColor = WHITE;
111 Color endColor = RED;
112 Color color = ColorLerp(startColor, endColor, transition);
113 DrawCube(particle->position, size, size, size, color);
114 }
115
116 void ParticleDraw()
117 {
118 for (int i = 0; i < particleCount; i++)
119 {
120 Particle particle = particles[i];
121 if (particle.particleType == PARTICLE_TYPE_NONE)
122 {
123 continue;
124 }
125
126 float age = gameTime.time - particle.spawnTime;
127 float transition = age / particle.lifetime;
128 switch (particle.particleType)
129 {
130 case PARTICLE_TYPE_EXPLOSION:
131 DrawExplosionParticle(&particle, transition);
132 break;
133 default:
134 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
135 break;
136 }
137 }
138 }
139
140 //# Pathfinding map
141
142 // The queue is a simple array of nodes, we add nodes to the end and remove
143 // nodes from the front. We keep the array around to avoid unnecessary allocations
144 static PathfindingNode *pathfindingNodeQueue = 0;
145 static int pathfindingNodeQueueCount = 0;
146 static int pathfindingNodeQueueCapacity = 0;
147
148 // The pathfinding map stores the distances from the castle to each cell in the map.
149 PathfindingMap pathfindingMap = {0};
150
151 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
152 {
153 // transforming between map space and world space allows us to adapt
154 // position and scale of the map without changing the pathfinding data
155 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
156 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
157 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
158 pathfindingMap.width = width;
159 pathfindingMap.height = height;
160 pathfindingMap.scale = scale;
161 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
162 for (int i = 0; i < width * height; i++)
163 {
164 pathfindingMap.distances[i] = -1.0f;
165 }
166
167 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
168 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
169 }
170
171 float PathFindingGetDistance(int mapX, int mapY)
172 {
173 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
174 {
175 // when outside the map, we return the manhattan distance to the castle (0,0)
176 return fabsf((float)mapX) + fabsf((float)mapY);
177 }
178
179 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
180 }
181
182 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
183 {
184 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
185 {
186 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
187 // we use MemAlloc/MemRealloc to allocate memory for the queue
188 // I am not entirely sure if MemRealloc allows passing a null pointer
189 // so we check if the pointer is null and use MemAlloc in that case
190 if (pathfindingNodeQueue == 0)
191 {
192 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
193 }
194 else
195 {
196 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
197 }
198 }
199
200 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
201 node->x = x;
202 node->y = y;
203 node->fromX = fromX;
204 node->fromY = fromY;
205 node->distance = distance;
206 }
207
208 PathfindingNode *PathFindingNodePop()
209 {
210 if (pathfindingNodeQueueCount == 0)
211 {
212 return 0;
213 }
214 // we return the first node in the queue; we want to return a pointer to the node
215 // so we can return 0 if the queue is empty.
216 // We should _not_ return a pointer to the element in the list, because the list
217 // may be reallocated and the pointer would become invalid. Or the
218 // popped element is overwritten by the next push operation.
219 // Using static here means that the variable is permanently allocated.
220 static PathfindingNode node;
221 node = pathfindingNodeQueue[0];
222 // we shift all nodes one position to the front
223 for (int i = 1; i < pathfindingNodeQueueCount; i++)
224 {
225 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
226 }
227 --pathfindingNodeQueueCount;
228 return &node;
229 }
230
231 // transform a world position to a map position in the array;
232 // returns true if the position is inside the map
233 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
234 {
235 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
236 *mapX = (int16_t)mapPosition.x;
237 *mapY = (int16_t)mapPosition.z;
238 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
239 }
240
241 void PathFindingMapUpdate()
242 {
243 const int castleX = 0, castleY = 0;
244 int16_t castleMapX, castleMapY;
245 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
246 {
247 return;
248 }
249 int width = pathfindingMap.width, height = pathfindingMap.height;
250
251 // reset the distances to -1
252 for (int i = 0; i < width * height; i++)
253 {
254 pathfindingMap.distances[i] = -1.0f;
255 }
256 // reset the tower indices
257 for (int i = 0; i < width * height; i++)
258 {
259 pathfindingMap.towerIndex[i] = -1;
260 }
261 // reset the delta src
262 for (int i = 0; i < width * height; i++)
263 {
264 pathfindingMap.deltaSrc[i].x = 0;
265 pathfindingMap.deltaSrc[i].y = 0;
266 }
267
268 for (int i = 0; i < towerCount; i++)
269 {
270 Tower *tower = &towers[i];
271 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
272 {
273 continue;
274 }
275 int16_t mapX, mapY;
276 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
277 // this would not work correctly and needs to be refined to allow towers covering multiple cells
278 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
279 // one cell. For now.
280 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
281 {
282 continue;
283 }
284 int index = mapY * width + mapX;
285 pathfindingMap.towerIndex[index] = i;
286 }
287
288 // we start at the castle and add the castle to the queue
289 pathfindingMap.maxDistance = 0.0f;
290 pathfindingNodeQueueCount = 0;
291 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
292 PathfindingNode *node = 0;
293 while ((node = PathFindingNodePop()))
294 {
295 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
296 {
297 continue;
298 }
299 int index = node->y * width + node->x;
300 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
301 {
302 continue;
303 }
304
305 int deltaX = node->x - node->fromX;
306 int deltaY = node->y - node->fromY;
307 // even if the cell is blocked by a tower, we still may want to store the direction
308 // (though this might not be needed, IDK right now)
309 pathfindingMap.deltaSrc[index].x = (char) deltaX;
310 pathfindingMap.deltaSrc[index].y = (char) deltaY;
311
312 // we skip nodes that are blocked by towers
313 if (pathfindingMap.towerIndex[index] >= 0)
314 {
315 node->distance += 8.0f;
316 }
317 pathfindingMap.distances[index] = node->distance;
318 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
319 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
320 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
321 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
322 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
323 }
324 }
325
326 void PathFindingMapDraw()
327 {
328 float cellSize = pathfindingMap.scale * 0.9f;
329 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
330 for (int x = 0; x < pathfindingMap.width; x++)
331 {
332 for (int y = 0; y < pathfindingMap.height; y++)
333 {
334 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
335 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
336 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
337 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
338 // animate the distance "wave" to show how the pathfinding algorithm expands
339 // from the castle
340 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
341 {
342 color = BLACK;
343 }
344 DrawCube(position, cellSize, 0.1f, cellSize, color);
345 }
346 }
347 }
348
349 Vector2 PathFindingGetGradient(Vector3 world)
350 {
351 int16_t mapX, mapY;
352 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
353 {
354 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
355 return (Vector2){(float)-delta.x, (float)-delta.y};
356 }
357 // fallback to a simple gradient calculation
358 float n = PathFindingGetDistance(mapX, mapY - 1);
359 float s = PathFindingGetDistance(mapX, mapY + 1);
360 float w = PathFindingGetDistance(mapX - 1, mapY);
361 float e = PathFindingGetDistance(mapX + 1, mapY);
362 return (Vector2){w - e + 0.25f, n - s + 0.125f};
363 }
364
365 //# Enemies
366 void EnemyInit()
367 {
368 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
369 {
370 enemies[i] = (Enemy){0};
371 }
372 enemyCount = 0;
373 }
374
375 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
376 {
377 return enemyClassConfigs[enemy->enemyType].speed;
378 }
379
380 float EnemyGetMaxHealth(Enemy *enemy)
381 {
382 return enemyClassConfigs[enemy->enemyType].health;
383 }
384
385 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
386 {
387 int16_t castleX = 0;
388 int16_t castleY = 0;
389 int16_t dx = castleX - currentX;
390 int16_t dy = castleY - currentY;
391 if (dx == 0 && dy == 0)
392 {
393 *nextX = currentX;
394 *nextY = currentY;
395 return 1;
396 }
397 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
398
399 if (gradient.x == 0 && gradient.y == 0)
400 {
401 *nextX = currentX;
402 *nextY = currentY;
403 return 1;
404 }
405
406 if (fabsf(gradient.x) > fabsf(gradient.y))
407 {
408 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
409 *nextY = currentY;
410 return 0;
411 }
412 *nextX = currentX;
413 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
414 return 0;
415 }
416
417
418 // this function predicts the movement of the unit for the next deltaT seconds
419 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
420 {
421 const float pointReachedDistance = 0.25f;
422 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
423 const float maxSimStepTime = 0.015625f;
424
425 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
426 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
427 int16_t nextX = enemy->nextX;
428 int16_t nextY = enemy->nextY;
429 Vector2 position = enemy->simPosition;
430 int passedCount = 0;
431 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
432 {
433 float stepTime = fminf(deltaT - t, maxSimStepTime);
434 Vector2 target = (Vector2){nextX, nextY};
435 float speed = Vector2Length(*velocity);
436 // draw the target position for debugging
437 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
438 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
439 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
440 {
441 // we reached the target position, let's move to the next waypoint
442 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
443 target = (Vector2){nextX, nextY};
444 // track how many waypoints we passed
445 passedCount++;
446 }
447
448 // acceleration towards the target
449 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
450 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
451 *velocity = Vector2Add(*velocity, acceleration);
452
453 // limit the speed to the maximum speed
454 if (speed > maxSpeed)
455 {
456 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
457 }
458
459 // move the enemy
460 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
461 }
462
463 if (waypointPassedCount)
464 {
465 (*waypointPassedCount) = passedCount;
466 }
467
468 return position;
469 }
470
471 void EnemyDraw()
472 {
473 for (int i = 0; i < enemyCount; i++)
474 {
475 Enemy enemy = enemies[i];
476 if (enemy.enemyType == ENEMY_TYPE_NONE)
477 {
478 continue;
479 }
480
481 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
482
483 if (enemy.movePathCount > 0)
484 {
485 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
486 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
487 }
488 for (int j = 1; j < enemy.movePathCount; j++)
489 {
490 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
491 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
492 DrawLine3D(p, q, GREEN);
493 }
494
495 switch (enemy.enemyType)
496 {
497 case ENEMY_TYPE_MINION:
498 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
499 break;
500 }
501 }
502 }
503
504 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
505 {
506 // damage the tower
507 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
508 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
509 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
510 float explosionRange2 = explosionRange * explosionRange;
511 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
512 // explode the enemy
513 if (tower->damage >= TowerGetMaxHealth(tower))
514 {
515 tower->towerType = TOWER_TYPE_NONE;
516 }
517
518 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
519 explosionSource,
520 (Vector3){0, 0.1f, 0}, 1.0f);
521
522 enemy->enemyType = ENEMY_TYPE_NONE;
523
524 // push back enemies & dealing damage
525 for (int i = 0; i < enemyCount; i++)
526 {
527 Enemy *other = &enemies[i];
528 if (other->enemyType == ENEMY_TYPE_NONE)
529 {
530 continue;
531 }
532 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
533 if (distanceSqr > 0 && distanceSqr < explosionRange2)
534 {
535 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
536 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
537 EnemyAddDamage(other, explosionDamge);
538 }
539 }
540 }
541
542 void EnemyUpdate()
543 {
544 const float castleX = 0;
545 const float castleY = 0;
546 const float maxPathDistance2 = 0.25f * 0.25f;
547
548 for (int i = 0; i < enemyCount; i++)
549 {
550 Enemy *enemy = &enemies[i];
551 if (enemy->enemyType == ENEMY_TYPE_NONE)
552 {
553 continue;
554 }
555
556 int waypointPassedCount = 0;
557 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
558 enemy->startMovingTime = gameTime.time;
559 // track path of unit
560 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
561 {
562 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
563 {
564 enemy->movePath[j] = enemy->movePath[j - 1];
565 }
566 enemy->movePath[0] = enemy->simPosition;
567 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
568 {
569 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
570 }
571 }
572
573 if (waypointPassedCount > 0)
574 {
575 enemy->currentX = enemy->nextX;
576 enemy->currentY = enemy->nextY;
577 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
578 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
579 {
580 // enemy reached the castle; remove it
581 enemy->enemyType = ENEMY_TYPE_NONE;
582 continue;
583 }
584 }
585 }
586
587 // handle collisions between enemies
588 for (int i = 0; i < enemyCount - 1; i++)
589 {
590 Enemy *enemyA = &enemies[i];
591 if (enemyA->enemyType == ENEMY_TYPE_NONE)
592 {
593 continue;
594 }
595 for (int j = i + 1; j < enemyCount; j++)
596 {
597 Enemy *enemyB = &enemies[j];
598 if (enemyB->enemyType == ENEMY_TYPE_NONE)
599 {
600 continue;
601 }
602 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
603 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
604 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
605 float radiusSum = radiusA + radiusB;
606 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
607 {
608 // collision
609 float distance = sqrtf(distanceSqr);
610 float overlap = radiusSum - distance;
611 // move the enemies apart, but softly; if we have a clog of enemies,
612 // moving them perfectly apart can cause them to jitter
613 float positionCorrection = overlap / 5.0f;
614 Vector2 direction = (Vector2){
615 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
616 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
617 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
618 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
619 }
620 }
621 }
622
623 // handle collisions between enemies and towers
624 for (int i = 0; i < enemyCount; i++)
625 {
626 Enemy *enemy = &enemies[i];
627 if (enemy->enemyType == ENEMY_TYPE_NONE)
628 {
629 continue;
630 }
631 enemy->contactTime -= gameTime.deltaTime;
632 if (enemy->contactTime < 0.0f)
633 {
634 enemy->contactTime = 0.0f;
635 }
636
637 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
638 // linear search over towers; could be optimized by using path finding tower map,
639 // but for now, we keep it simple
640 for (int j = 0; j < towerCount; j++)
641 {
642 Tower *tower = &towers[j];
643 if (tower->towerType == TOWER_TYPE_NONE)
644 {
645 continue;
646 }
647 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
648 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
649 if (distanceSqr > combinedRadius * combinedRadius)
650 {
651 continue;
652 }
653 // potential collision; square / circle intersection
654 float dx = tower->x - enemy->simPosition.x;
655 float dy = tower->y - enemy->simPosition.y;
656 float absDx = fabsf(dx);
657 float absDy = fabsf(dy);
658 Vector3 contactPoint = {0};
659 if (absDx <= 0.5f && absDx <= absDy) {
660 // vertical collision; push the enemy out horizontally
661 float overlap = enemyRadius + 0.5f - absDy;
662 if (overlap < 0.0f)
663 {
664 continue;
665 }
666 float direction = dy > 0.0f ? -1.0f : 1.0f;
667 enemy->simPosition.y += direction * overlap;
668 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
669 }
670 else if (absDy <= 0.5f && absDy <= absDx)
671 {
672 // horizontal collision; push the enemy out vertically
673 float overlap = enemyRadius + 0.5f - absDx;
674 if (overlap < 0.0f)
675 {
676 continue;
677 }
678 float direction = dx > 0.0f ? -1.0f : 1.0f;
679 enemy->simPosition.x += direction * overlap;
680 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
681 }
682 else
683 {
684 // possible collision with a corner
685 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
686 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
687 float cornerX = tower->x + cornerDX;
688 float cornerY = tower->y + cornerDY;
689 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
690 if (cornerDistanceSqr > enemyRadius * enemyRadius)
691 {
692 continue;
693 }
694 // push the enemy out along the diagonal
695 float cornerDistance = sqrtf(cornerDistanceSqr);
696 float overlap = enemyRadius - cornerDistance;
697 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
698 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
699 enemy->simPosition.x -= directionX * overlap;
700 enemy->simPosition.y -= directionY * overlap;
701 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
702 }
703
704 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
705 {
706 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
707 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
708 {
709 EnemyTriggerExplode(enemy, tower, contactPoint);
710 }
711 }
712 }
713 }
714 }
715
716 EnemyId EnemyGetId(Enemy *enemy)
717 {
718 return (EnemyId){enemy - enemies, enemy->generation};
719 }
720
721 Enemy *EnemyTryResolve(EnemyId enemyId)
722 {
723 if (enemyId.index >= ENEMY_MAX_COUNT)
724 {
725 return 0;
726 }
727 Enemy *enemy = &enemies[enemyId.index];
728 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
729 {
730 return 0;
731 }
732 return enemy;
733 }
734
735 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
736 {
737 Enemy *spawn = 0;
738 for (int i = 0; i < enemyCount; i++)
739 {
740 Enemy *enemy = &enemies[i];
741 if (enemy->enemyType == ENEMY_TYPE_NONE)
742 {
743 spawn = enemy;
744 break;
745 }
746 }
747
748 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
749 {
750 spawn = &enemies[enemyCount++];
751 }
752
753 if (spawn)
754 {
755 spawn->currentX = currentX;
756 spawn->currentY = currentY;
757 spawn->nextX = currentX;
758 spawn->nextY = currentY;
759 spawn->simPosition = (Vector2){currentX, currentY};
760 spawn->simVelocity = (Vector2){0, 0};
761 spawn->enemyType = enemyType;
762 spawn->startMovingTime = gameTime.time;
763 spawn->damage = 0.0f;
764 spawn->futureDamage = 0.0f;
765 spawn->generation++;
766 spawn->movePathCount = 0;
767 }
768
769 return spawn;
770 }
771
772 int EnemyAddDamage(Enemy *enemy, float damage)
773 {
774 enemy->damage += damage;
775 if (enemy->damage >= EnemyGetMaxHealth(enemy))
776 {
777 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
778 enemy->enemyType = ENEMY_TYPE_NONE;
779 return 1;
780 }
781
782 return 0;
783 }
784
785 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
786 {
787 int16_t castleX = 0;
788 int16_t castleY = 0;
789 Enemy* closest = 0;
790 int16_t closestDistance = 0;
791 float range2 = range * range;
792 for (int i = 0; i < enemyCount; i++)
793 {
794 Enemy* enemy = &enemies[i];
795 if (enemy->enemyType == ENEMY_TYPE_NONE)
796 {
797 continue;
798 }
799 float maxHealth = EnemyGetMaxHealth(enemy);
800 if (enemy->futureDamage >= maxHealth)
801 {
802 // ignore enemies that will die soon
803 continue;
804 }
805 int16_t dx = castleX - enemy->currentX;
806 int16_t dy = castleY - enemy->currentY;
807 int16_t distance = abs(dx) + abs(dy);
808 if (!closest || distance < closestDistance)
809 {
810 float tdx = towerX - enemy->currentX;
811 float tdy = towerY - enemy->currentY;
812 float tdistance2 = tdx * tdx + tdy * tdy;
813 if (tdistance2 <= range2)
814 {
815 closest = enemy;
816 closestDistance = distance;
817 }
818 }
819 }
820 return closest;
821 }
822
823 int EnemyCount()
824 {
825 int count = 0;
826 for (int i = 0; i < enemyCount; i++)
827 {
828 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
829 {
830 count++;
831 }
832 }
833 return count;
834 }
835
836 //# Projectiles
837 Projectile projectiles[PROJECTILE_MAX_COUNT];
838 int projectileCount = 0;
839
840 void ProjectileInit()
841 {
842 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
843 {
844 projectiles[i] = (Projectile){0};
845 }
846 }
847
848 void ProjectileDraw()
849 {
850 for (int i = 0; i < projectileCount; i++)
851 {
852 Projectile projectile = projectiles[i];
853 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
854 {
855 continue;
856 }
857 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
858 if (transition >= 1.0f)
859 {
860 continue;
861 }
862 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
863 float x = position.x;
864 float y = position.y;
865 float dx = projectile.directionNormal.x;
866 float dy = projectile.directionNormal.y;
867 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
868 {
869 x -= dx * 0.1f;
870 y -= dy * 0.1f;
871 float size = 0.1f * d;
872 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
873 }
874 }
875 }
876
877 void ProjectileUpdate()
878 {
879 for (int i = 0; i < projectileCount; i++)
880 {
881 Projectile *projectile = &projectiles[i];
882 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
883 {
884 continue;
885 }
886 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
887 if (transition >= 1.0f)
888 {
889 projectile->projectileType = PROJECTILE_TYPE_NONE;
890 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
891 if (enemy)
892 {
893 EnemyAddDamage(enemy, projectile->damage);
894 }
895 continue;
896 }
897 }
898 }
899
900 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
901 {
902 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
903 {
904 Projectile *projectile = &projectiles[i];
905 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
906 {
907 projectile->projectileType = projectileType;
908 projectile->shootTime = gameTime.time;
909 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
910 projectile->damage = damage;
911 projectile->position = position;
912 projectile->target = target;
913 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
914 projectile->targetEnemy = EnemyGetId(enemy);
915 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
916 return projectile;
917 }
918 }
919 return 0;
920 }
921
922 //# Towers
923
924 void TowerInit()
925 {
926 for (int i = 0; i < TOWER_MAX_COUNT; i++)
927 {
928 towers[i] = (Tower){0};
929 }
930 towerCount = 0;
931 }
932
933 Tower *TowerGetAt(int16_t x, int16_t y)
934 {
935 for (int i = 0; i < towerCount; i++)
936 {
937 if (towers[i].x == x && towers[i].y == y)
938 {
939 return &towers[i];
940 }
941 }
942 return 0;
943 }
944
945 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
946 {
947 if (towerCount >= TOWER_MAX_COUNT)
948 {
949 return 0;
950 }
951
952 Tower *tower = TowerGetAt(x, y);
953 if (tower)
954 {
955 return 0;
956 }
957
958 tower = &towers[towerCount++];
959 tower->x = x;
960 tower->y = y;
961 tower->towerType = towerType;
962 tower->cooldown = 0.0f;
963 tower->damage = 0.0f;
964 return tower;
965 }
966
967 Tower *GetTowerByType(uint8_t towerType)
968 {
969 for (int i = 0; i < towerCount; i++)
970 {
971 if (towers[i].towerType == towerType)
972 {
973 return &towers[i];
974 }
975 }
976 return 0;
977 }
978
979 int GetTowerCosts(uint8_t towerType)
980 {
981 switch (towerType)
982 {
983 case TOWER_TYPE_BASE:
984 return 0;
985 case TOWER_TYPE_GUN:
986 return 6;
987 case TOWER_TYPE_WALL:
988 return 2;
989 }
990 return 0;
991 }
992
993 float TowerGetMaxHealth(Tower *tower)
994 {
995 switch (tower->towerType)
996 {
997 case TOWER_TYPE_BASE:
998 return 10.0f;
999 case TOWER_TYPE_GUN:
1000 return 3.0f;
1001 case TOWER_TYPE_WALL:
1002 return 5.0f;
1003 }
1004 return 0.0f;
1005 }
1006
1007 void TowerDraw()
1008 {
1009 for (int i = 0; i < towerCount; i++)
1010 {
1011 Tower tower = towers[i];
1012 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
1013 switch (tower.towerType)
1014 {
1015 case TOWER_TYPE_BASE:
1016 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
1017 break;
1018 case TOWER_TYPE_GUN:
1019 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
1020 break;
1021 case TOWER_TYPE_WALL:
1022 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
1023 break;
1024 }
1025 }
1026 }
1027
1028 void TowerGunUpdate(Tower *tower)
1029 {
1030 if (tower->cooldown <= 0)
1031 {
1032 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
1033 if (enemy)
1034 {
1035 tower->cooldown = 0.125f;
1036 // shoot the enemy; determine future position of the enemy
1037 float bulletSpeed = 1.0f;
1038 float bulletDamage = 3.0f;
1039 Vector2 velocity = enemy->simVelocity;
1040 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
1041 Vector2 towerPosition = {tower->x, tower->y};
1042 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
1043 for (int i = 0; i < 8; i++) {
1044 velocity = enemy->simVelocity;
1045 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
1046 float distance = Vector2Distance(towerPosition, futurePosition);
1047 float eta2 = distance / bulletSpeed;
1048 if (fabs(eta - eta2) < 0.01f) {
1049 break;
1050 }
1051 eta = (eta2 + eta) * 0.5f;
1052 }
1053 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
1054 bulletSpeed, bulletDamage);
1055 enemy->futureDamage += bulletDamage;
1056 }
1057 }
1058 else
1059 {
1060 tower->cooldown -= gameTime.deltaTime;
1061 }
1062 }
1063
1064 void TowerUpdate()
1065 {
1066 for (int i = 0; i < towerCount; i++)
1067 {
1068 Tower *tower = &towers[i];
1069 switch (tower->towerType)
1070 {
1071 case TOWER_TYPE_GUN:
1072 TowerGunUpdate(tower);
1073 break;
1074 }
1075 }
1076 }
1077
1078 //# Game
1079
1080 void InitLevel(Level *level)
1081 {
1082 TowerInit();
1083 EnemyInit();
1084 ProjectileInit();
1085 ParticleInit();
1086 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
1087
1088 level->placementMode = 0;
1089 level->state = LEVEL_STATE_BUILDING;
1090 level->nextState = LEVEL_STATE_NONE;
1091 level->playerGold = level->initialGold;
1092
1093 Camera *camera = &level->camera;
1094 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
1095 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
1096 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
1097 camera->fovy = 45.0f;
1098 camera->projection = CAMERA_PERSPECTIVE;
1099 }
1100
1101 void DrawLevelHud(Level *level)
1102 {
1103 const char *text = TextFormat("Gold: %d", level->playerGold);
1104 Font font = GetFontDefault();
1105 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
1106 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
1107 }
1108
1109 void DrawLevelReportLostWave(Level *level)
1110 {
1111 BeginMode3D(level->camera);
1112 DrawGrid(10, 1.0f);
1113 TowerDraw();
1114 EnemyDraw();
1115 ProjectileDraw();
1116 ParticleDraw();
1117 guiState.isBlocked = 0;
1118 EndMode3D();
1119
1120 const char *text = "Wave lost";
1121 int textWidth = MeasureText(text, 20);
1122 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1123
1124 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1125 {
1126 level->nextState = LEVEL_STATE_RESET;
1127 }
1128 }
1129
1130 int HasLevelNextWave(Level *level)
1131 {
1132 for (int i = 0; i < 10; i++)
1133 {
1134 EnemyWave *wave = &level->waves[i];
1135 if (wave->wave == level->currentWave)
1136 {
1137 return 1;
1138 }
1139 }
1140 return 0;
1141 }
1142
1143 void DrawLevelReportWonWave(Level *level)
1144 {
1145 BeginMode3D(level->camera);
1146 DrawGrid(10, 1.0f);
1147 TowerDraw();
1148 EnemyDraw();
1149 ProjectileDraw();
1150 ParticleDraw();
1151 guiState.isBlocked = 0;
1152 EndMode3D();
1153
1154 const char *text = "Wave won";
1155 int textWidth = MeasureText(text, 20);
1156 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1157
1158
1159 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1160 {
1161 level->nextState = LEVEL_STATE_RESET;
1162 }
1163
1164 if (HasLevelNextWave(level))
1165 {
1166 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
1167 {
1168 level->nextState = LEVEL_STATE_BUILDING;
1169 }
1170 }
1171 else {
1172 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
1173 {
1174 level->nextState = LEVEL_STATE_WON_LEVEL;
1175 }
1176 }
1177 }
1178
1179 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
1180 {
1181 static ButtonState buttonStates[8] = {0};
1182 int cost = GetTowerCosts(towerType);
1183 const char *text = TextFormat("%s: %d", name, cost);
1184 buttonStates[towerType].isSelected = level->placementMode == towerType;
1185 buttonStates[towerType].isDisabled = level->playerGold < cost;
1186 if (Button(text, x, y, width, height, &buttonStates[towerType]))
1187 {
1188 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
1189 }
1190 }
1191
1192 void DrawLevelBuildingState(Level *level)
1193 {
1194 BeginMode3D(level->camera);
1195 DrawGrid(10, 1.0f);
1196 TowerDraw();
1197 EnemyDraw();
1198 ProjectileDraw();
1199 ParticleDraw();
1200
1201 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
1202 float planeDistance = ray.position.y / -ray.direction.y;
1203 float planeX = ray.direction.x * planeDistance + ray.position.x;
1204 float planeY = ray.direction.z * planeDistance + ray.position.z;
1205 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
1206 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
1207 if (level->placementMode && !guiState.isBlocked)
1208 {
1209 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
1210 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1211 {
1212 if (TowerTryAdd(level->placementMode, mapX, mapY))
1213 {
1214 level->playerGold -= GetTowerCosts(level->placementMode);
1215 level->placementMode = TOWER_TYPE_NONE;
1216 }
1217 }
1218 }
1219
1220 guiState.isBlocked = 0;
1221
1222 EndMode3D();
1223
1224 static ButtonState buildWallButtonState = {0};
1225 static ButtonState buildGunButtonState = {0};
1226 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
1227 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
1228
1229 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
1230 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
1231
1232 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1233 {
1234 level->nextState = LEVEL_STATE_RESET;
1235 }
1236
1237 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
1238 {
1239 level->nextState = LEVEL_STATE_BATTLE;
1240 }
1241
1242 const char *text = "Building phase";
1243 int textWidth = MeasureText(text, 20);
1244 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1245 }
1246
1247 void InitBattleStateConditions(Level *level)
1248 {
1249 level->state = LEVEL_STATE_BATTLE;
1250 level->nextState = LEVEL_STATE_NONE;
1251 level->waveEndTimer = 0.0f;
1252 for (int i = 0; i < 10; i++)
1253 {
1254 EnemyWave *wave = &level->waves[i];
1255 wave->spawned = 0;
1256 wave->timeToSpawnNext = wave->delay;
1257 }
1258 }
1259
1260 void DrawLevelBattleState(Level *level)
1261 {
1262 BeginMode3D(level->camera);
1263 DrawGrid(10, 1.0f);
1264 TowerDraw();
1265 EnemyDraw();
1266 ProjectileDraw();
1267 ParticleDraw();
1268 guiState.isBlocked = 0;
1269 EndMode3D();
1270
1271 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
1272 {
1273 level->nextState = LEVEL_STATE_RESET;
1274 }
1275
1276 int maxCount = 0;
1277 int remainingCount = 0;
1278 for (int i = 0; i < 10; i++)
1279 {
1280 EnemyWave *wave = &level->waves[i];
1281 if (wave->wave != level->currentWave)
1282 {
1283 continue;
1284 }
1285 maxCount += wave->count;
1286 remainingCount += wave->count - wave->spawned;
1287 }
1288 int aliveCount = EnemyCount();
1289 remainingCount += aliveCount;
1290
1291 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
1292 int textWidth = MeasureText(text, 20);
1293 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
1294 }
1295
1296 void DrawLevel(Level *level)
1297 {
1298 switch (level->state)
1299 {
1300 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
1301 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
1302 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
1303 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
1304 default: break;
1305 }
1306
1307 DrawLevelHud(level);
1308 }
1309
1310 void UpdateLevel(Level *level)
1311 {
1312 if (level->state == LEVEL_STATE_BATTLE)
1313 {
1314 int activeWaves = 0;
1315 for (int i = 0; i < 10; i++)
1316 {
1317 EnemyWave *wave = &level->waves[i];
1318 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
1319 {
1320 continue;
1321 }
1322 activeWaves++;
1323 wave->timeToSpawnNext -= gameTime.deltaTime;
1324 if (wave->timeToSpawnNext <= 0.0f)
1325 {
1326 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
1327 if (enemy)
1328 {
1329 wave->timeToSpawnNext = wave->interval;
1330 wave->spawned++;
1331 }
1332 }
1333 }
1334 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
1335 level->waveEndTimer += gameTime.deltaTime;
1336 if (level->waveEndTimer >= 2.0f)
1337 {
1338 level->nextState = LEVEL_STATE_LOST_WAVE;
1339 }
1340 }
1341 else if (activeWaves == 0 && EnemyCount() == 0)
1342 {
1343 level->waveEndTimer += gameTime.deltaTime;
1344 if (level->waveEndTimer >= 2.0f)
1345 {
1346 level->nextState = LEVEL_STATE_WON_WAVE;
1347 }
1348 }
1349 }
1350
1351 PathFindingMapUpdate();
1352 EnemyUpdate();
1353 TowerUpdate();
1354 ProjectileUpdate();
1355 ParticleUpdate();
1356
1357 if (level->nextState == LEVEL_STATE_RESET)
1358 {
1359 InitLevel(level);
1360 }
1361
1362 if (level->nextState == LEVEL_STATE_BATTLE)
1363 {
1364 InitBattleStateConditions(level);
1365 }
1366
1367 if (level->nextState == LEVEL_STATE_WON_WAVE)
1368 {
1369 level->currentWave++;
1370 level->state = LEVEL_STATE_WON_WAVE;
1371 }
1372
1373 if (level->nextState == LEVEL_STATE_LOST_WAVE)
1374 {
1375 level->state = LEVEL_STATE_LOST_WAVE;
1376 }
1377
1378 if (level->nextState == LEVEL_STATE_BUILDING)
1379 {
1380 level->state = LEVEL_STATE_BUILDING;
1381 }
1382
1383 if (level->nextState == LEVEL_STATE_WON_LEVEL)
1384 {
1385 // make something of this later
1386 InitLevel(level);
1387 }
1388
1389 level->nextState = LEVEL_STATE_NONE;
1390 }
1391
1392 float nextSpawnTime = 0.0f;
1393
1394 void ResetGame()
1395 {
1396 InitLevel(currentLevel);
1397 }
1398
1399 void InitGame()
1400 {
1401 TowerInit();
1402 EnemyInit();
1403 ProjectileInit();
1404 ParticleInit();
1405 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
1406
1407 currentLevel = levels;
1408 InitLevel(currentLevel);
1409 }
1410
1411 //# Immediate GUI functions
1412
1413 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
1414 {
1415 Rectangle bounds = {x, y, width, height};
1416 int isPressed = 0;
1417 int isSelected = state && state->isSelected;
1418 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
1419 {
1420 Color color = isSelected ? DARKGRAY : GRAY;
1421 DrawRectangle(x, y, width, height, color);
1422 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
1423 {
1424 isPressed = 1;
1425 }
1426 guiState.isBlocked = 1;
1427 }
1428 else
1429 {
1430 Color color = isSelected ? WHITE : LIGHTGRAY;
1431 DrawRectangle(x, y, width, height, color);
1432 }
1433 Font font = GetFontDefault();
1434 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
1435 Color textColor = state->isDisabled ? GRAY : BLACK;
1436 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
1437 return isPressed;
1438 }
1439
1440 //# Main game loop
1441
1442 void GameUpdate()
1443 {
1444 float dt = GetFrameTime();
1445 // cap maximum delta time to 0.1 seconds to prevent large time steps
1446 if (dt > 0.1f) dt = 0.1f;
1447 gameTime.time += dt;
1448 gameTime.deltaTime = dt;
1449
1450 UpdateLevel(currentLevel);
1451 }
1452
1453 int main(void)
1454 {
1455 int screenWidth, screenHeight;
1456 GetPreferredSize(&screenWidth, &screenHeight);
1457 InitWindow(screenWidth, screenHeight, "Tower defense");
1458 SetTargetFPS(30);
1459
1460 InitGame();
1461
1462 while (!WindowShouldClose())
1463 {
1464 if (IsPaused()) {
1465 // canvas is not visible in browser - do nothing
1466 continue;
1467 }
1468
1469 BeginDrawing();
1470 ClearBackground(DARKBLUE);
1471
1472 GameUpdate();
1473 DrawLevel(currentLevel);
1474
1475 EndDrawing();
1476 }
1477
1478 CloseWindow();
1479
1480 return 0;
1481 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 } Particle;
28
29 #define TOWER_MAX_COUNT 400
30 #define TOWER_TYPE_NONE 0
31 #define TOWER_TYPE_BASE 1
32 #define TOWER_TYPE_GUN 2
33 #define TOWER_TYPE_WALL 3
34
35 typedef struct Tower
36 {
37 int16_t x, y;
38 uint8_t towerType;
39 float cooldown;
40 float damage;
41 } Tower;
42
43 typedef struct GameTime
44 {
45 float time;
46 float deltaTime;
47 } GameTime;
48
49 typedef struct ButtonState {
50 char isSelected;
51 char isDisabled;
52 } ButtonState;
53
54 typedef struct GUIState {
55 int isBlocked;
56 } GUIState;
57
58 GUIState guiState = {0};
59 GameTime gameTime = {0};
60 Tower towers[TOWER_MAX_COUNT];
61 int towerCount = 0;
62
63 typedef enum LevelState
64 {
65 LEVEL_STATE_NONE,
66 LEVEL_STATE_BUILDING,
67 LEVEL_STATE_BATTLE,
68 LEVEL_STATE_WON_WAVE,
69 LEVEL_STATE_LOST_WAVE,
70 LEVEL_STATE_WON_LEVEL,
71 LEVEL_STATE_RESET,
72 } LevelState;
73
74 typedef struct EnemyWave {
75 uint8_t enemyType;
76 uint8_t wave;
77 uint16_t count;
78 float interval;
79 float delay;
80 Vector2 spawnPosition;
81
82 uint16_t spawned;
83 float timeToSpawnNext;
84 } EnemyWave;
85
86 typedef struct Level
87 {
88 LevelState state;
89 LevelState nextState;
90 Camera3D camera;
91 int placementMode;
92
93 int initialGold;
94 int playerGold;
95
96 EnemyWave waves[10];
97 int currentWave;
98 float waveEndTimer;
99 } Level;
100
101 typedef struct DeltaSrc
102 {
103 char x, y;
104 } DeltaSrc;
105
106 typedef struct PathfindingMap
107 {
108 int width, height;
109 float scale;
110 float *distances;
111 long *towerIndex;
112 DeltaSrc *deltaSrc;
113 float maxDistance;
114 Matrix toMapSpace;
115 Matrix toWorldSpace;
116 } PathfindingMap;
117
118 // when we execute the pathfinding algorithm, we need to store the active nodes
119 // in a queue. Each node has a position, a distance from the start, and the
120 // position of the node that we came from.
121 typedef struct PathfindingNode
122 {
123 int16_t x, y, fromX, fromY;
124 float distance;
125 } PathfindingNode;
126
127 typedef struct EnemyId
128 {
129 uint16_t index;
130 uint16_t generation;
131 } EnemyId;
132
133 typedef struct EnemyClassConfig
134 {
135 float speed;
136 float health;
137 float radius;
138 float maxAcceleration;
139 float requiredContactTime;
140 float explosionDamage;
141 float explosionRange;
142 float explosionPushbackPower;
143 int goldValue;
144 } EnemyClassConfig;
145
146 typedef struct Enemy
147 {
148 int16_t currentX, currentY;
149 int16_t nextX, nextY;
150 Vector2 simPosition;
151 Vector2 simVelocity;
152 uint16_t generation;
153 float startMovingTime;
154 float damage, futureDamage;
155 float contactTime;
156 uint8_t enemyType;
157 uint8_t movePathCount;
158 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
159 } Enemy;
160
161 Enemy enemies[ENEMY_MAX_COUNT];
162 int enemyCount = 0;
163
164 EnemyClassConfig enemyClassConfigs[] = {
165 [ENEMY_TYPE_MINION] = {
166 .health = 3.0f,
167 .speed = 1.0f,
168 .radius = 0.25f,
169 .maxAcceleration = 1.0f,
170 .explosionDamage = 1.0f,
171 .requiredContactTime = 0.5f,
172 .explosionRange = 1.0f,
173 .explosionPushbackPower = 0.25f,
174 .goldValue = 1,
175 },
176 };
177
178 #define PROJECTILE_MAX_COUNT 1200
179 #define PROJECTILE_TYPE_NONE 0
180 #define PROJECTILE_TYPE_BULLET 1
181
182 typedef struct Projectile
183 {
184 uint8_t projectileType;
185 float shootTime;
186 float arrivalTime;
187 float damage;
188 Vector2 position;
189 Vector2 target;
190 Vector2 directionNormal;
191 EnemyId targetEnemy;
192 } Projectile;
193
194 //# Function declarations
195 float TowerGetMaxHealth(Tower *tower);
196 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
197 int EnemyAddDamage(Enemy *enemy, float damage);
198
199 #endif
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
Another change you may notice is, that the code files are now in a subdirectory instead of a flat file like it was before - this reflects how I am organizing the code files for the tutorial steps here. In a real project, this would probably be called "src". If a project grows bigger, it's good practice to introduce subdirectories for different parts of the application, but I don't think this tutorial will get that far.
Anyway, when refactoring, it's always good to be able to do smaller steps while still being able to compile the application to test if everything is still working. Since we are only moving code around and don't do much else, this should be save, but nothing is more annoying than having changed 2000 lines of code and then having to find out what went wrong.
After moving the struct and function declarations into the header file, let's start extracting the enemy parts:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Variables
7 GUIState guiState = {0};
8 GameTime gameTime = {0};
9 Tower towers[TOWER_MAX_COUNT];
10 int towerCount = 0;
11
12 Level levels[] = {
13 [0] = {
14 .state = LEVEL_STATE_BUILDING,
15 .initialGold = 10,
16 .waves[0] = {
17 .enemyType = ENEMY_TYPE_MINION,
18 .wave = 0,
19 .count = 10,
20 .interval = 1.0f,
21 .delay = 1.0f,
22 .spawnPosition = {0, 6},
23 },
24 .waves[1] = {
25 .enemyType = ENEMY_TYPE_MINION,
26 .wave = 1,
27 .count = 20,
28 .interval = 0.5f,
29 .delay = 1.0f,
30 .spawnPosition = {0, 6},
31 },
32 .waves[2] = {
33 .enemyType = ENEMY_TYPE_MINION,
34 .wave = 2,
35 .count = 30,
36 .interval = 0.25f,
37 .delay = 1.0f,
38 .spawnPosition = {0, 6},
39 }
40 },
41 };
42
43 Level *currentLevel = levels;
44
45 Particle particles[PARTICLE_MAX_COUNT];
46 int particleCount = 0;
47
48 //# Particle system
49
50 void ParticleInit()
51 {
52 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
53 {
54 particles[i] = (Particle){0};
55 }
56 particleCount = 0;
57 }
58
59 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
60 {
61 if (particleCount >= PARTICLE_MAX_COUNT)
62 {
63 return;
64 }
65
66 int index = -1;
67 for (int i = 0; i < particleCount; i++)
68 {
69 if (particles[i].particleType == PARTICLE_TYPE_NONE)
70 {
71 index = i;
72 break;
73 }
74 }
75
76 if (index == -1)
77 {
78 index = particleCount++;
79 }
80
81 Particle *particle = &particles[index];
82 particle->particleType = particleType;
83 particle->spawnTime = gameTime.time;
84 particle->lifetime = lifetime;
85 particle->position = position;
86 particle->velocity = velocity;
87 }
88
89 void ParticleUpdate()
90 {
91 for (int i = 0; i < particleCount; i++)
92 {
93 Particle *particle = &particles[i];
94 if (particle->particleType == PARTICLE_TYPE_NONE)
95 {
96 continue;
97 }
98
99 float age = gameTime.time - particle->spawnTime;
100
101 if (particle->lifetime > age)
102 {
103 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
104 }
105 else {
106 particle->particleType = PARTICLE_TYPE_NONE;
107 }
108 }
109 }
110
111 void DrawExplosionParticle(Particle *particle, float transition)
112 {
113 float size = 1.2f * (1.0f - transition);
114 Color startColor = WHITE;
115 Color endColor = RED;
116 Color color = ColorLerp(startColor, endColor, transition);
117 DrawCube(particle->position, size, size, size, color);
118 }
119
120 void ParticleDraw()
121 {
122 for (int i = 0; i < particleCount; i++)
123 {
124 Particle particle = particles[i];
125 if (particle.particleType == PARTICLE_TYPE_NONE)
126 {
127 continue;
128 }
129
130 float age = gameTime.time - particle.spawnTime;
131 float transition = age / particle.lifetime;
132 switch (particle.particleType)
133 {
134 case PARTICLE_TYPE_EXPLOSION:
135 DrawExplosionParticle(&particle, transition);
136 break;
137 default:
138 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
139 break;
140 }
141 }
142 }
143
144 //# Pathfinding map
145
146 // The queue is a simple array of nodes, we add nodes to the end and remove
147 // nodes from the front. We keep the array around to avoid unnecessary allocations
148 static PathfindingNode *pathfindingNodeQueue = 0;
149 static int pathfindingNodeQueueCount = 0;
150 static int pathfindingNodeQueueCapacity = 0;
151
152 // The pathfinding map stores the distances from the castle to each cell in the map.
153 PathfindingMap pathfindingMap = {0};
154
155 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
156 {
157 // transforming between map space and world space allows us to adapt
158 // position and scale of the map without changing the pathfinding data
159 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
160 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
161 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
162 pathfindingMap.width = width;
163 pathfindingMap.height = height;
164 pathfindingMap.scale = scale;
165 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
166 for (int i = 0; i < width * height; i++)
167 {
168 pathfindingMap.distances[i] = -1.0f;
169 }
170
171 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
172 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
173 }
174
175 float PathFindingGetDistance(int mapX, int mapY)
176 {
177 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
178 {
179 // when outside the map, we return the manhattan distance to the castle (0,0)
180 return fabsf((float)mapX) + fabsf((float)mapY);
181 }
182
183 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
184 }
185
186 void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
187 {
188 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
189 {
190 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
191 // we use MemAlloc/MemRealloc to allocate memory for the queue
192 // I am not entirely sure if MemRealloc allows passing a null pointer
193 // so we check if the pointer is null and use MemAlloc in that case
194 if (pathfindingNodeQueue == 0)
195 {
196 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
197 }
198 else
199 {
200 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
201 }
202 }
203
204 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
205 node->x = x;
206 node->y = y;
207 node->fromX = fromX;
208 node->fromY = fromY;
209 node->distance = distance;
210 }
211
212 PathfindingNode *PathFindingNodePop()
213 {
214 if (pathfindingNodeQueueCount == 0)
215 {
216 return 0;
217 }
218 // we return the first node in the queue; we want to return a pointer to the node
219 // so we can return 0 if the queue is empty.
220 // We should _not_ return a pointer to the element in the list, because the list
221 // may be reallocated and the pointer would become invalid. Or the
222 // popped element is overwritten by the next push operation.
223 // Using static here means that the variable is permanently allocated.
224 static PathfindingNode node;
225 node = pathfindingNodeQueue[0];
226 // we shift all nodes one position to the front
227 for (int i = 1; i < pathfindingNodeQueueCount; i++)
228 {
229 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
230 }
231 --pathfindingNodeQueueCount;
232 return &node;
233 }
234
235 // transform a world position to a map position in the array;
236 // returns true if the position is inside the map
237 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
238 {
239 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
240 *mapX = (int16_t)mapPosition.x;
241 *mapY = (int16_t)mapPosition.z;
242 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
243 }
244
245 void PathFindingMapUpdate()
246 {
247 const int castleX = 0, castleY = 0;
248 int16_t castleMapX, castleMapY;
249 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
250 {
251 return;
252 }
253 int width = pathfindingMap.width, height = pathfindingMap.height;
254
255 // reset the distances to -1
256 for (int i = 0; i < width * height; i++)
257 {
258 pathfindingMap.distances[i] = -1.0f;
259 }
260 // reset the tower indices
261 for (int i = 0; i < width * height; i++)
262 {
263 pathfindingMap.towerIndex[i] = -1;
264 }
265 // reset the delta src
266 for (int i = 0; i < width * height; i++)
267 {
268 pathfindingMap.deltaSrc[i].x = 0;
269 pathfindingMap.deltaSrc[i].y = 0;
270 }
271
272 for (int i = 0; i < towerCount; i++)
273 {
274 Tower *tower = &towers[i];
275 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
276 {
277 continue;
278 }
279 int16_t mapX, mapY;
280 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
281 // this would not work correctly and needs to be refined to allow towers covering multiple cells
282 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
283 // one cell. For now.
284 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
285 {
286 continue;
287 }
288 int index = mapY * width + mapX;
289 pathfindingMap.towerIndex[index] = i;
290 }
291
292 // we start at the castle and add the castle to the queue
293 pathfindingMap.maxDistance = 0.0f;
294 pathfindingNodeQueueCount = 0;
295 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
296 PathfindingNode *node = 0;
297 while ((node = PathFindingNodePop()))
298 {
299 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
300 {
301 continue;
302 }
303 int index = node->y * width + node->x;
304 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
305 {
306 continue;
307 }
308
309 int deltaX = node->x - node->fromX;
310 int deltaY = node->y - node->fromY;
311 // even if the cell is blocked by a tower, we still may want to store the direction
312 // (though this might not be needed, IDK right now)
313 pathfindingMap.deltaSrc[index].x = (char) deltaX;
314 pathfindingMap.deltaSrc[index].y = (char) deltaY;
315
316 // we skip nodes that are blocked by towers
317 if (pathfindingMap.towerIndex[index] >= 0)
318 {
319 node->distance += 8.0f;
320 }
321 pathfindingMap.distances[index] = node->distance;
322 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
323 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
324 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
325 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
326 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
327 }
328 }
329
330 void PathFindingMapDraw()
331 {
332 float cellSize = pathfindingMap.scale * 0.9f;
333 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
334 for (int x = 0; x < pathfindingMap.width; x++)
335 {
336 for (int y = 0; y < pathfindingMap.height; y++)
337 {
338 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
339 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
340 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
341 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
342 // animate the distance "wave" to show how the pathfinding algorithm expands
343 // from the castle
344 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
345 {
346 color = BLACK;
347 }
348 DrawCube(position, cellSize, 0.1f, cellSize, color);
349 }
350 }
351 }
352
353 Vector2 PathFindingGetGradient(Vector3 world)
354 {
355 int16_t mapX, mapY;
356 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
357 {
358 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
359 return (Vector2){(float)-delta.x, (float)-delta.y};
360 }
361 // fallback to a simple gradient calculation
362 float n = PathFindingGetDistance(mapX, mapY - 1);
363 float s = PathFindingGetDistance(mapX, mapY + 1);
364 float w = PathFindingGetDistance(mapX - 1, mapY);
365 float e = PathFindingGetDistance(mapX + 1, mapY);
366 return (Vector2){w - e + 0.25f, n - s + 0.125f};
367 }
368
369 //# Projectiles
370 Projectile projectiles[PROJECTILE_MAX_COUNT];
371 int projectileCount = 0;
372
373 void ProjectileInit()
374 {
375 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
376 {
377 projectiles[i] = (Projectile){0};
378 }
379 }
380
381 void ProjectileDraw()
382 {
383 for (int i = 0; i < projectileCount; i++)
384 {
385 Projectile projectile = projectiles[i];
386 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
387 {
388 continue;
389 }
390 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
391 if (transition >= 1.0f)
392 {
393 continue;
394 }
395 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
396 float x = position.x;
397 float y = position.y;
398 float dx = projectile.directionNormal.x;
399 float dy = projectile.directionNormal.y;
400 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
401 {
402 x -= dx * 0.1f;
403 y -= dy * 0.1f;
404 float size = 0.1f * d;
405 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
406 }
407 }
408 }
409
410 void ProjectileUpdate()
411 {
412 for (int i = 0; i < projectileCount; i++)
413 {
414 Projectile *projectile = &projectiles[i];
415 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
416 {
417 continue;
418 }
419 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
420 if (transition >= 1.0f)
421 {
422 projectile->projectileType = PROJECTILE_TYPE_NONE;
423 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
424 if (enemy)
425 {
426 EnemyAddDamage(enemy, projectile->damage);
427 }
428 continue;
429 }
430 }
431 }
432
433 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
434 {
435 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
436 {
437 Projectile *projectile = &projectiles[i];
438 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
439 {
440 projectile->projectileType = projectileType;
441 projectile->shootTime = gameTime.time;
442 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
443 projectile->damage = damage;
444 projectile->position = position;
445 projectile->target = target;
446 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
447 projectile->targetEnemy = EnemyGetId(enemy);
448 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
449 return projectile;
450 }
451 }
452 return 0;
453 }
454
455 //# Towers
456
457 void TowerInit()
458 {
459 for (int i = 0; i < TOWER_MAX_COUNT; i++)
460 {
461 towers[i] = (Tower){0};
462 }
463 towerCount = 0;
464 }
465
466 Tower *TowerGetAt(int16_t x, int16_t y)
467 {
468 for (int i = 0; i < towerCount; i++)
469 {
470 if (towers[i].x == x && towers[i].y == y)
471 {
472 return &towers[i];
473 }
474 }
475 return 0;
476 }
477
478 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
479 {
480 if (towerCount >= TOWER_MAX_COUNT)
481 {
482 return 0;
483 }
484
485 Tower *tower = TowerGetAt(x, y);
486 if (tower)
487 {
488 return 0;
489 }
490
491 tower = &towers[towerCount++];
492 tower->x = x;
493 tower->y = y;
494 tower->towerType = towerType;
495 tower->cooldown = 0.0f;
496 tower->damage = 0.0f;
497 return tower;
498 }
499
500 Tower *GetTowerByType(uint8_t towerType)
501 {
502 for (int i = 0; i < towerCount; i++)
503 {
504 if (towers[i].towerType == towerType)
505 {
506 return &towers[i];
507 }
508 }
509 return 0;
510 }
511
512 int GetTowerCosts(uint8_t towerType)
513 {
514 switch (towerType)
515 {
516 case TOWER_TYPE_BASE:
517 return 0;
518 case TOWER_TYPE_GUN:
519 return 6;
520 case TOWER_TYPE_WALL:
521 return 2;
522 }
523 return 0;
524 }
525
526 float TowerGetMaxHealth(Tower *tower)
527 {
528 switch (tower->towerType)
529 {
530 case TOWER_TYPE_BASE:
531 return 10.0f;
532 case TOWER_TYPE_GUN:
533 return 3.0f;
534 case TOWER_TYPE_WALL:
535 return 5.0f;
536 }
537 return 0.0f;
538 }
539
540 void TowerDraw()
541 {
542 for (int i = 0; i < towerCount; i++)
543 {
544 Tower tower = towers[i];
545 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
546 switch (tower.towerType)
547 {
548 case TOWER_TYPE_BASE:
549 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
550 break;
551 case TOWER_TYPE_GUN:
552 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
553 break;
554 case TOWER_TYPE_WALL:
555 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
556 break;
557 }
558 }
559 }
560
561 void TowerGunUpdate(Tower *tower)
562 {
563 if (tower->cooldown <= 0)
564 {
565 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
566 if (enemy)
567 {
568 tower->cooldown = 0.125f;
569 // shoot the enemy; determine future position of the enemy
570 float bulletSpeed = 1.0f;
571 float bulletDamage = 3.0f;
572 Vector2 velocity = enemy->simVelocity;
573 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
574 Vector2 towerPosition = {tower->x, tower->y};
575 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
576 for (int i = 0; i < 8; i++) {
577 velocity = enemy->simVelocity;
578 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
579 float distance = Vector2Distance(towerPosition, futurePosition);
580 float eta2 = distance / bulletSpeed;
581 if (fabs(eta - eta2) < 0.01f) {
582 break;
583 }
584 eta = (eta2 + eta) * 0.5f;
585 }
586 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
587 bulletSpeed, bulletDamage);
588 enemy->futureDamage += bulletDamage;
589 }
590 }
591 else
592 {
593 tower->cooldown -= gameTime.deltaTime;
594 }
595 }
596
597 void TowerUpdate()
598 {
599 for (int i = 0; i < towerCount; i++)
600 {
601 Tower *tower = &towers[i];
602 switch (tower->towerType)
603 {
604 case TOWER_TYPE_GUN:
605 TowerGunUpdate(tower);
606 break;
607 }
608 }
609 }
610
611 //# Game
612
613 void InitLevel(Level *level)
614 {
615 TowerInit();
616 EnemyInit();
617 ProjectileInit();
618 ParticleInit();
619 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
620
621 level->placementMode = 0;
622 level->state = LEVEL_STATE_BUILDING;
623 level->nextState = LEVEL_STATE_NONE;
624 level->playerGold = level->initialGold;
625
626 Camera *camera = &level->camera;
627 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
628 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
629 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
630 camera->fovy = 45.0f;
631 camera->projection = CAMERA_PERSPECTIVE;
632 }
633
634 void DrawLevelHud(Level *level)
635 {
636 const char *text = TextFormat("Gold: %d", level->playerGold);
637 Font font = GetFontDefault();
638 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
639 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
640 }
641
642 void DrawLevelReportLostWave(Level *level)
643 {
644 BeginMode3D(level->camera);
645 DrawGrid(10, 1.0f);
646 TowerDraw();
647 EnemyDraw();
648 ProjectileDraw();
649 ParticleDraw();
650 guiState.isBlocked = 0;
651 EndMode3D();
652
653 const char *text = "Wave lost";
654 int textWidth = MeasureText(text, 20);
655 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
656
657 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
658 {
659 level->nextState = LEVEL_STATE_RESET;
660 }
661 }
662
663 int HasLevelNextWave(Level *level)
664 {
665 for (int i = 0; i < 10; i++)
666 {
667 EnemyWave *wave = &level->waves[i];
668 if (wave->wave == level->currentWave)
669 {
670 return 1;
671 }
672 }
673 return 0;
674 }
675
676 void DrawLevelReportWonWave(Level *level)
677 {
678 BeginMode3D(level->camera);
679 DrawGrid(10, 1.0f);
680 TowerDraw();
681 EnemyDraw();
682 ProjectileDraw();
683 ParticleDraw();
684 guiState.isBlocked = 0;
685 EndMode3D();
686
687 const char *text = "Wave won";
688 int textWidth = MeasureText(text, 20);
689 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
690
691
692 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
693 {
694 level->nextState = LEVEL_STATE_RESET;
695 }
696
697 if (HasLevelNextWave(level))
698 {
699 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
700 {
701 level->nextState = LEVEL_STATE_BUILDING;
702 }
703 }
704 else {
705 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
706 {
707 level->nextState = LEVEL_STATE_WON_LEVEL;
708 }
709 }
710 }
711
712 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
713 {
714 static ButtonState buttonStates[8] = {0};
715 int cost = GetTowerCosts(towerType);
716 const char *text = TextFormat("%s: %d", name, cost);
717 buttonStates[towerType].isSelected = level->placementMode == towerType;
718 buttonStates[towerType].isDisabled = level->playerGold < cost;
719 if (Button(text, x, y, width, height, &buttonStates[towerType]))
720 {
721 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
722 }
723 }
724
725 void DrawLevelBuildingState(Level *level)
726 {
727 BeginMode3D(level->camera);
728 DrawGrid(10, 1.0f);
729 TowerDraw();
730 EnemyDraw();
731 ProjectileDraw();
732 ParticleDraw();
733
734 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
735 float planeDistance = ray.position.y / -ray.direction.y;
736 float planeX = ray.direction.x * planeDistance + ray.position.x;
737 float planeY = ray.direction.z * planeDistance + ray.position.z;
738 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
739 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
740 if (level->placementMode && !guiState.isBlocked)
741 {
742 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
743 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
744 {
745 if (TowerTryAdd(level->placementMode, mapX, mapY))
746 {
747 level->playerGold -= GetTowerCosts(level->placementMode);
748 level->placementMode = TOWER_TYPE_NONE;
749 }
750 }
751 }
752
753 guiState.isBlocked = 0;
754
755 EndMode3D();
756
757 static ButtonState buildWallButtonState = {0};
758 static ButtonState buildGunButtonState = {0};
759 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
760 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
761
762 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
763 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
764
765 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
766 {
767 level->nextState = LEVEL_STATE_RESET;
768 }
769
770 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
771 {
772 level->nextState = LEVEL_STATE_BATTLE;
773 }
774
775 const char *text = "Building phase";
776 int textWidth = MeasureText(text, 20);
777 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
778 }
779
780 void InitBattleStateConditions(Level *level)
781 {
782 level->state = LEVEL_STATE_BATTLE;
783 level->nextState = LEVEL_STATE_NONE;
784 level->waveEndTimer = 0.0f;
785 for (int i = 0; i < 10; i++)
786 {
787 EnemyWave *wave = &level->waves[i];
788 wave->spawned = 0;
789 wave->timeToSpawnNext = wave->delay;
790 }
791 }
792
793 void DrawLevelBattleState(Level *level)
794 {
795 BeginMode3D(level->camera);
796 DrawGrid(10, 1.0f);
797 TowerDraw();
798 EnemyDraw();
799 ProjectileDraw();
800 ParticleDraw();
801 guiState.isBlocked = 0;
802 EndMode3D();
803
804 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
805 {
806 level->nextState = LEVEL_STATE_RESET;
807 }
808
809 int maxCount = 0;
810 int remainingCount = 0;
811 for (int i = 0; i < 10; i++)
812 {
813 EnemyWave *wave = &level->waves[i];
814 if (wave->wave != level->currentWave)
815 {
816 continue;
817 }
818 maxCount += wave->count;
819 remainingCount += wave->count - wave->spawned;
820 }
821 int aliveCount = EnemyCount();
822 remainingCount += aliveCount;
823
824 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
825 int textWidth = MeasureText(text, 20);
826 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
827 }
828
829 void DrawLevel(Level *level)
830 {
831 switch (level->state)
832 {
833 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
834 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
835 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
836 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
837 default: break;
838 }
839
840 DrawLevelHud(level);
841 }
842
843 void UpdateLevel(Level *level)
844 {
845 if (level->state == LEVEL_STATE_BATTLE)
846 {
847 int activeWaves = 0;
848 for (int i = 0; i < 10; i++)
849 {
850 EnemyWave *wave = &level->waves[i];
851 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
852 {
853 continue;
854 }
855 activeWaves++;
856 wave->timeToSpawnNext -= gameTime.deltaTime;
857 if (wave->timeToSpawnNext <= 0.0f)
858 {
859 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
860 if (enemy)
861 {
862 wave->timeToSpawnNext = wave->interval;
863 wave->spawned++;
864 }
865 }
866 }
867 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
868 level->waveEndTimer += gameTime.deltaTime;
869 if (level->waveEndTimer >= 2.0f)
870 {
871 level->nextState = LEVEL_STATE_LOST_WAVE;
872 }
873 }
874 else if (activeWaves == 0 && EnemyCount() == 0)
875 {
876 level->waveEndTimer += gameTime.deltaTime;
877 if (level->waveEndTimer >= 2.0f)
878 {
879 level->nextState = LEVEL_STATE_WON_WAVE;
880 }
881 }
882 }
883
884 PathFindingMapUpdate();
885 EnemyUpdate();
886 TowerUpdate();
887 ProjectileUpdate();
888 ParticleUpdate();
889
890 if (level->nextState == LEVEL_STATE_RESET)
891 {
892 InitLevel(level);
893 }
894
895 if (level->nextState == LEVEL_STATE_BATTLE)
896 {
897 InitBattleStateConditions(level);
898 }
899
900 if (level->nextState == LEVEL_STATE_WON_WAVE)
901 {
902 level->currentWave++;
903 level->state = LEVEL_STATE_WON_WAVE;
904 }
905
906 if (level->nextState == LEVEL_STATE_LOST_WAVE)
907 {
908 level->state = LEVEL_STATE_LOST_WAVE;
909 }
910
911 if (level->nextState == LEVEL_STATE_BUILDING)
912 {
913 level->state = LEVEL_STATE_BUILDING;
914 }
915
916 if (level->nextState == LEVEL_STATE_WON_LEVEL)
917 {
918 // make something of this later
919 InitLevel(level);
920 }
921
922 level->nextState = LEVEL_STATE_NONE;
923 }
924
925 float nextSpawnTime = 0.0f;
926
927 void ResetGame()
928 {
929 InitLevel(currentLevel);
930 }
931
932 void InitGame()
933 {
934 TowerInit();
935 EnemyInit();
936 ProjectileInit();
937 ParticleInit();
938 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
939
940 currentLevel = levels;
941 InitLevel(currentLevel);
942 }
943
944 //# Immediate GUI functions
945
946 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
947 {
948 Rectangle bounds = {x, y, width, height};
949 int isPressed = 0;
950 int isSelected = state && state->isSelected;
951 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
952 {
953 Color color = isSelected ? DARKGRAY : GRAY;
954 DrawRectangle(x, y, width, height, color);
955 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
956 {
957 isPressed = 1;
958 }
959 guiState.isBlocked = 1;
960 }
961 else
962 {
963 Color color = isSelected ? WHITE : LIGHTGRAY;
964 DrawRectangle(x, y, width, height, color);
965 }
966 Font font = GetFontDefault();
967 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
968 Color textColor = state->isDisabled ? GRAY : BLACK;
969 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
970 return isPressed;
971 }
972
973 //# Main game loop
974
975 void GameUpdate()
976 {
977 float dt = GetFrameTime();
978 // cap maximum delta time to 0.1 seconds to prevent large time steps
979 if (dt > 0.1f) dt = 0.1f;
980 gameTime.time += dt;
981 gameTime.deltaTime = dt;
982
983 UpdateLevel(currentLevel);
984 }
985
986 int main(void)
987 {
988 int screenWidth, screenHeight;
989 GetPreferredSize(&screenWidth, &screenHeight);
990 InitWindow(screenWidth, screenHeight, "Tower defense");
991 SetTargetFPS(30);
992
993 InitGame();
994
995 while (!WindowShouldClose())
996 {
997 if (IsPaused()) {
998 // canvas is not visible in browser - do nothing
999 continue;
1000 }
1001
1002 BeginDrawing();
1003 ClearBackground(DARKBLUE);
1004
1005 GameUpdate();
1006 DrawLevel(currentLevel);
1007
1008 EndDrawing();
1009 }
1010
1011 CloseWindow();
1012
1013 return 0;
1014 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 } Particle;
28
29 #define TOWER_MAX_COUNT 400
30 #define TOWER_TYPE_NONE 0
31 #define TOWER_TYPE_BASE 1
32 #define TOWER_TYPE_GUN 2
33 #define TOWER_TYPE_WALL 3
34
35 typedef struct Tower
36 {
37 int16_t x, y;
38 uint8_t towerType;
39 float cooldown;
40 float damage;
41 } Tower;
42
43 typedef struct GameTime
44 {
45 float time;
46 float deltaTime;
47 } GameTime;
48
49 typedef struct ButtonState {
50 char isSelected;
51 char isDisabled;
52 } ButtonState;
53
54 typedef struct GUIState {
55 int isBlocked;
56 } GUIState;
57
58 typedef enum LevelState
59 {
60 LEVEL_STATE_NONE,
61 LEVEL_STATE_BUILDING,
62 LEVEL_STATE_BATTLE,
63 LEVEL_STATE_WON_WAVE,
64 LEVEL_STATE_LOST_WAVE,
65 LEVEL_STATE_WON_LEVEL,
66 LEVEL_STATE_RESET,
67 } LevelState;
68
69 typedef struct EnemyWave {
70 uint8_t enemyType;
71 uint8_t wave;
72 uint16_t count;
73 float interval;
74 float delay;
75 Vector2 spawnPosition;
76
77 uint16_t spawned;
78 float timeToSpawnNext;
79 } EnemyWave;
80
81 typedef struct Level
82 {
83 LevelState state;
84 LevelState nextState;
85 Camera3D camera;
86 int placementMode;
87
88 int initialGold;
89 int playerGold;
90
91 EnemyWave waves[10];
92 int currentWave;
93 float waveEndTimer;
94 } Level;
95
96 typedef struct DeltaSrc
97 {
98 char x, y;
99 } DeltaSrc;
100
101 typedef struct PathfindingMap
102 {
103 int width, height;
104 float scale;
105 float *distances;
106 long *towerIndex;
107 DeltaSrc *deltaSrc;
108 float maxDistance;
109 Matrix toMapSpace;
110 Matrix toWorldSpace;
111 } PathfindingMap;
112
113 // when we execute the pathfinding algorithm, we need to store the active nodes
114 // in a queue. Each node has a position, a distance from the start, and the
115 // position of the node that we came from.
116 typedef struct PathfindingNode
117 {
118 int16_t x, y, fromX, fromY;
119 float distance;
120 } PathfindingNode;
121
122 typedef struct EnemyId
123 {
124 uint16_t index;
125 uint16_t generation;
126 } EnemyId;
127
128 typedef struct EnemyClassConfig
129 {
130 float speed;
131 float health;
132 float radius;
133 float maxAcceleration;
134 float requiredContactTime;
135 float explosionDamage;
136 float explosionRange;
137 float explosionPushbackPower;
138 int goldValue;
139 } EnemyClassConfig;
140
141 typedef struct Enemy
142 {
143 int16_t currentX, currentY;
144 int16_t nextX, nextY;
145 Vector2 simPosition;
146 Vector2 simVelocity;
147 uint16_t generation;
148 float startMovingTime;
149 float damage, futureDamage;
150 float contactTime;
151 uint8_t enemyType;
152 uint8_t movePathCount;
153 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
154 } Enemy;
155
156 #define PROJECTILE_MAX_COUNT 1200
157 #define PROJECTILE_TYPE_NONE 0
158 #define PROJECTILE_TYPE_BULLET 1
159
160 typedef struct Projectile
161 {
162 uint8_t projectileType;
163 float shootTime;
164 float arrivalTime;
165 float damage;
166 Vector2 position;
167 Vector2 target;
168 Vector2 directionNormal;
169 EnemyId targetEnemy;
170 } Projectile;
171
172 //# Function declarations
173 float TowerGetMaxHealth(Tower *tower);
174 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
175 int EnemyAddDamage(Enemy *enemy, float damage);
176
177 //# Enemy functions
178 void EnemyInit();
179 void EnemyDraw();
180 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
181 void EnemyUpdate();
182 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
183 float EnemyGetMaxHealth(Enemy *enemy);
184 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
185 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
186 EnemyId EnemyGetId(Enemy *enemy);
187 Enemy *EnemyTryResolve(EnemyId enemyId);
188 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
189 int EnemyAddDamage(Enemy *enemy, float damage);
190 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
191 int EnemyCount();
192
193 //# Particles
194 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
195
196 //# Path finding
197 Vector2 PathFindingGetGradient(Vector3 world);
198
199 //# variables
200 extern Level *currentLevel;
201 extern Enemy enemies[ENEMY_MAX_COUNT];
202 extern int enemyCount;
203 extern EnemyClassConfig enemyClassConfigs[];
204
205 extern GUIState guiState;
206 extern GameTime gameTime;
207 extern Tower towers[TOWER_MAX_COUNT];
208 extern int towerCount;
209
210 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 3.0f,
9 .speed = 1.0f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 void EnemyInit()
24 {
25 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
26 {
27 enemies[i] = (Enemy){0};
28 }
29 enemyCount = 0;
30 }
31
32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
33 {
34 return enemyClassConfigs[enemy->enemyType].speed;
35 }
36
37 float EnemyGetMaxHealth(Enemy *enemy)
38 {
39 return enemyClassConfigs[enemy->enemyType].health;
40 }
41
42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
43 {
44 int16_t castleX = 0;
45 int16_t castleY = 0;
46 int16_t dx = castleX - currentX;
47 int16_t dy = castleY - currentY;
48 if (dx == 0 && dy == 0)
49 {
50 *nextX = currentX;
51 *nextY = currentY;
52 return 1;
53 }
54 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
55
56 if (gradient.x == 0 && gradient.y == 0)
57 {
58 *nextX = currentX;
59 *nextY = currentY;
60 return 1;
61 }
62
63 if (fabsf(gradient.x) > fabsf(gradient.y))
64 {
65 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
66 *nextY = currentY;
67 return 0;
68 }
69 *nextX = currentX;
70 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
71 return 0;
72 }
73
74
75 // this function predicts the movement of the unit for the next deltaT seconds
76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
77 {
78 const float pointReachedDistance = 0.25f;
79 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
80 const float maxSimStepTime = 0.015625f;
81
82 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
83 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
84 int16_t nextX = enemy->nextX;
85 int16_t nextY = enemy->nextY;
86 Vector2 position = enemy->simPosition;
87 int passedCount = 0;
88 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
89 {
90 float stepTime = fminf(deltaT - t, maxSimStepTime);
91 Vector2 target = (Vector2){nextX, nextY};
92 float speed = Vector2Length(*velocity);
93 // draw the target position for debugging
94 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
95 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
96 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
97 {
98 // we reached the target position, let's move to the next waypoint
99 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100 target = (Vector2){nextX, nextY};
101 // track how many waypoints we passed
102 passedCount++;
103 }
104
105 // acceleration towards the target
106 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108 *velocity = Vector2Add(*velocity, acceleration);
109
110 // limit the speed to the maximum speed
111 if (speed > maxSpeed)
112 {
113 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114 }
115
116 // move the enemy
117 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118 }
119
120 if (waypointPassedCount)
121 {
122 (*waypointPassedCount) = passedCount;
123 }
124
125 return position;
126 }
127
128 void EnemyDraw()
129 {
130 for (int i = 0; i < enemyCount; i++)
131 {
132 Enemy enemy = enemies[i];
133 if (enemy.enemyType == ENEMY_TYPE_NONE)
134 {
135 continue;
136 }
137
138 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139
140 if (enemy.movePathCount > 0)
141 {
142 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144 }
145 for (int j = 1; j < enemy.movePathCount; j++)
146 {
147 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149 DrawLine3D(p, q, GREEN);
150 }
151
152 switch (enemy.enemyType)
153 {
154 case ENEMY_TYPE_MINION:
155 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156 break;
157 }
158 }
159 }
160
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163 // damage the tower
164 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167 float explosionRange2 = explosionRange * explosionRange;
168 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169 // explode the enemy
170 if (tower->damage >= TowerGetMaxHealth(tower))
171 {
172 tower->towerType = TOWER_TYPE_NONE;
173 }
174
175 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
176 explosionSource,
177 (Vector3){0, 0.1f, 0}, 1.0f);
178
179 enemy->enemyType = ENEMY_TYPE_NONE;
180
181 // push back enemies & dealing damage
182 for (int i = 0; i < enemyCount; i++)
183 {
184 Enemy *other = &enemies[i];
185 if (other->enemyType == ENEMY_TYPE_NONE)
186 {
187 continue;
188 }
189 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190 if (distanceSqr > 0 && distanceSqr < explosionRange2)
191 {
192 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194 EnemyAddDamage(other, explosionDamge);
195 }
196 }
197 }
198
199 void EnemyUpdate()
200 {
201 const float castleX = 0;
202 const float castleY = 0;
203 const float maxPathDistance2 = 0.25f * 0.25f;
204
205 for (int i = 0; i < enemyCount; i++)
206 {
207 Enemy *enemy = &enemies[i];
208 if (enemy->enemyType == ENEMY_TYPE_NONE)
209 {
210 continue;
211 }
212
213 int waypointPassedCount = 0;
214 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215 enemy->startMovingTime = gameTime.time;
216 // track path of unit
217 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218 {
219 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220 {
221 enemy->movePath[j] = enemy->movePath[j - 1];
222 }
223 enemy->movePath[0] = enemy->simPosition;
224 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225 {
226 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227 }
228 }
229
230 if (waypointPassedCount > 0)
231 {
232 enemy->currentX = enemy->nextX;
233 enemy->currentY = enemy->nextY;
234 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236 {
237 // enemy reached the castle; remove it
238 enemy->enemyType = ENEMY_TYPE_NONE;
239 continue;
240 }
241 }
242 }
243
244 // handle collisions between enemies
245 for (int i = 0; i < enemyCount - 1; i++)
246 {
247 Enemy *enemyA = &enemies[i];
248 if (enemyA->enemyType == ENEMY_TYPE_NONE)
249 {
250 continue;
251 }
252 for (int j = i + 1; j < enemyCount; j++)
253 {
254 Enemy *enemyB = &enemies[j];
255 if (enemyB->enemyType == ENEMY_TYPE_NONE)
256 {
257 continue;
258 }
259 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262 float radiusSum = radiusA + radiusB;
263 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264 {
265 // collision
266 float distance = sqrtf(distanceSqr);
267 float overlap = radiusSum - distance;
268 // move the enemies apart, but softly; if we have a clog of enemies,
269 // moving them perfectly apart can cause them to jitter
270 float positionCorrection = overlap / 5.0f;
271 Vector2 direction = (Vector2){
272 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276 }
277 }
278 }
279
280 // handle collisions between enemies and towers
281 for (int i = 0; i < enemyCount; i++)
282 {
283 Enemy *enemy = &enemies[i];
284 if (enemy->enemyType == ENEMY_TYPE_NONE)
285 {
286 continue;
287 }
288 enemy->contactTime -= gameTime.deltaTime;
289 if (enemy->contactTime < 0.0f)
290 {
291 enemy->contactTime = 0.0f;
292 }
293
294 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295 // linear search over towers; could be optimized by using path finding tower map,
296 // but for now, we keep it simple
297 for (int j = 0; j < towerCount; j++)
298 {
299 Tower *tower = &towers[j];
300 if (tower->towerType == TOWER_TYPE_NONE)
301 {
302 continue;
303 }
304 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306 if (distanceSqr > combinedRadius * combinedRadius)
307 {
308 continue;
309 }
310 // potential collision; square / circle intersection
311 float dx = tower->x - enemy->simPosition.x;
312 float dy = tower->y - enemy->simPosition.y;
313 float absDx = fabsf(dx);
314 float absDy = fabsf(dy);
315 Vector3 contactPoint = {0};
316 if (absDx <= 0.5f && absDx <= absDy) {
317 // vertical collision; push the enemy out horizontally
318 float overlap = enemyRadius + 0.5f - absDy;
319 if (overlap < 0.0f)
320 {
321 continue;
322 }
323 float direction = dy > 0.0f ? -1.0f : 1.0f;
324 enemy->simPosition.y += direction * overlap;
325 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
326 }
327 else if (absDy <= 0.5f && absDy <= absDx)
328 {
329 // horizontal collision; push the enemy out vertically
330 float overlap = enemyRadius + 0.5f - absDx;
331 if (overlap < 0.0f)
332 {
333 continue;
334 }
335 float direction = dx > 0.0f ? -1.0f : 1.0f;
336 enemy->simPosition.x += direction * overlap;
337 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
338 }
339 else
340 {
341 // possible collision with a corner
342 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
343 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
344 float cornerX = tower->x + cornerDX;
345 float cornerY = tower->y + cornerDY;
346 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
347 if (cornerDistanceSqr > enemyRadius * enemyRadius)
348 {
349 continue;
350 }
351 // push the enemy out along the diagonal
352 float cornerDistance = sqrtf(cornerDistanceSqr);
353 float overlap = enemyRadius - cornerDistance;
354 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
355 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
356 enemy->simPosition.x -= directionX * overlap;
357 enemy->simPosition.y -= directionY * overlap;
358 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
359 }
360
361 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
362 {
363 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
364 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
365 {
366 EnemyTriggerExplode(enemy, tower, contactPoint);
367 }
368 }
369 }
370 }
371 }
372
373 EnemyId EnemyGetId(Enemy *enemy)
374 {
375 return (EnemyId){enemy - enemies, enemy->generation};
376 }
377
378 Enemy *EnemyTryResolve(EnemyId enemyId)
379 {
380 if (enemyId.index >= ENEMY_MAX_COUNT)
381 {
382 return 0;
383 }
384 Enemy *enemy = &enemies[enemyId.index];
385 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
386 {
387 return 0;
388 }
389 return enemy;
390 }
391
392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
393 {
394 Enemy *spawn = 0;
395 for (int i = 0; i < enemyCount; i++)
396 {
397 Enemy *enemy = &enemies[i];
398 if (enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 spawn = enemy;
401 break;
402 }
403 }
404
405 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
406 {
407 spawn = &enemies[enemyCount++];
408 }
409
410 if (spawn)
411 {
412 spawn->currentX = currentX;
413 spawn->currentY = currentY;
414 spawn->nextX = currentX;
415 spawn->nextY = currentY;
416 spawn->simPosition = (Vector2){currentX, currentY};
417 spawn->simVelocity = (Vector2){0, 0};
418 spawn->enemyType = enemyType;
419 spawn->startMovingTime = gameTime.time;
420 spawn->damage = 0.0f;
421 spawn->futureDamage = 0.0f;
422 spawn->generation++;
423 spawn->movePathCount = 0;
424 }
425
426 return spawn;
427 }
428
429 int EnemyAddDamage(Enemy *enemy, float damage)
430 {
431 enemy->damage += damage;
432 if (enemy->damage >= EnemyGetMaxHealth(enemy))
433 {
434 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
435 enemy->enemyType = ENEMY_TYPE_NONE;
436 return 1;
437 }
438
439 return 0;
440 }
441
442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
443 {
444 int16_t castleX = 0;
445 int16_t castleY = 0;
446 Enemy* closest = 0;
447 int16_t closestDistance = 0;
448 float range2 = range * range;
449 for (int i = 0; i < enemyCount; i++)
450 {
451 Enemy* enemy = &enemies[i];
452 if (enemy->enemyType == ENEMY_TYPE_NONE)
453 {
454 continue;
455 }
456 float maxHealth = EnemyGetMaxHealth(enemy);
457 if (enemy->futureDamage >= maxHealth)
458 {
459 // ignore enemies that will die soon
460 continue;
461 }
462 int16_t dx = castleX - enemy->currentX;
463 int16_t dy = castleY - enemy->currentY;
464 int16_t distance = abs(dx) + abs(dy);
465 if (!closest || distance < closestDistance)
466 {
467 float tdx = towerX - enemy->currentX;
468 float tdy = towerY - enemy->currentY;
469 float tdistance2 = tdx * tdx + tdy * tdy;
470 if (tdistance2 <= range2)
471 {
472 closest = enemy;
473 closestDistance = distance;
474 }
475 }
476 }
477 return closest;
478 }
479
480 int EnemyCount()
481 {
482 int count = 0;
483 for (int i = 0; i < enemyCount; i++)
484 {
485 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
486 {
487 count++;
488 }
489 }
490 return count;
491 }
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
During the first step, I did a few errors that I had to fix. I want to go over these to illustrate common problems you may encounter too:
wasm-ld: error: duplicate symbol: towerCount
>>> defined in C:\(...)\Local\Temp\emscripten_temp_vt9xjlri\td_main_0.o
>>> defined in C:\(...)\Local\Temp\emscripten_temp_vt9xjlri\enemy_1.o
This error message tells me that there is a variable called "towerCount" that is defined in two object files. It happened, because the variable was declared in the header file:
1 int towerCount = 0
When there was only a single file that included the header, this was no problem. But when having two .c files the include the same header, the variable is defined in both files, causing this error. The solution is to declare the variable as "extern" in the header file:
1 extern int towerCount
... and to declare it in one of the .c files:
1 int towerCount = 0
This way, the variable is only defined in one file and the other files know that this variable is defined somewhere else. While I only extracted the enemy functions and global variable declarations to the enemy.c file, I already had to declare a couple of functions in the header file that were previously only provided due to the order of functions in the main file. Let's proceed and extract the path finding:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Variables
7 GUIState guiState = {0};
8 GameTime gameTime = {0};
9 Tower towers[TOWER_MAX_COUNT];
10 int towerCount = 0;
11
12 Level levels[] = {
13 [0] = {
14 .state = LEVEL_STATE_BUILDING,
15 .initialGold = 10,
16 .waves[0] = {
17 .enemyType = ENEMY_TYPE_MINION,
18 .wave = 0,
19 .count = 10,
20 .interval = 1.0f,
21 .delay = 1.0f,
22 .spawnPosition = {0, 6},
23 },
24 .waves[1] = {
25 .enemyType = ENEMY_TYPE_MINION,
26 .wave = 1,
27 .count = 20,
28 .interval = 0.5f,
29 .delay = 1.0f,
30 .spawnPosition = {0, 6},
31 },
32 .waves[2] = {
33 .enemyType = ENEMY_TYPE_MINION,
34 .wave = 2,
35 .count = 30,
36 .interval = 0.25f,
37 .delay = 1.0f,
38 .spawnPosition = {0, 6},
39 }
40 },
41 };
42
43 Level *currentLevel = levels;
44
45 Particle particles[PARTICLE_MAX_COUNT];
46 int particleCount = 0;
47
48 //# Particle system
49
50 void ParticleInit()
51 {
52 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
53 {
54 particles[i] = (Particle){0};
55 }
56 particleCount = 0;
57 }
58
59 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
60 {
61 if (particleCount >= PARTICLE_MAX_COUNT)
62 {
63 return;
64 }
65
66 int index = -1;
67 for (int i = 0; i < particleCount; i++)
68 {
69 if (particles[i].particleType == PARTICLE_TYPE_NONE)
70 {
71 index = i;
72 break;
73 }
74 }
75
76 if (index == -1)
77 {
78 index = particleCount++;
79 }
80
81 Particle *particle = &particles[index];
82 particle->particleType = particleType;
83 particle->spawnTime = gameTime.time;
84 particle->lifetime = lifetime;
85 particle->position = position;
86 particle->velocity = velocity;
87 }
88
89 void ParticleUpdate()
90 {
91 for (int i = 0; i < particleCount; i++)
92 {
93 Particle *particle = &particles[i];
94 if (particle->particleType == PARTICLE_TYPE_NONE)
95 {
96 continue;
97 }
98
99 float age = gameTime.time - particle->spawnTime;
100
101 if (particle->lifetime > age)
102 {
103 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
104 }
105 else {
106 particle->particleType = PARTICLE_TYPE_NONE;
107 }
108 }
109 }
110
111 void DrawExplosionParticle(Particle *particle, float transition)
112 {
113 float size = 1.2f * (1.0f - transition);
114 Color startColor = WHITE;
115 Color endColor = RED;
116 Color color = ColorLerp(startColor, endColor, transition);
117 DrawCube(particle->position, size, size, size, color);
118 }
119
120 void ParticleDraw()
121 {
122 for (int i = 0; i < particleCount; i++)
123 {
124 Particle particle = particles[i];
125 if (particle.particleType == PARTICLE_TYPE_NONE)
126 {
127 continue;
128 }
129
130 float age = gameTime.time - particle.spawnTime;
131 float transition = age / particle.lifetime;
132 switch (particle.particleType)
133 {
134 case PARTICLE_TYPE_EXPLOSION:
135 DrawExplosionParticle(&particle, transition);
136 break;
137 default:
138 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
139 break;
140 }
141 }
142 }
143
144 //# Projectiles
145 Projectile projectiles[PROJECTILE_MAX_COUNT];
146 int projectileCount = 0;
147
148 void ProjectileInit()
149 {
150 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
151 {
152 projectiles[i] = (Projectile){0};
153 }
154 }
155
156 void ProjectileDraw()
157 {
158 for (int i = 0; i < projectileCount; i++)
159 {
160 Projectile projectile = projectiles[i];
161 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
162 {
163 continue;
164 }
165 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
166 if (transition >= 1.0f)
167 {
168 continue;
169 }
170 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
171 float x = position.x;
172 float y = position.y;
173 float dx = projectile.directionNormal.x;
174 float dy = projectile.directionNormal.y;
175 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
176 {
177 x -= dx * 0.1f;
178 y -= dy * 0.1f;
179 float size = 0.1f * d;
180 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
181 }
182 }
183 }
184
185 void ProjectileUpdate()
186 {
187 for (int i = 0; i < projectileCount; i++)
188 {
189 Projectile *projectile = &projectiles[i];
190 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
191 {
192 continue;
193 }
194 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
195 if (transition >= 1.0f)
196 {
197 projectile->projectileType = PROJECTILE_TYPE_NONE;
198 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
199 if (enemy)
200 {
201 EnemyAddDamage(enemy, projectile->damage);
202 }
203 continue;
204 }
205 }
206 }
207
208 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
209 {
210 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
211 {
212 Projectile *projectile = &projectiles[i];
213 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
214 {
215 projectile->projectileType = projectileType;
216 projectile->shootTime = gameTime.time;
217 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
218 projectile->damage = damage;
219 projectile->position = position;
220 projectile->target = target;
221 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
222 projectile->targetEnemy = EnemyGetId(enemy);
223 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
224 return projectile;
225 }
226 }
227 return 0;
228 }
229
230 //# Towers
231
232 void TowerInit()
233 {
234 for (int i = 0; i < TOWER_MAX_COUNT; i++)
235 {
236 towers[i] = (Tower){0};
237 }
238 towerCount = 0;
239 }
240
241 Tower *TowerGetAt(int16_t x, int16_t y)
242 {
243 for (int i = 0; i < towerCount; i++)
244 {
245 if (towers[i].x == x && towers[i].y == y)
246 {
247 return &towers[i];
248 }
249 }
250 return 0;
251 }
252
253 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
254 {
255 if (towerCount >= TOWER_MAX_COUNT)
256 {
257 return 0;
258 }
259
260 Tower *tower = TowerGetAt(x, y);
261 if (tower)
262 {
263 return 0;
264 }
265
266 tower = &towers[towerCount++];
267 tower->x = x;
268 tower->y = y;
269 tower->towerType = towerType;
270 tower->cooldown = 0.0f;
271 tower->damage = 0.0f;
272 return tower;
273 }
274
275 Tower *GetTowerByType(uint8_t towerType)
276 {
277 for (int i = 0; i < towerCount; i++)
278 {
279 if (towers[i].towerType == towerType)
280 {
281 return &towers[i];
282 }
283 }
284 return 0;
285 }
286
287 int GetTowerCosts(uint8_t towerType)
288 {
289 switch (towerType)
290 {
291 case TOWER_TYPE_BASE:
292 return 0;
293 case TOWER_TYPE_GUN:
294 return 6;
295 case TOWER_TYPE_WALL:
296 return 2;
297 }
298 return 0;
299 }
300
301 float TowerGetMaxHealth(Tower *tower)
302 {
303 switch (tower->towerType)
304 {
305 case TOWER_TYPE_BASE:
306 return 10.0f;
307 case TOWER_TYPE_GUN:
308 return 3.0f;
309 case TOWER_TYPE_WALL:
310 return 5.0f;
311 }
312 return 0.0f;
313 }
314
315 void TowerDraw()
316 {
317 for (int i = 0; i < towerCount; i++)
318 {
319 Tower tower = towers[i];
320 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
321 switch (tower.towerType)
322 {
323 case TOWER_TYPE_BASE:
324 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
325 break;
326 case TOWER_TYPE_GUN:
327 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
328 break;
329 case TOWER_TYPE_WALL:
330 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
331 break;
332 }
333 }
334 }
335
336 void TowerGunUpdate(Tower *tower)
337 {
338 if (tower->cooldown <= 0)
339 {
340 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
341 if (enemy)
342 {
343 tower->cooldown = 0.125f;
344 // shoot the enemy; determine future position of the enemy
345 float bulletSpeed = 1.0f;
346 float bulletDamage = 3.0f;
347 Vector2 velocity = enemy->simVelocity;
348 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
349 Vector2 towerPosition = {tower->x, tower->y};
350 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
351 for (int i = 0; i < 8; i++) {
352 velocity = enemy->simVelocity;
353 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
354 float distance = Vector2Distance(towerPosition, futurePosition);
355 float eta2 = distance / bulletSpeed;
356 if (fabs(eta - eta2) < 0.01f) {
357 break;
358 }
359 eta = (eta2 + eta) * 0.5f;
360 }
361 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
362 bulletSpeed, bulletDamage);
363 enemy->futureDamage += bulletDamage;
364 }
365 }
366 else
367 {
368 tower->cooldown -= gameTime.deltaTime;
369 }
370 }
371
372 void TowerUpdate()
373 {
374 for (int i = 0; i < towerCount; i++)
375 {
376 Tower *tower = &towers[i];
377 switch (tower->towerType)
378 {
379 case TOWER_TYPE_GUN:
380 TowerGunUpdate(tower);
381 break;
382 }
383 }
384 }
385
386 //# Game
387
388 void InitLevel(Level *level)
389 {
390 TowerInit();
391 EnemyInit();
392 ProjectileInit();
393 ParticleInit();
394 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
395
396 level->placementMode = 0;
397 level->state = LEVEL_STATE_BUILDING;
398 level->nextState = LEVEL_STATE_NONE;
399 level->playerGold = level->initialGold;
400
401 Camera *camera = &level->camera;
402 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
403 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
404 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
405 camera->fovy = 45.0f;
406 camera->projection = CAMERA_PERSPECTIVE;
407 }
408
409 void DrawLevelHud(Level *level)
410 {
411 const char *text = TextFormat("Gold: %d", level->playerGold);
412 Font font = GetFontDefault();
413 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
414 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
415 }
416
417 void DrawLevelReportLostWave(Level *level)
418 {
419 BeginMode3D(level->camera);
420 DrawGrid(10, 1.0f);
421 TowerDraw();
422 EnemyDraw();
423 ProjectileDraw();
424 ParticleDraw();
425 guiState.isBlocked = 0;
426 EndMode3D();
427
428 const char *text = "Wave lost";
429 int textWidth = MeasureText(text, 20);
430 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
431
432 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
433 {
434 level->nextState = LEVEL_STATE_RESET;
435 }
436 }
437
438 int HasLevelNextWave(Level *level)
439 {
440 for (int i = 0; i < 10; i++)
441 {
442 EnemyWave *wave = &level->waves[i];
443 if (wave->wave == level->currentWave)
444 {
445 return 1;
446 }
447 }
448 return 0;
449 }
450
451 void DrawLevelReportWonWave(Level *level)
452 {
453 BeginMode3D(level->camera);
454 DrawGrid(10, 1.0f);
455 TowerDraw();
456 EnemyDraw();
457 ProjectileDraw();
458 ParticleDraw();
459 guiState.isBlocked = 0;
460 EndMode3D();
461
462 const char *text = "Wave won";
463 int textWidth = MeasureText(text, 20);
464 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
465
466
467 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
468 {
469 level->nextState = LEVEL_STATE_RESET;
470 }
471
472 if (HasLevelNextWave(level))
473 {
474 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
475 {
476 level->nextState = LEVEL_STATE_BUILDING;
477 }
478 }
479 else {
480 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
481 {
482 level->nextState = LEVEL_STATE_WON_LEVEL;
483 }
484 }
485 }
486
487 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
488 {
489 static ButtonState buttonStates[8] = {0};
490 int cost = GetTowerCosts(towerType);
491 const char *text = TextFormat("%s: %d", name, cost);
492 buttonStates[towerType].isSelected = level->placementMode == towerType;
493 buttonStates[towerType].isDisabled = level->playerGold < cost;
494 if (Button(text, x, y, width, height, &buttonStates[towerType]))
495 {
496 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
497 }
498 }
499
500 void DrawLevelBuildingState(Level *level)
501 {
502 BeginMode3D(level->camera);
503 DrawGrid(10, 1.0f);
504 TowerDraw();
505 EnemyDraw();
506 ProjectileDraw();
507 ParticleDraw();
508
509 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
510 float planeDistance = ray.position.y / -ray.direction.y;
511 float planeX = ray.direction.x * planeDistance + ray.position.x;
512 float planeY = ray.direction.z * planeDistance + ray.position.z;
513 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
514 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
515 if (level->placementMode && !guiState.isBlocked)
516 {
517 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
518 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
519 {
520 if (TowerTryAdd(level->placementMode, mapX, mapY))
521 {
522 level->playerGold -= GetTowerCosts(level->placementMode);
523 level->placementMode = TOWER_TYPE_NONE;
524 }
525 }
526 }
527
528 guiState.isBlocked = 0;
529
530 EndMode3D();
531
532 static ButtonState buildWallButtonState = {0};
533 static ButtonState buildGunButtonState = {0};
534 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
535 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
536
537 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
538 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
539
540 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
541 {
542 level->nextState = LEVEL_STATE_RESET;
543 }
544
545 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
546 {
547 level->nextState = LEVEL_STATE_BATTLE;
548 }
549
550 const char *text = "Building phase";
551 int textWidth = MeasureText(text, 20);
552 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
553 }
554
555 void InitBattleStateConditions(Level *level)
556 {
557 level->state = LEVEL_STATE_BATTLE;
558 level->nextState = LEVEL_STATE_NONE;
559 level->waveEndTimer = 0.0f;
560 for (int i = 0; i < 10; i++)
561 {
562 EnemyWave *wave = &level->waves[i];
563 wave->spawned = 0;
564 wave->timeToSpawnNext = wave->delay;
565 }
566 }
567
568 void DrawLevelBattleState(Level *level)
569 {
570 BeginMode3D(level->camera);
571 DrawGrid(10, 1.0f);
572 TowerDraw();
573 EnemyDraw();
574 ProjectileDraw();
575 ParticleDraw();
576 guiState.isBlocked = 0;
577 EndMode3D();
578
579 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
580 {
581 level->nextState = LEVEL_STATE_RESET;
582 }
583
584 int maxCount = 0;
585 int remainingCount = 0;
586 for (int i = 0; i < 10; i++)
587 {
588 EnemyWave *wave = &level->waves[i];
589 if (wave->wave != level->currentWave)
590 {
591 continue;
592 }
593 maxCount += wave->count;
594 remainingCount += wave->count - wave->spawned;
595 }
596 int aliveCount = EnemyCount();
597 remainingCount += aliveCount;
598
599 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
600 int textWidth = MeasureText(text, 20);
601 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
602 }
603
604 void DrawLevel(Level *level)
605 {
606 switch (level->state)
607 {
608 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
609 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
610 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
611 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
612 default: break;
613 }
614
615 DrawLevelHud(level);
616 }
617
618 void UpdateLevel(Level *level)
619 {
620 if (level->state == LEVEL_STATE_BATTLE)
621 {
622 int activeWaves = 0;
623 for (int i = 0; i < 10; i++)
624 {
625 EnemyWave *wave = &level->waves[i];
626 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
627 {
628 continue;
629 }
630 activeWaves++;
631 wave->timeToSpawnNext -= gameTime.deltaTime;
632 if (wave->timeToSpawnNext <= 0.0f)
633 {
634 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
635 if (enemy)
636 {
637 wave->timeToSpawnNext = wave->interval;
638 wave->spawned++;
639 }
640 }
641 }
642 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
643 level->waveEndTimer += gameTime.deltaTime;
644 if (level->waveEndTimer >= 2.0f)
645 {
646 level->nextState = LEVEL_STATE_LOST_WAVE;
647 }
648 }
649 else if (activeWaves == 0 && EnemyCount() == 0)
650 {
651 level->waveEndTimer += gameTime.deltaTime;
652 if (level->waveEndTimer >= 2.0f)
653 {
654 level->nextState = LEVEL_STATE_WON_WAVE;
655 }
656 }
657 }
658
659 PathFindingMapUpdate();
660 EnemyUpdate();
661 TowerUpdate();
662 ProjectileUpdate();
663 ParticleUpdate();
664
665 if (level->nextState == LEVEL_STATE_RESET)
666 {
667 InitLevel(level);
668 }
669
670 if (level->nextState == LEVEL_STATE_BATTLE)
671 {
672 InitBattleStateConditions(level);
673 }
674
675 if (level->nextState == LEVEL_STATE_WON_WAVE)
676 {
677 level->currentWave++;
678 level->state = LEVEL_STATE_WON_WAVE;
679 }
680
681 if (level->nextState == LEVEL_STATE_LOST_WAVE)
682 {
683 level->state = LEVEL_STATE_LOST_WAVE;
684 }
685
686 if (level->nextState == LEVEL_STATE_BUILDING)
687 {
688 level->state = LEVEL_STATE_BUILDING;
689 }
690
691 if (level->nextState == LEVEL_STATE_WON_LEVEL)
692 {
693 // make something of this later
694 InitLevel(level);
695 }
696
697 level->nextState = LEVEL_STATE_NONE;
698 }
699
700 float nextSpawnTime = 0.0f;
701
702 void ResetGame()
703 {
704 InitLevel(currentLevel);
705 }
706
707 void InitGame()
708 {
709 TowerInit();
710 EnemyInit();
711 ProjectileInit();
712 ParticleInit();
713 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
714
715 currentLevel = levels;
716 InitLevel(currentLevel);
717 }
718
719 //# Immediate GUI functions
720
721 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
722 {
723 Rectangle bounds = {x, y, width, height};
724 int isPressed = 0;
725 int isSelected = state && state->isSelected;
726 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
727 {
728 Color color = isSelected ? DARKGRAY : GRAY;
729 DrawRectangle(x, y, width, height, color);
730 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
731 {
732 isPressed = 1;
733 }
734 guiState.isBlocked = 1;
735 }
736 else
737 {
738 Color color = isSelected ? WHITE : LIGHTGRAY;
739 DrawRectangle(x, y, width, height, color);
740 }
741 Font font = GetFontDefault();
742 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
743 Color textColor = state->isDisabled ? GRAY : BLACK;
744 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
745 return isPressed;
746 }
747
748 //# Main game loop
749
750 void GameUpdate()
751 {
752 float dt = GetFrameTime();
753 // cap maximum delta time to 0.1 seconds to prevent large time steps
754 if (dt > 0.1f) dt = 0.1f;
755 gameTime.time += dt;
756 gameTime.deltaTime = dt;
757
758 UpdateLevel(currentLevel);
759 }
760
761 int main(void)
762 {
763 int screenWidth, screenHeight;
764 GetPreferredSize(&screenWidth, &screenHeight);
765 InitWindow(screenWidth, screenHeight, "Tower defense");
766 SetTargetFPS(30);
767
768 InitGame();
769
770 while (!WindowShouldClose())
771 {
772 if (IsPaused()) {
773 // canvas is not visible in browser - do nothing
774 continue;
775 }
776
777 BeginDrawing();
778 ClearBackground(DARKBLUE);
779
780 GameUpdate();
781 DrawLevel(currentLevel);
782
783 EndDrawing();
784 }
785
786 CloseWindow();
787
788 return 0;
789 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 } Particle;
28
29 #define TOWER_MAX_COUNT 400
30 #define TOWER_TYPE_NONE 0
31 #define TOWER_TYPE_BASE 1
32 #define TOWER_TYPE_GUN 2
33 #define TOWER_TYPE_WALL 3
34
35 typedef struct Tower
36 {
37 int16_t x, y;
38 uint8_t towerType;
39 float cooldown;
40 float damage;
41 } Tower;
42
43 typedef struct GameTime
44 {
45 float time;
46 float deltaTime;
47 } GameTime;
48
49 typedef struct ButtonState {
50 char isSelected;
51 char isDisabled;
52 } ButtonState;
53
54 typedef struct GUIState {
55 int isBlocked;
56 } GUIState;
57
58 typedef enum LevelState
59 {
60 LEVEL_STATE_NONE,
61 LEVEL_STATE_BUILDING,
62 LEVEL_STATE_BATTLE,
63 LEVEL_STATE_WON_WAVE,
64 LEVEL_STATE_LOST_WAVE,
65 LEVEL_STATE_WON_LEVEL,
66 LEVEL_STATE_RESET,
67 } LevelState;
68
69 typedef struct EnemyWave {
70 uint8_t enemyType;
71 uint8_t wave;
72 uint16_t count;
73 float interval;
74 float delay;
75 Vector2 spawnPosition;
76
77 uint16_t spawned;
78 float timeToSpawnNext;
79 } EnemyWave;
80
81 typedef struct Level
82 {
83 LevelState state;
84 LevelState nextState;
85 Camera3D camera;
86 int placementMode;
87
88 int initialGold;
89 int playerGold;
90
91 EnemyWave waves[10];
92 int currentWave;
93 float waveEndTimer;
94 } Level;
95
96 typedef struct DeltaSrc
97 {
98 char x, y;
99 } DeltaSrc;
100
101 typedef struct PathfindingMap
102 {
103 int width, height;
104 float scale;
105 float *distances;
106 long *towerIndex;
107 DeltaSrc *deltaSrc;
108 float maxDistance;
109 Matrix toMapSpace;
110 Matrix toWorldSpace;
111 } PathfindingMap;
112
113 // when we execute the pathfinding algorithm, we need to store the active nodes
114 // in a queue. Each node has a position, a distance from the start, and the
115 // position of the node that we came from.
116 typedef struct PathfindingNode
117 {
118 int16_t x, y, fromX, fromY;
119 float distance;
120 } PathfindingNode;
121
122 typedef struct EnemyId
123 {
124 uint16_t index;
125 uint16_t generation;
126 } EnemyId;
127
128 typedef struct EnemyClassConfig
129 {
130 float speed;
131 float health;
132 float radius;
133 float maxAcceleration;
134 float requiredContactTime;
135 float explosionDamage;
136 float explosionRange;
137 float explosionPushbackPower;
138 int goldValue;
139 } EnemyClassConfig;
140
141 typedef struct Enemy
142 {
143 int16_t currentX, currentY;
144 int16_t nextX, nextY;
145 Vector2 simPosition;
146 Vector2 simVelocity;
147 uint16_t generation;
148 float startMovingTime;
149 float damage, futureDamage;
150 float contactTime;
151 uint8_t enemyType;
152 uint8_t movePathCount;
153 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
154 } Enemy;
155
156 #define PROJECTILE_MAX_COUNT 1200
157 #define PROJECTILE_TYPE_NONE 0
158 #define PROJECTILE_TYPE_BULLET 1
159
160 typedef struct Projectile
161 {
162 uint8_t projectileType;
163 float shootTime;
164 float arrivalTime;
165 float damage;
166 Vector2 position;
167 Vector2 target;
168 Vector2 directionNormal;
169 EnemyId targetEnemy;
170 } Projectile;
171
172 //# Function declarations
173 float TowerGetMaxHealth(Tower *tower);
174 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
175 int EnemyAddDamage(Enemy *enemy, float damage);
176
177 //# Enemy functions
178 void EnemyInit();
179 void EnemyDraw();
180 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
181 void EnemyUpdate();
182 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
183 float EnemyGetMaxHealth(Enemy *enemy);
184 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
185 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
186 EnemyId EnemyGetId(Enemy *enemy);
187 Enemy *EnemyTryResolve(EnemyId enemyId);
188 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
189 int EnemyAddDamage(Enemy *enemy, float damage);
190 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
191 int EnemyCount();
192
193 //# Particles
194 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
195
196 //# Pathfinding map
197 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
198 float PathFindingGetDistance(int mapX, int mapY);
199 Vector2 PathFindingGetGradient(Vector3 world);
200 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
201 void PathFindingMapUpdate();
202 void PathFindingMapDraw();
203
204 //# variables
205 extern Level *currentLevel;
206 extern Enemy enemies[ENEMY_MAX_COUNT];
207 extern int enemyCount;
208 extern EnemyClassConfig enemyClassConfigs[];
209
210 extern GUIState guiState;
211 extern GameTime gameTime;
212 extern Tower towers[TOWER_MAX_COUNT];
213 extern int towerCount;
214
215 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 3.0f,
9 .speed = 1.0f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 void EnemyInit()
24 {
25 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
26 {
27 enemies[i] = (Enemy){0};
28 }
29 enemyCount = 0;
30 }
31
32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
33 {
34 return enemyClassConfigs[enemy->enemyType].speed;
35 }
36
37 float EnemyGetMaxHealth(Enemy *enemy)
38 {
39 return enemyClassConfigs[enemy->enemyType].health;
40 }
41
42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
43 {
44 int16_t castleX = 0;
45 int16_t castleY = 0;
46 int16_t dx = castleX - currentX;
47 int16_t dy = castleY - currentY;
48 if (dx == 0 && dy == 0)
49 {
50 *nextX = currentX;
51 *nextY = currentY;
52 return 1;
53 }
54 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
55
56 if (gradient.x == 0 && gradient.y == 0)
57 {
58 *nextX = currentX;
59 *nextY = currentY;
60 return 1;
61 }
62
63 if (fabsf(gradient.x) > fabsf(gradient.y))
64 {
65 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
66 *nextY = currentY;
67 return 0;
68 }
69 *nextX = currentX;
70 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
71 return 0;
72 }
73
74
75 // this function predicts the movement of the unit for the next deltaT seconds
76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
77 {
78 const float pointReachedDistance = 0.25f;
79 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
80 const float maxSimStepTime = 0.015625f;
81
82 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
83 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
84 int16_t nextX = enemy->nextX;
85 int16_t nextY = enemy->nextY;
86 Vector2 position = enemy->simPosition;
87 int passedCount = 0;
88 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
89 {
90 float stepTime = fminf(deltaT - t, maxSimStepTime);
91 Vector2 target = (Vector2){nextX, nextY};
92 float speed = Vector2Length(*velocity);
93 // draw the target position for debugging
94 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
95 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
96 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
97 {
98 // we reached the target position, let's move to the next waypoint
99 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100 target = (Vector2){nextX, nextY};
101 // track how many waypoints we passed
102 passedCount++;
103 }
104
105 // acceleration towards the target
106 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108 *velocity = Vector2Add(*velocity, acceleration);
109
110 // limit the speed to the maximum speed
111 if (speed > maxSpeed)
112 {
113 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114 }
115
116 // move the enemy
117 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118 }
119
120 if (waypointPassedCount)
121 {
122 (*waypointPassedCount) = passedCount;
123 }
124
125 return position;
126 }
127
128 void EnemyDraw()
129 {
130 for (int i = 0; i < enemyCount; i++)
131 {
132 Enemy enemy = enemies[i];
133 if (enemy.enemyType == ENEMY_TYPE_NONE)
134 {
135 continue;
136 }
137
138 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139
140 if (enemy.movePathCount > 0)
141 {
142 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144 }
145 for (int j = 1; j < enemy.movePathCount; j++)
146 {
147 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149 DrawLine3D(p, q, GREEN);
150 }
151
152 switch (enemy.enemyType)
153 {
154 case ENEMY_TYPE_MINION:
155 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156 break;
157 }
158 }
159 }
160
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163 // damage the tower
164 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167 float explosionRange2 = explosionRange * explosionRange;
168 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169 // explode the enemy
170 if (tower->damage >= TowerGetMaxHealth(tower))
171 {
172 tower->towerType = TOWER_TYPE_NONE;
173 }
174
175 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
176 explosionSource,
177 (Vector3){0, 0.1f, 0}, 1.0f);
178
179 enemy->enemyType = ENEMY_TYPE_NONE;
180
181 // push back enemies & dealing damage
182 for (int i = 0; i < enemyCount; i++)
183 {
184 Enemy *other = &enemies[i];
185 if (other->enemyType == ENEMY_TYPE_NONE)
186 {
187 continue;
188 }
189 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190 if (distanceSqr > 0 && distanceSqr < explosionRange2)
191 {
192 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194 EnemyAddDamage(other, explosionDamge);
195 }
196 }
197 }
198
199 void EnemyUpdate()
200 {
201 const float castleX = 0;
202 const float castleY = 0;
203 const float maxPathDistance2 = 0.25f * 0.25f;
204
205 for (int i = 0; i < enemyCount; i++)
206 {
207 Enemy *enemy = &enemies[i];
208 if (enemy->enemyType == ENEMY_TYPE_NONE)
209 {
210 continue;
211 }
212
213 int waypointPassedCount = 0;
214 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215 enemy->startMovingTime = gameTime.time;
216 // track path of unit
217 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218 {
219 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220 {
221 enemy->movePath[j] = enemy->movePath[j - 1];
222 }
223 enemy->movePath[0] = enemy->simPosition;
224 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225 {
226 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227 }
228 }
229
230 if (waypointPassedCount > 0)
231 {
232 enemy->currentX = enemy->nextX;
233 enemy->currentY = enemy->nextY;
234 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236 {
237 // enemy reached the castle; remove it
238 enemy->enemyType = ENEMY_TYPE_NONE;
239 continue;
240 }
241 }
242 }
243
244 // handle collisions between enemies
245 for (int i = 0; i < enemyCount - 1; i++)
246 {
247 Enemy *enemyA = &enemies[i];
248 if (enemyA->enemyType == ENEMY_TYPE_NONE)
249 {
250 continue;
251 }
252 for (int j = i + 1; j < enemyCount; j++)
253 {
254 Enemy *enemyB = &enemies[j];
255 if (enemyB->enemyType == ENEMY_TYPE_NONE)
256 {
257 continue;
258 }
259 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262 float radiusSum = radiusA + radiusB;
263 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264 {
265 // collision
266 float distance = sqrtf(distanceSqr);
267 float overlap = radiusSum - distance;
268 // move the enemies apart, but softly; if we have a clog of enemies,
269 // moving them perfectly apart can cause them to jitter
270 float positionCorrection = overlap / 5.0f;
271 Vector2 direction = (Vector2){
272 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276 }
277 }
278 }
279
280 // handle collisions between enemies and towers
281 for (int i = 0; i < enemyCount; i++)
282 {
283 Enemy *enemy = &enemies[i];
284 if (enemy->enemyType == ENEMY_TYPE_NONE)
285 {
286 continue;
287 }
288 enemy->contactTime -= gameTime.deltaTime;
289 if (enemy->contactTime < 0.0f)
290 {
291 enemy->contactTime = 0.0f;
292 }
293
294 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295 // linear search over towers; could be optimized by using path finding tower map,
296 // but for now, we keep it simple
297 for (int j = 0; j < towerCount; j++)
298 {
299 Tower *tower = &towers[j];
300 if (tower->towerType == TOWER_TYPE_NONE)
301 {
302 continue;
303 }
304 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306 if (distanceSqr > combinedRadius * combinedRadius)
307 {
308 continue;
309 }
310 // potential collision; square / circle intersection
311 float dx = tower->x - enemy->simPosition.x;
312 float dy = tower->y - enemy->simPosition.y;
313 float absDx = fabsf(dx);
314 float absDy = fabsf(dy);
315 Vector3 contactPoint = {0};
316 if (absDx <= 0.5f && absDx <= absDy) {
317 // vertical collision; push the enemy out horizontally
318 float overlap = enemyRadius + 0.5f - absDy;
319 if (overlap < 0.0f)
320 {
321 continue;
322 }
323 float direction = dy > 0.0f ? -1.0f : 1.0f;
324 enemy->simPosition.y += direction * overlap;
325 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
326 }
327 else if (absDy <= 0.5f && absDy <= absDx)
328 {
329 // horizontal collision; push the enemy out vertically
330 float overlap = enemyRadius + 0.5f - absDx;
331 if (overlap < 0.0f)
332 {
333 continue;
334 }
335 float direction = dx > 0.0f ? -1.0f : 1.0f;
336 enemy->simPosition.x += direction * overlap;
337 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
338 }
339 else
340 {
341 // possible collision with a corner
342 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
343 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
344 float cornerX = tower->x + cornerDX;
345 float cornerY = tower->y + cornerDY;
346 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
347 if (cornerDistanceSqr > enemyRadius * enemyRadius)
348 {
349 continue;
350 }
351 // push the enemy out along the diagonal
352 float cornerDistance = sqrtf(cornerDistanceSqr);
353 float overlap = enemyRadius - cornerDistance;
354 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
355 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
356 enemy->simPosition.x -= directionX * overlap;
357 enemy->simPosition.y -= directionY * overlap;
358 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
359 }
360
361 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
362 {
363 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
364 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
365 {
366 EnemyTriggerExplode(enemy, tower, contactPoint);
367 }
368 }
369 }
370 }
371 }
372
373 EnemyId EnemyGetId(Enemy *enemy)
374 {
375 return (EnemyId){enemy - enemies, enemy->generation};
376 }
377
378 Enemy *EnemyTryResolve(EnemyId enemyId)
379 {
380 if (enemyId.index >= ENEMY_MAX_COUNT)
381 {
382 return 0;
383 }
384 Enemy *enemy = &enemies[enemyId.index];
385 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
386 {
387 return 0;
388 }
389 return enemy;
390 }
391
392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
393 {
394 Enemy *spawn = 0;
395 for (int i = 0; i < enemyCount; i++)
396 {
397 Enemy *enemy = &enemies[i];
398 if (enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 spawn = enemy;
401 break;
402 }
403 }
404
405 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
406 {
407 spawn = &enemies[enemyCount++];
408 }
409
410 if (spawn)
411 {
412 spawn->currentX = currentX;
413 spawn->currentY = currentY;
414 spawn->nextX = currentX;
415 spawn->nextY = currentY;
416 spawn->simPosition = (Vector2){currentX, currentY};
417 spawn->simVelocity = (Vector2){0, 0};
418 spawn->enemyType = enemyType;
419 spawn->startMovingTime = gameTime.time;
420 spawn->damage = 0.0f;
421 spawn->futureDamage = 0.0f;
422 spawn->generation++;
423 spawn->movePathCount = 0;
424 }
425
426 return spawn;
427 }
428
429 int EnemyAddDamage(Enemy *enemy, float damage)
430 {
431 enemy->damage += damage;
432 if (enemy->damage >= EnemyGetMaxHealth(enemy))
433 {
434 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
435 enemy->enemyType = ENEMY_TYPE_NONE;
436 return 1;
437 }
438
439 return 0;
440 }
441
442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
443 {
444 int16_t castleX = 0;
445 int16_t castleY = 0;
446 Enemy* closest = 0;
447 int16_t closestDistance = 0;
448 float range2 = range * range;
449 for (int i = 0; i < enemyCount; i++)
450 {
451 Enemy* enemy = &enemies[i];
452 if (enemy->enemyType == ENEMY_TYPE_NONE)
453 {
454 continue;
455 }
456 float maxHealth = EnemyGetMaxHealth(enemy);
457 if (enemy->futureDamage >= maxHealth)
458 {
459 // ignore enemies that will die soon
460 continue;
461 }
462 int16_t dx = castleX - enemy->currentX;
463 int16_t dy = castleY - enemy->currentY;
464 int16_t distance = abs(dx) + abs(dy);
465 if (!closest || distance < closestDistance)
466 {
467 float tdx = towerX - enemy->currentX;
468 float tdy = towerY - enemy->currentY;
469 float tdistance2 = tdx * tdx + tdy * tdy;
470 if (tdistance2 <= range2)
471 {
472 closest = enemy;
473 closestDistance = distance;
474 }
475 }
476 }
477 return closest;
478 }
479
480 int EnemyCount()
481 {
482 int count = 0;
483 for (int i = 0; i < enemyCount; i++)
484 {
485 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
486 {
487 count++;
488 }
489 }
490 return count;
491 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate()
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < towerCount; i++)
131 {
132 Tower *tower = &towers[i];
133 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134 {
135 continue;
136 }
137 int16_t mapX, mapY;
138 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139 // this would not work correctly and needs to be refined to allow towers covering multiple cells
140 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141 // one cell. For now.
142 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143 {
144 continue;
145 }
146 int index = mapY * width + mapX;
147 pathfindingMap.towerIndex[index] = i;
148 }
149
150 // we start at the castle and add the castle to the queue
151 pathfindingMap.maxDistance = 0.0f;
152 pathfindingNodeQueueCount = 0;
153 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154 PathfindingNode *node = 0;
155 while ((node = PathFindingNodePop()))
156 {
157 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158 {
159 continue;
160 }
161 int index = node->y * width + node->x;
162 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163 {
164 continue;
165 }
166
167 int deltaX = node->x - node->fromX;
168 int deltaY = node->y - node->fromY;
169 // even if the cell is blocked by a tower, we still may want to store the direction
170 // (though this might not be needed, IDK right now)
171 pathfindingMap.deltaSrc[index].x = (char) deltaX;
172 pathfindingMap.deltaSrc[index].y = (char) deltaY;
173
174 // we skip nodes that are blocked by towers
175 if (pathfindingMap.towerIndex[index] >= 0)
176 {
177 node->distance += 8.0f;
178 }
179 pathfindingMap.distances[index] = node->distance;
180 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185 }
186 }
187
188 void PathFindingMapDraw()
189 {
190 float cellSize = pathfindingMap.scale * 0.9f;
191 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192 for (int x = 0; x < pathfindingMap.width; x++)
193 {
194 for (int y = 0; y < pathfindingMap.height; y++)
195 {
196 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200 // animate the distance "wave" to show how the pathfinding algorithm expands
201 // from the castle
202 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203 {
204 color = BLACK;
205 }
206 DrawCube(position, cellSize, 0.1f, cellSize, color);
207 }
208 }
209 }
210
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213 int16_t mapX, mapY;
214 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215 {
216 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217 return (Vector2){(float)-delta.x, (float)-delta.y};
218 }
219 // fallback to a simple gradient calculation
220 float n = PathFindingGetDistance(mapX, mapY - 1);
221 float s = PathFindingGetDistance(mapX, mapY + 1);
222 float w = PathFindingGetDistance(mapX - 1, mapY);
223 float e = PathFindingGetDistance(mapX + 1, mapY);
224 return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
1 #include "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 path finding module is interesting to look at, because a few decisions may require a bit of explaining:
1 // The queue is a simple array of nodes, we add nodes to the end and remove
2 // nodes from the front. We keep the array around to avoid unnecessary allocations
3 static PathfindingNode *pathfindingNodeQueue = 0;
4 static int pathfindingNodeQueueCount = 0;
5 static int pathfindingNodeQueueCapacity = 0;
6
7 // The pathfinding map stores the distances from the castle to each cell in the map.
8 static PathfindingMap pathfindingMap = {0};
The variables are now all declared as static.
When I structure a C file module, I try to put the global variables at the top of the file (optimally: none - but for the sake of simplicity in this tutorial, I am using them more often than I would in a "real" project). A global variable is a variable that is accessible from all functions in the project - however, static variables are only accessible from the file they are declared in. Non static variables are accessible from other files that know of their existence via declaration ("extern ...").
In this case, all the global variables that the path finding module uses are static - which is a bit better than a global variable that is accessible from all files. Using global variables is not wrong per se, but the fewer the better and the smaller their scope the better.
There are also a few static functions that are only used in this file. My strategy is usually to avoid forward declarations of functions if possible, so I put static functions at the top of the file. This way, the functions are declared before they are used just naturally. If there is some kind of initialization function, I try to put it at the very top of the file, just because I regard it as a central function that plays an important role in the module.
Let's move on with extracting modules from the main file:
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Variables
7 GUIState guiState = {0};
8 GameTime gameTime = {0};
9 Tower towers[TOWER_MAX_COUNT];
10 int towerCount = 0;
11
12 Level levels[] = {
13 [0] = {
14 .state = LEVEL_STATE_BUILDING,
15 .initialGold = 10,
16 .waves[0] = {
17 .enemyType = ENEMY_TYPE_MINION,
18 .wave = 0,
19 .count = 10,
20 .interval = 1.0f,
21 .delay = 1.0f,
22 .spawnPosition = {0, 6},
23 },
24 .waves[1] = {
25 .enemyType = ENEMY_TYPE_MINION,
26 .wave = 1,
27 .count = 20,
28 .interval = 0.5f,
29 .delay = 1.0f,
30 .spawnPosition = {0, 6},
31 },
32 .waves[2] = {
33 .enemyType = ENEMY_TYPE_MINION,
34 .wave = 2,
35 .count = 30,
36 .interval = 0.25f,
37 .delay = 1.0f,
38 .spawnPosition = {0, 6},
39 }
40 },
41 };
42
43 Level *currentLevel = levels;
44
45 //# Towers
46
47 void TowerInit()
48 {
49 for (int i = 0; i < TOWER_MAX_COUNT; i++)
50 {
51 towers[i] = (Tower){0};
52 }
53 towerCount = 0;
54 }
55
56 Tower *TowerGetAt(int16_t x, int16_t y)
57 {
58 for (int i = 0; i < towerCount; i++)
59 {
60 if (towers[i].x == x && towers[i].y == y)
61 {
62 return &towers[i];
63 }
64 }
65 return 0;
66 }
67
68 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
69 {
70 if (towerCount >= TOWER_MAX_COUNT)
71 {
72 return 0;
73 }
74
75 Tower *tower = TowerGetAt(x, y);
76 if (tower)
77 {
78 return 0;
79 }
80
81 tower = &towers[towerCount++];
82 tower->x = x;
83 tower->y = y;
84 tower->towerType = towerType;
85 tower->cooldown = 0.0f;
86 tower->damage = 0.0f;
87 return tower;
88 }
89
90 Tower *GetTowerByType(uint8_t towerType)
91 {
92 for (int i = 0; i < towerCount; i++)
93 {
94 if (towers[i].towerType == towerType)
95 {
96 return &towers[i];
97 }
98 }
99 return 0;
100 }
101
102 int GetTowerCosts(uint8_t towerType)
103 {
104 switch (towerType)
105 {
106 case TOWER_TYPE_BASE:
107 return 0;
108 case TOWER_TYPE_GUN:
109 return 6;
110 case TOWER_TYPE_WALL:
111 return 2;
112 }
113 return 0;
114 }
115
116 float TowerGetMaxHealth(Tower *tower)
117 {
118 switch (tower->towerType)
119 {
120 case TOWER_TYPE_BASE:
121 return 10.0f;
122 case TOWER_TYPE_GUN:
123 return 3.0f;
124 case TOWER_TYPE_WALL:
125 return 5.0f;
126 }
127 return 0.0f;
128 }
129
130 void TowerDraw()
131 {
132 for (int i = 0; i < towerCount; i++)
133 {
134 Tower tower = towers[i];
135 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
136 switch (tower.towerType)
137 {
138 case TOWER_TYPE_BASE:
139 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
140 break;
141 case TOWER_TYPE_GUN:
142 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
143 break;
144 case TOWER_TYPE_WALL:
145 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
146 break;
147 }
148 }
149 }
150
151 void TowerGunUpdate(Tower *tower)
152 {
153 if (tower->cooldown <= 0)
154 {
155 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
156 if (enemy)
157 {
158 tower->cooldown = 0.125f;
159 // shoot the enemy; determine future position of the enemy
160 float bulletSpeed = 1.0f;
161 float bulletDamage = 3.0f;
162 Vector2 velocity = enemy->simVelocity;
163 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
164 Vector2 towerPosition = {tower->x, tower->y};
165 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
166 for (int i = 0; i < 8; i++) {
167 velocity = enemy->simVelocity;
168 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
169 float distance = Vector2Distance(towerPosition, futurePosition);
170 float eta2 = distance / bulletSpeed;
171 if (fabs(eta - eta2) < 0.01f) {
172 break;
173 }
174 eta = (eta2 + eta) * 0.5f;
175 }
176 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
177 bulletSpeed, bulletDamage);
178 enemy->futureDamage += bulletDamage;
179 }
180 }
181 else
182 {
183 tower->cooldown -= gameTime.deltaTime;
184 }
185 }
186
187 void TowerUpdate()
188 {
189 for (int i = 0; i < towerCount; i++)
190 {
191 Tower *tower = &towers[i];
192 switch (tower->towerType)
193 {
194 case TOWER_TYPE_GUN:
195 TowerGunUpdate(tower);
196 break;
197 }
198 }
199 }
200
201 //# Game
202
203 void InitLevel(Level *level)
204 {
205 TowerInit();
206 EnemyInit();
207 ProjectileInit();
208 ParticleInit();
209 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
210
211 level->placementMode = 0;
212 level->state = LEVEL_STATE_BUILDING;
213 level->nextState = LEVEL_STATE_NONE;
214 level->playerGold = level->initialGold;
215
216 Camera *camera = &level->camera;
217 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
218 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
219 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
220 camera->fovy = 45.0f;
221 camera->projection = CAMERA_PERSPECTIVE;
222 }
223
224 void DrawLevelHud(Level *level)
225 {
226 const char *text = TextFormat("Gold: %d", level->playerGold);
227 Font font = GetFontDefault();
228 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
229 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
230 }
231
232 void DrawLevelReportLostWave(Level *level)
233 {
234 BeginMode3D(level->camera);
235 DrawGrid(10, 1.0f);
236 TowerDraw();
237 EnemyDraw();
238 ProjectileDraw();
239 ParticleDraw();
240 guiState.isBlocked = 0;
241 EndMode3D();
242
243 const char *text = "Wave lost";
244 int textWidth = MeasureText(text, 20);
245 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
246
247 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
248 {
249 level->nextState = LEVEL_STATE_RESET;
250 }
251 }
252
253 int HasLevelNextWave(Level *level)
254 {
255 for (int i = 0; i < 10; i++)
256 {
257 EnemyWave *wave = &level->waves[i];
258 if (wave->wave == level->currentWave)
259 {
260 return 1;
261 }
262 }
263 return 0;
264 }
265
266 void DrawLevelReportWonWave(Level *level)
267 {
268 BeginMode3D(level->camera);
269 DrawGrid(10, 1.0f);
270 TowerDraw();
271 EnemyDraw();
272 ProjectileDraw();
273 ParticleDraw();
274 guiState.isBlocked = 0;
275 EndMode3D();
276
277 const char *text = "Wave won";
278 int textWidth = MeasureText(text, 20);
279 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
280
281
282 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
283 {
284 level->nextState = LEVEL_STATE_RESET;
285 }
286
287 if (HasLevelNextWave(level))
288 {
289 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
290 {
291 level->nextState = LEVEL_STATE_BUILDING;
292 }
293 }
294 else {
295 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
296 {
297 level->nextState = LEVEL_STATE_WON_LEVEL;
298 }
299 }
300 }
301
302 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
303 {
304 static ButtonState buttonStates[8] = {0};
305 int cost = GetTowerCosts(towerType);
306 const char *text = TextFormat("%s: %d", name, cost);
307 buttonStates[towerType].isSelected = level->placementMode == towerType;
308 buttonStates[towerType].isDisabled = level->playerGold < cost;
309 if (Button(text, x, y, width, height, &buttonStates[towerType]))
310 {
311 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
312 }
313 }
314
315 void DrawLevelBuildingState(Level *level)
316 {
317 BeginMode3D(level->camera);
318 DrawGrid(10, 1.0f);
319 TowerDraw();
320 EnemyDraw();
321 ProjectileDraw();
322 ParticleDraw();
323
324 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
325 float planeDistance = ray.position.y / -ray.direction.y;
326 float planeX = ray.direction.x * planeDistance + ray.position.x;
327 float planeY = ray.direction.z * planeDistance + ray.position.z;
328 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
329 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
330 if (level->placementMode && !guiState.isBlocked)
331 {
332 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
333 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
334 {
335 if (TowerTryAdd(level->placementMode, mapX, mapY))
336 {
337 level->playerGold -= GetTowerCosts(level->placementMode);
338 level->placementMode = TOWER_TYPE_NONE;
339 }
340 }
341 }
342
343 guiState.isBlocked = 0;
344
345 EndMode3D();
346
347 static ButtonState buildWallButtonState = {0};
348 static ButtonState buildGunButtonState = {0};
349 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
350 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
351
352 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
353 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
354
355 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
356 {
357 level->nextState = LEVEL_STATE_RESET;
358 }
359
360 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
361 {
362 level->nextState = LEVEL_STATE_BATTLE;
363 }
364
365 const char *text = "Building phase";
366 int textWidth = MeasureText(text, 20);
367 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
368 }
369
370 void InitBattleStateConditions(Level *level)
371 {
372 level->state = LEVEL_STATE_BATTLE;
373 level->nextState = LEVEL_STATE_NONE;
374 level->waveEndTimer = 0.0f;
375 for (int i = 0; i < 10; i++)
376 {
377 EnemyWave *wave = &level->waves[i];
378 wave->spawned = 0;
379 wave->timeToSpawnNext = wave->delay;
380 }
381 }
382
383 void DrawLevelBattleState(Level *level)
384 {
385 BeginMode3D(level->camera);
386 DrawGrid(10, 1.0f);
387 TowerDraw();
388 EnemyDraw();
389 ProjectileDraw();
390 ParticleDraw();
391 guiState.isBlocked = 0;
392 EndMode3D();
393
394 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
395 {
396 level->nextState = LEVEL_STATE_RESET;
397 }
398
399 int maxCount = 0;
400 int remainingCount = 0;
401 for (int i = 0; i < 10; i++)
402 {
403 EnemyWave *wave = &level->waves[i];
404 if (wave->wave != level->currentWave)
405 {
406 continue;
407 }
408 maxCount += wave->count;
409 remainingCount += wave->count - wave->spawned;
410 }
411 int aliveCount = EnemyCount();
412 remainingCount += aliveCount;
413
414 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
415 int textWidth = MeasureText(text, 20);
416 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
417 }
418
419 void DrawLevel(Level *level)
420 {
421 switch (level->state)
422 {
423 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
424 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
425 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
426 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
427 default: break;
428 }
429
430 DrawLevelHud(level);
431 }
432
433 void UpdateLevel(Level *level)
434 {
435 if (level->state == LEVEL_STATE_BATTLE)
436 {
437 int activeWaves = 0;
438 for (int i = 0; i < 10; i++)
439 {
440 EnemyWave *wave = &level->waves[i];
441 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
442 {
443 continue;
444 }
445 activeWaves++;
446 wave->timeToSpawnNext -= gameTime.deltaTime;
447 if (wave->timeToSpawnNext <= 0.0f)
448 {
449 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
450 if (enemy)
451 {
452 wave->timeToSpawnNext = wave->interval;
453 wave->spawned++;
454 }
455 }
456 }
457 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
458 level->waveEndTimer += gameTime.deltaTime;
459 if (level->waveEndTimer >= 2.0f)
460 {
461 level->nextState = LEVEL_STATE_LOST_WAVE;
462 }
463 }
464 else if (activeWaves == 0 && EnemyCount() == 0)
465 {
466 level->waveEndTimer += gameTime.deltaTime;
467 if (level->waveEndTimer >= 2.0f)
468 {
469 level->nextState = LEVEL_STATE_WON_WAVE;
470 }
471 }
472 }
473
474 PathFindingMapUpdate();
475 EnemyUpdate();
476 TowerUpdate();
477 ProjectileUpdate();
478 ParticleUpdate();
479
480 if (level->nextState == LEVEL_STATE_RESET)
481 {
482 InitLevel(level);
483 }
484
485 if (level->nextState == LEVEL_STATE_BATTLE)
486 {
487 InitBattleStateConditions(level);
488 }
489
490 if (level->nextState == LEVEL_STATE_WON_WAVE)
491 {
492 level->currentWave++;
493 level->state = LEVEL_STATE_WON_WAVE;
494 }
495
496 if (level->nextState == LEVEL_STATE_LOST_WAVE)
497 {
498 level->state = LEVEL_STATE_LOST_WAVE;
499 }
500
501 if (level->nextState == LEVEL_STATE_BUILDING)
502 {
503 level->state = LEVEL_STATE_BUILDING;
504 }
505
506 if (level->nextState == LEVEL_STATE_WON_LEVEL)
507 {
508 // make something of this later
509 InitLevel(level);
510 }
511
512 level->nextState = LEVEL_STATE_NONE;
513 }
514
515 float nextSpawnTime = 0.0f;
516
517 void ResetGame()
518 {
519 InitLevel(currentLevel);
520 }
521
522 void InitGame()
523 {
524 TowerInit();
525 EnemyInit();
526 ProjectileInit();
527 ParticleInit();
528 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
529
530 currentLevel = levels;
531 InitLevel(currentLevel);
532 }
533
534 //# Immediate GUI functions
535
536 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
537 {
538 Rectangle bounds = {x, y, width, height};
539 int isPressed = 0;
540 int isSelected = state && state->isSelected;
541 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
542 {
543 Color color = isSelected ? DARKGRAY : GRAY;
544 DrawRectangle(x, y, width, height, color);
545 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
546 {
547 isPressed = 1;
548 }
549 guiState.isBlocked = 1;
550 }
551 else
552 {
553 Color color = isSelected ? WHITE : LIGHTGRAY;
554 DrawRectangle(x, y, width, height, color);
555 }
556 Font font = GetFontDefault();
557 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
558 Color textColor = state->isDisabled ? GRAY : BLACK;
559 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
560 return isPressed;
561 }
562
563 //# Main game loop
564
565 void GameUpdate()
566 {
567 float dt = GetFrameTime();
568 // cap maximum delta time to 0.1 seconds to prevent large time steps
569 if (dt > 0.1f) dt = 0.1f;
570 gameTime.time += dt;
571 gameTime.deltaTime = dt;
572
573 UpdateLevel(currentLevel);
574 }
575
576 int main(void)
577 {
578 int screenWidth, screenHeight;
579 GetPreferredSize(&screenWidth, &screenHeight);
580 InitWindow(screenWidth, screenHeight, "Tower defense");
581 SetTargetFPS(30);
582
583 InitGame();
584
585 while (!WindowShouldClose())
586 {
587 if (IsPaused()) {
588 // canvas is not visible in browser - do nothing
589 continue;
590 }
591
592 BeginDrawing();
593 ClearBackground(DARKBLUE);
594
595 GameUpdate();
596 DrawLevel(currentLevel);
597
598 EndDrawing();
599 }
600
601 CloseWindow();
602
603 return 0;
604 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 } Particle;
28
29 #define TOWER_MAX_COUNT 400
30 #define TOWER_TYPE_NONE 0
31 #define TOWER_TYPE_BASE 1
32 #define TOWER_TYPE_GUN 2
33 #define TOWER_TYPE_WALL 3
34
35 typedef struct Tower
36 {
37 int16_t x, y;
38 uint8_t towerType;
39 float cooldown;
40 float damage;
41 } Tower;
42
43 typedef struct GameTime
44 {
45 float time;
46 float deltaTime;
47 } GameTime;
48
49 typedef struct ButtonState {
50 char isSelected;
51 char isDisabled;
52 } ButtonState;
53
54 typedef struct GUIState {
55 int isBlocked;
56 } GUIState;
57
58 typedef enum LevelState
59 {
60 LEVEL_STATE_NONE,
61 LEVEL_STATE_BUILDING,
62 LEVEL_STATE_BATTLE,
63 LEVEL_STATE_WON_WAVE,
64 LEVEL_STATE_LOST_WAVE,
65 LEVEL_STATE_WON_LEVEL,
66 LEVEL_STATE_RESET,
67 } LevelState;
68
69 typedef struct EnemyWave {
70 uint8_t enemyType;
71 uint8_t wave;
72 uint16_t count;
73 float interval;
74 float delay;
75 Vector2 spawnPosition;
76
77 uint16_t spawned;
78 float timeToSpawnNext;
79 } EnemyWave;
80
81 typedef struct Level
82 {
83 LevelState state;
84 LevelState nextState;
85 Camera3D camera;
86 int placementMode;
87
88 int initialGold;
89 int playerGold;
90
91 EnemyWave waves[10];
92 int currentWave;
93 float waveEndTimer;
94 } Level;
95
96 typedef struct DeltaSrc
97 {
98 char x, y;
99 } DeltaSrc;
100
101 typedef struct PathfindingMap
102 {
103 int width, height;
104 float scale;
105 float *distances;
106 long *towerIndex;
107 DeltaSrc *deltaSrc;
108 float maxDistance;
109 Matrix toMapSpace;
110 Matrix toWorldSpace;
111 } PathfindingMap;
112
113 // when we execute the pathfinding algorithm, we need to store the active nodes
114 // in a queue. Each node has a position, a distance from the start, and the
115 // position of the node that we came from.
116 typedef struct PathfindingNode
117 {
118 int16_t x, y, fromX, fromY;
119 float distance;
120 } PathfindingNode;
121
122 typedef struct EnemyId
123 {
124 uint16_t index;
125 uint16_t generation;
126 } EnemyId;
127
128 typedef struct EnemyClassConfig
129 {
130 float speed;
131 float health;
132 float radius;
133 float maxAcceleration;
134 float requiredContactTime;
135 float explosionDamage;
136 float explosionRange;
137 float explosionPushbackPower;
138 int goldValue;
139 } EnemyClassConfig;
140
141 typedef struct Enemy
142 {
143 int16_t currentX, currentY;
144 int16_t nextX, nextY;
145 Vector2 simPosition;
146 Vector2 simVelocity;
147 uint16_t generation;
148 float startMovingTime;
149 float damage, futureDamage;
150 float contactTime;
151 uint8_t enemyType;
152 uint8_t movePathCount;
153 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
154 } Enemy;
155
156 #define PROJECTILE_MAX_COUNT 1200
157 #define PROJECTILE_TYPE_NONE 0
158 #define PROJECTILE_TYPE_BULLET 1
159
160 typedef struct Projectile
161 {
162 uint8_t projectileType;
163 float shootTime;
164 float arrivalTime;
165 float damage;
166 Vector2 position;
167 Vector2 target;
168 Vector2 directionNormal;
169 EnemyId targetEnemy;
170 } Projectile;
171
172 //# Function declarations
173 float TowerGetMaxHealth(Tower *tower);
174 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
175 int EnemyAddDamage(Enemy *enemy, float damage);
176
177 //# Enemy functions
178 void EnemyInit();
179 void EnemyDraw();
180 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
181 void EnemyUpdate();
182 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
183 float EnemyGetMaxHealth(Enemy *enemy);
184 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
185 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
186 EnemyId EnemyGetId(Enemy *enemy);
187 Enemy *EnemyTryResolve(EnemyId enemyId);
188 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
189 int EnemyAddDamage(Enemy *enemy, float damage);
190 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
191 int EnemyCount();
192
193 //# Particles
194 void ParticleInit();
195 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
196 void ParticleUpdate();
197 void ParticleDraw();
198
199 //# Projectiles
200 void ProjectileInit();
201 void ProjectileDraw();
202 void ProjectileUpdate();
203 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage);
204
205 //# Pathfinding map
206 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
207 float PathFindingGetDistance(int mapX, int mapY);
208 Vector2 PathFindingGetGradient(Vector3 world);
209 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
210 void PathFindingMapUpdate();
211 void PathFindingMapDraw();
212
213 //# variables
214 extern Level *currentLevel;
215 extern Enemy enemies[ENEMY_MAX_COUNT];
216 extern int enemyCount;
217 extern EnemyClassConfig enemyClassConfigs[];
218
219 extern GUIState guiState;
220 extern GameTime gameTime;
221 extern Tower towers[TOWER_MAX_COUNT];
222 extern int towerCount;
223
224 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 3.0f,
9 .speed = 1.0f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 void EnemyInit()
24 {
25 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
26 {
27 enemies[i] = (Enemy){0};
28 }
29 enemyCount = 0;
30 }
31
32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
33 {
34 return enemyClassConfigs[enemy->enemyType].speed;
35 }
36
37 float EnemyGetMaxHealth(Enemy *enemy)
38 {
39 return enemyClassConfigs[enemy->enemyType].health;
40 }
41
42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
43 {
44 int16_t castleX = 0;
45 int16_t castleY = 0;
46 int16_t dx = castleX - currentX;
47 int16_t dy = castleY - currentY;
48 if (dx == 0 && dy == 0)
49 {
50 *nextX = currentX;
51 *nextY = currentY;
52 return 1;
53 }
54 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
55
56 if (gradient.x == 0 && gradient.y == 0)
57 {
58 *nextX = currentX;
59 *nextY = currentY;
60 return 1;
61 }
62
63 if (fabsf(gradient.x) > fabsf(gradient.y))
64 {
65 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
66 *nextY = currentY;
67 return 0;
68 }
69 *nextX = currentX;
70 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
71 return 0;
72 }
73
74
75 // this function predicts the movement of the unit for the next deltaT seconds
76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
77 {
78 const float pointReachedDistance = 0.25f;
79 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
80 const float maxSimStepTime = 0.015625f;
81
82 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
83 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
84 int16_t nextX = enemy->nextX;
85 int16_t nextY = enemy->nextY;
86 Vector2 position = enemy->simPosition;
87 int passedCount = 0;
88 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
89 {
90 float stepTime = fminf(deltaT - t, maxSimStepTime);
91 Vector2 target = (Vector2){nextX, nextY};
92 float speed = Vector2Length(*velocity);
93 // draw the target position for debugging
94 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
95 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
96 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
97 {
98 // we reached the target position, let's move to the next waypoint
99 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100 target = (Vector2){nextX, nextY};
101 // track how many waypoints we passed
102 passedCount++;
103 }
104
105 // acceleration towards the target
106 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108 *velocity = Vector2Add(*velocity, acceleration);
109
110 // limit the speed to the maximum speed
111 if (speed > maxSpeed)
112 {
113 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114 }
115
116 // move the enemy
117 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118 }
119
120 if (waypointPassedCount)
121 {
122 (*waypointPassedCount) = passedCount;
123 }
124
125 return position;
126 }
127
128 void EnemyDraw()
129 {
130 for (int i = 0; i < enemyCount; i++)
131 {
132 Enemy enemy = enemies[i];
133 if (enemy.enemyType == ENEMY_TYPE_NONE)
134 {
135 continue;
136 }
137
138 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139
140 if (enemy.movePathCount > 0)
141 {
142 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144 }
145 for (int j = 1; j < enemy.movePathCount; j++)
146 {
147 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149 DrawLine3D(p, q, GREEN);
150 }
151
152 switch (enemy.enemyType)
153 {
154 case ENEMY_TYPE_MINION:
155 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156 break;
157 }
158 }
159 }
160
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163 // damage the tower
164 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167 float explosionRange2 = explosionRange * explosionRange;
168 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169 // explode the enemy
170 if (tower->damage >= TowerGetMaxHealth(tower))
171 {
172 tower->towerType = TOWER_TYPE_NONE;
173 }
174
175 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
176 explosionSource,
177 (Vector3){0, 0.1f, 0}, 1.0f);
178
179 enemy->enemyType = ENEMY_TYPE_NONE;
180
181 // push back enemies & dealing damage
182 for (int i = 0; i < enemyCount; i++)
183 {
184 Enemy *other = &enemies[i];
185 if (other->enemyType == ENEMY_TYPE_NONE)
186 {
187 continue;
188 }
189 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190 if (distanceSqr > 0 && distanceSqr < explosionRange2)
191 {
192 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194 EnemyAddDamage(other, explosionDamge);
195 }
196 }
197 }
198
199 void EnemyUpdate()
200 {
201 const float castleX = 0;
202 const float castleY = 0;
203 const float maxPathDistance2 = 0.25f * 0.25f;
204
205 for (int i = 0; i < enemyCount; i++)
206 {
207 Enemy *enemy = &enemies[i];
208 if (enemy->enemyType == ENEMY_TYPE_NONE)
209 {
210 continue;
211 }
212
213 int waypointPassedCount = 0;
214 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215 enemy->startMovingTime = gameTime.time;
216 // track path of unit
217 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218 {
219 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220 {
221 enemy->movePath[j] = enemy->movePath[j - 1];
222 }
223 enemy->movePath[0] = enemy->simPosition;
224 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225 {
226 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227 }
228 }
229
230 if (waypointPassedCount > 0)
231 {
232 enemy->currentX = enemy->nextX;
233 enemy->currentY = enemy->nextY;
234 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236 {
237 // enemy reached the castle; remove it
238 enemy->enemyType = ENEMY_TYPE_NONE;
239 continue;
240 }
241 }
242 }
243
244 // handle collisions between enemies
245 for (int i = 0; i < enemyCount - 1; i++)
246 {
247 Enemy *enemyA = &enemies[i];
248 if (enemyA->enemyType == ENEMY_TYPE_NONE)
249 {
250 continue;
251 }
252 for (int j = i + 1; j < enemyCount; j++)
253 {
254 Enemy *enemyB = &enemies[j];
255 if (enemyB->enemyType == ENEMY_TYPE_NONE)
256 {
257 continue;
258 }
259 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262 float radiusSum = radiusA + radiusB;
263 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264 {
265 // collision
266 float distance = sqrtf(distanceSqr);
267 float overlap = radiusSum - distance;
268 // move the enemies apart, but softly; if we have a clog of enemies,
269 // moving them perfectly apart can cause them to jitter
270 float positionCorrection = overlap / 5.0f;
271 Vector2 direction = (Vector2){
272 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276 }
277 }
278 }
279
280 // handle collisions between enemies and towers
281 for (int i = 0; i < enemyCount; i++)
282 {
283 Enemy *enemy = &enemies[i];
284 if (enemy->enemyType == ENEMY_TYPE_NONE)
285 {
286 continue;
287 }
288 enemy->contactTime -= gameTime.deltaTime;
289 if (enemy->contactTime < 0.0f)
290 {
291 enemy->contactTime = 0.0f;
292 }
293
294 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295 // linear search over towers; could be optimized by using path finding tower map,
296 // but for now, we keep it simple
297 for (int j = 0; j < towerCount; j++)
298 {
299 Tower *tower = &towers[j];
300 if (tower->towerType == TOWER_TYPE_NONE)
301 {
302 continue;
303 }
304 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306 if (distanceSqr > combinedRadius * combinedRadius)
307 {
308 continue;
309 }
310 // potential collision; square / circle intersection
311 float dx = tower->x - enemy->simPosition.x;
312 float dy = tower->y - enemy->simPosition.y;
313 float absDx = fabsf(dx);
314 float absDy = fabsf(dy);
315 Vector3 contactPoint = {0};
316 if (absDx <= 0.5f && absDx <= absDy) {
317 // vertical collision; push the enemy out horizontally
318 float overlap = enemyRadius + 0.5f - absDy;
319 if (overlap < 0.0f)
320 {
321 continue;
322 }
323 float direction = dy > 0.0f ? -1.0f : 1.0f;
324 enemy->simPosition.y += direction * overlap;
325 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
326 }
327 else if (absDy <= 0.5f && absDy <= absDx)
328 {
329 // horizontal collision; push the enemy out vertically
330 float overlap = enemyRadius + 0.5f - absDx;
331 if (overlap < 0.0f)
332 {
333 continue;
334 }
335 float direction = dx > 0.0f ? -1.0f : 1.0f;
336 enemy->simPosition.x += direction * overlap;
337 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
338 }
339 else
340 {
341 // possible collision with a corner
342 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
343 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
344 float cornerX = tower->x + cornerDX;
345 float cornerY = tower->y + cornerDY;
346 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
347 if (cornerDistanceSqr > enemyRadius * enemyRadius)
348 {
349 continue;
350 }
351 // push the enemy out along the diagonal
352 float cornerDistance = sqrtf(cornerDistanceSqr);
353 float overlap = enemyRadius - cornerDistance;
354 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
355 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
356 enemy->simPosition.x -= directionX * overlap;
357 enemy->simPosition.y -= directionY * overlap;
358 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
359 }
360
361 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
362 {
363 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
364 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
365 {
366 EnemyTriggerExplode(enemy, tower, contactPoint);
367 }
368 }
369 }
370 }
371 }
372
373 EnemyId EnemyGetId(Enemy *enemy)
374 {
375 return (EnemyId){enemy - enemies, enemy->generation};
376 }
377
378 Enemy *EnemyTryResolve(EnemyId enemyId)
379 {
380 if (enemyId.index >= ENEMY_MAX_COUNT)
381 {
382 return 0;
383 }
384 Enemy *enemy = &enemies[enemyId.index];
385 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
386 {
387 return 0;
388 }
389 return enemy;
390 }
391
392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
393 {
394 Enemy *spawn = 0;
395 for (int i = 0; i < enemyCount; i++)
396 {
397 Enemy *enemy = &enemies[i];
398 if (enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 spawn = enemy;
401 break;
402 }
403 }
404
405 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
406 {
407 spawn = &enemies[enemyCount++];
408 }
409
410 if (spawn)
411 {
412 spawn->currentX = currentX;
413 spawn->currentY = currentY;
414 spawn->nextX = currentX;
415 spawn->nextY = currentY;
416 spawn->simPosition = (Vector2){currentX, currentY};
417 spawn->simVelocity = (Vector2){0, 0};
418 spawn->enemyType = enemyType;
419 spawn->startMovingTime = gameTime.time;
420 spawn->damage = 0.0f;
421 spawn->futureDamage = 0.0f;
422 spawn->generation++;
423 spawn->movePathCount = 0;
424 }
425
426 return spawn;
427 }
428
429 int EnemyAddDamage(Enemy *enemy, float damage)
430 {
431 enemy->damage += damage;
432 if (enemy->damage >= EnemyGetMaxHealth(enemy))
433 {
434 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
435 enemy->enemyType = ENEMY_TYPE_NONE;
436 return 1;
437 }
438
439 return 0;
440 }
441
442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
443 {
444 int16_t castleX = 0;
445 int16_t castleY = 0;
446 Enemy* closest = 0;
447 int16_t closestDistance = 0;
448 float range2 = range * range;
449 for (int i = 0; i < enemyCount; i++)
450 {
451 Enemy* enemy = &enemies[i];
452 if (enemy->enemyType == ENEMY_TYPE_NONE)
453 {
454 continue;
455 }
456 float maxHealth = EnemyGetMaxHealth(enemy);
457 if (enemy->futureDamage >= maxHealth)
458 {
459 // ignore enemies that will die soon
460 continue;
461 }
462 int16_t dx = castleX - enemy->currentX;
463 int16_t dy = castleY - enemy->currentY;
464 int16_t distance = abs(dx) + abs(dy);
465 if (!closest || distance < closestDistance)
466 {
467 float tdx = towerX - enemy->currentX;
468 float tdy = towerY - enemy->currentY;
469 float tdistance2 = tdx * tdx + tdy * tdy;
470 if (tdistance2 <= range2)
471 {
472 closest = enemy;
473 closestDistance = distance;
474 }
475 }
476 }
477 return closest;
478 }
479
480 int EnemyCount()
481 {
482 int count = 0;
483 for (int i = 0; i < enemyCount; i++)
484 {
485 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
486 {
487 count++;
488 }
489 }
490 return count;
491 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate()
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < towerCount; i++)
131 {
132 Tower *tower = &towers[i];
133 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134 {
135 continue;
136 }
137 int16_t mapX, mapY;
138 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139 // this would not work correctly and needs to be refined to allow towers covering multiple cells
140 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141 // one cell. For now.
142 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143 {
144 continue;
145 }
146 int index = mapY * width + mapX;
147 pathfindingMap.towerIndex[index] = i;
148 }
149
150 // we start at the castle and add the castle to the queue
151 pathfindingMap.maxDistance = 0.0f;
152 pathfindingNodeQueueCount = 0;
153 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154 PathfindingNode *node = 0;
155 while ((node = PathFindingNodePop()))
156 {
157 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158 {
159 continue;
160 }
161 int index = node->y * width + node->x;
162 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163 {
164 continue;
165 }
166
167 int deltaX = node->x - node->fromX;
168 int deltaY = node->y - node->fromY;
169 // even if the cell is blocked by a tower, we still may want to store the direction
170 // (though this might not be needed, IDK right now)
171 pathfindingMap.deltaSrc[index].x = (char) deltaX;
172 pathfindingMap.deltaSrc[index].y = (char) deltaY;
173
174 // we skip nodes that are blocked by towers
175 if (pathfindingMap.towerIndex[index] >= 0)
176 {
177 node->distance += 8.0f;
178 }
179 pathfindingMap.distances[index] = node->distance;
180 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185 }
186 }
187
188 void PathFindingMapDraw()
189 {
190 float cellSize = pathfindingMap.scale * 0.9f;
191 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192 for (int x = 0; x < pathfindingMap.width; x++)
193 {
194 for (int y = 0; y < pathfindingMap.height; y++)
195 {
196 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200 // animate the distance "wave" to show how the pathfinding algorithm expands
201 // from the castle
202 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203 {
204 color = BLACK;
205 }
206 DrawCube(position, cellSize, 0.1f, cellSize, color);
207 }
208 }
209 }
210
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213 int16_t mapX, mapY;
214 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215 {
216 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217 return (Vector2){(float)-delta.x, (float)-delta.y};
218 }
219 // fallback to a simple gradient calculation
220 float n = PathFindingGetDistance(mapX, mapY - 1);
221 float s = PathFindingGetDistance(mapX, mapY + 1);
222 float w = PathFindingGetDistance(mapX - 1, mapY);
223 float e = PathFindingGetDistance(mapX + 1, mapY);
224 return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 void ProjectileInit()
8 {
9 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
10 {
11 projectiles[i] = (Projectile){0};
12 }
13 }
14
15 void ProjectileDraw()
16 {
17 for (int i = 0; i < projectileCount; i++)
18 {
19 Projectile projectile = projectiles[i];
20 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
21 {
22 continue;
23 }
24 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
25 if (transition >= 1.0f)
26 {
27 continue;
28 }
29 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
30 float x = position.x;
31 float y = position.y;
32 float dx = projectile.directionNormal.x;
33 float dy = projectile.directionNormal.y;
34 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
35 {
36 x -= dx * 0.1f;
37 y -= dy * 0.1f;
38 float size = 0.1f * d;
39 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
40 }
41 }
42 }
43
44 void ProjectileUpdate()
45 {
46 for (int i = 0; i < projectileCount; i++)
47 {
48 Projectile *projectile = &projectiles[i];
49 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
50 {
51 continue;
52 }
53 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
54 if (transition >= 1.0f)
55 {
56 projectile->projectileType = PROJECTILE_TYPE_NONE;
57 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
58 if (enemy)
59 {
60 EnemyAddDamage(enemy, projectile->damage);
61 }
62 continue;
63 }
64 }
65 }
66
67 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
68 {
69 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
70 {
71 Projectile *projectile = &projectiles[i];
72 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
73 {
74 projectile->projectileType = projectileType;
75 projectile->shootTime = gameTime.time;
76 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
77 projectile->damage = damage;
78 projectile->position = position;
79 projectile->target = target;
80 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
81 projectile->targetEnemy = EnemyGetId(enemy);
82 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
83 return projectile;
84 }
85 }
86 return 0;
87 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Particle particles[PARTICLE_MAX_COUNT];
5 static int particleCount = 0;
6
7 void ParticleInit()
8 {
9 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
10 {
11 particles[i] = (Particle){0};
12 }
13 particleCount = 0;
14 }
15
16 static void DrawExplosionParticle(Particle *particle, float transition)
17 {
18 float size = 1.2f * (1.0f - transition);
19 Color startColor = WHITE;
20 Color endColor = RED;
21 Color color = ColorLerp(startColor, endColor, transition);
22 DrawCube(particle->position, size, size, size, color);
23 }
24
25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
26 {
27 if (particleCount >= PARTICLE_MAX_COUNT)
28 {
29 return;
30 }
31
32 int index = -1;
33 for (int i = 0; i < particleCount; i++)
34 {
35 if (particles[i].particleType == PARTICLE_TYPE_NONE)
36 {
37 index = i;
38 break;
39 }
40 }
41
42 if (index == -1)
43 {
44 index = particleCount++;
45 }
46
47 Particle *particle = &particles[index];
48 particle->particleType = particleType;
49 particle->spawnTime = gameTime.time;
50 particle->lifetime = lifetime;
51 particle->position = position;
52 particle->velocity = velocity;
53 }
54
55 void ParticleUpdate()
56 {
57 for (int i = 0; i < particleCount; i++)
58 {
59 Particle *particle = &particles[i];
60 if (particle->particleType == PARTICLE_TYPE_NONE)
61 {
62 continue;
63 }
64
65 float age = gameTime.time - particle->spawnTime;
66
67 if (particle->lifetime > age)
68 {
69 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
70 }
71 else {
72 particle->particleType = PARTICLE_TYPE_NONE;
73 }
74 }
75 }
76
77 void ParticleDraw()
78 {
79 for (int i = 0; i < particleCount; i++)
80 {
81 Particle particle = particles[i];
82 if (particle.particleType == PARTICLE_TYPE_NONE)
83 {
84 continue;
85 }
86
87 float age = gameTime.time - particle.spawnTime;
88 float transition = age / particle.lifetime;
89 switch (particle.particleType)
90 {
91 case PARTICLE_TYPE_EXPLOSION:
92 DrawExplosionParticle(&particle, transition);
93 break;
94 default:
95 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
96 break;
97 }
98 }
99 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
In this step, I extracted the projectile and particle system code. The global variables can now be declared as static as well, so we only need to expose the functions in the header file.
The tab view of the editor web page is now getting quite crowded, which is exactly why I refrained from splitting the code up too early - it's easy to get lost. When I will later add features again, I will have to try to avoid doing changes in multiple files at once so the changes are easier to follow, but this'll be a challenge.
Let's proceeed!
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 //# Variables
7 GUIState guiState = {0};
8 GameTime gameTime = {0};
9
10 Level levels[] = {
11 [0] = {
12 .state = LEVEL_STATE_BUILDING,
13 .initialGold = 10,
14 .waves[0] = {
15 .enemyType = ENEMY_TYPE_MINION,
16 .wave = 0,
17 .count = 10,
18 .interval = 1.0f,
19 .delay = 1.0f,
20 .spawnPosition = {0, 6},
21 },
22 .waves[1] = {
23 .enemyType = ENEMY_TYPE_MINION,
24 .wave = 1,
25 .count = 20,
26 .interval = 0.5f,
27 .delay = 1.0f,
28 .spawnPosition = {0, 6},
29 },
30 .waves[2] = {
31 .enemyType = ENEMY_TYPE_MINION,
32 .wave = 2,
33 .count = 30,
34 .interval = 0.25f,
35 .delay = 1.0f,
36 .spawnPosition = {0, 6},
37 }
38 },
39 };
40
41 Level *currentLevel = levels;
42
43 //# Game
44
45 void InitLevel(Level *level)
46 {
47 TowerInit();
48 EnemyInit();
49 ProjectileInit();
50 ParticleInit();
51 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
52
53 level->placementMode = 0;
54 level->state = LEVEL_STATE_BUILDING;
55 level->nextState = LEVEL_STATE_NONE;
56 level->playerGold = level->initialGold;
57
58 Camera *camera = &level->camera;
59 camera->position = (Vector3){1.0f, 12.0f, 6.5f};
60 camera->target = (Vector3){0.0f, 0.5f, 1.0f};
61 camera->up = (Vector3){0.0f, 1.0f, 0.0f};
62 camera->fovy = 45.0f;
63 camera->projection = CAMERA_PERSPECTIVE;
64 }
65
66 void DrawLevelHud(Level *level)
67 {
68 const char *text = TextFormat("Gold: %d", level->playerGold);
69 Font font = GetFontDefault();
70 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 120, 10}, font.baseSize * 2.0f, 2.0f, BLACK);
71 DrawTextEx(font, text, (Vector2){GetScreenWidth() - 122, 8}, font.baseSize * 2.0f, 2.0f, YELLOW);
72 }
73
74 void DrawLevelReportLostWave(Level *level)
75 {
76 BeginMode3D(level->camera);
77 DrawGrid(10, 1.0f);
78 TowerDraw();
79 EnemyDraw();
80 ProjectileDraw();
81 ParticleDraw();
82 guiState.isBlocked = 0;
83 EndMode3D();
84
85 const char *text = "Wave lost";
86 int textWidth = MeasureText(text, 20);
87 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
88
89 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
90 {
91 level->nextState = LEVEL_STATE_RESET;
92 }
93 }
94
95 int HasLevelNextWave(Level *level)
96 {
97 for (int i = 0; i < 10; i++)
98 {
99 EnemyWave *wave = &level->waves[i];
100 if (wave->wave == level->currentWave)
101 {
102 return 1;
103 }
104 }
105 return 0;
106 }
107
108 void DrawLevelReportWonWave(Level *level)
109 {
110 BeginMode3D(level->camera);
111 DrawGrid(10, 1.0f);
112 TowerDraw();
113 EnemyDraw();
114 ProjectileDraw();
115 ParticleDraw();
116 guiState.isBlocked = 0;
117 EndMode3D();
118
119 const char *text = "Wave won";
120 int textWidth = MeasureText(text, 20);
121 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
122
123
124 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
125 {
126 level->nextState = LEVEL_STATE_RESET;
127 }
128
129 if (HasLevelNextWave(level))
130 {
131 if (Button("Prepare for next wave", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
132 {
133 level->nextState = LEVEL_STATE_BUILDING;
134 }
135 }
136 else {
137 if (Button("Level won", GetScreenWidth() - 300, GetScreenHeight() - 40, 300, 30, 0))
138 {
139 level->nextState = LEVEL_STATE_WON_LEVEL;
140 }
141 }
142 }
143
144 void DrawBuildingBuildButton(Level *level, int x, int y, int width, int height, uint8_t towerType, const char *name)
145 {
146 static ButtonState buttonStates[8] = {0};
147 int cost = GetTowerCosts(towerType);
148 const char *text = TextFormat("%s: %d", name, cost);
149 buttonStates[towerType].isSelected = level->placementMode == towerType;
150 buttonStates[towerType].isDisabled = level->playerGold < cost;
151 if (Button(text, x, y, width, height, &buttonStates[towerType]))
152 {
153 level->placementMode = buttonStates[towerType].isSelected ? 0 : towerType;
154 }
155 }
156
157 void DrawLevelBuildingState(Level *level)
158 {
159 BeginMode3D(level->camera);
160 DrawGrid(10, 1.0f);
161 TowerDraw();
162 EnemyDraw();
163 ProjectileDraw();
164 ParticleDraw();
165
166 Ray ray = GetScreenToWorldRay(GetMousePosition(), level->camera);
167 float planeDistance = ray.position.y / -ray.direction.y;
168 float planeX = ray.direction.x * planeDistance + ray.position.x;
169 float planeY = ray.direction.z * planeDistance + ray.position.z;
170 int16_t mapX = (int16_t)floorf(planeX + 0.5f);
171 int16_t mapY = (int16_t)floorf(planeY + 0.5f);
172 if (level->placementMode && !guiState.isBlocked)
173 {
174 DrawCubeWires((Vector3){mapX, 0.2f, mapY}, 1.0f, 0.4f, 1.0f, RED);
175 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
176 {
177 if (TowerTryAdd(level->placementMode, mapX, mapY))
178 {
179 level->playerGold -= GetTowerCosts(level->placementMode);
180 level->placementMode = TOWER_TYPE_NONE;
181 }
182 }
183 }
184
185 guiState.isBlocked = 0;
186
187 EndMode3D();
188
189 static ButtonState buildWallButtonState = {0};
190 static ButtonState buildGunButtonState = {0};
191 buildWallButtonState.isSelected = level->placementMode == TOWER_TYPE_WALL;
192 buildGunButtonState.isSelected = level->placementMode == TOWER_TYPE_GUN;
193
194 DrawBuildingBuildButton(level, 10, 10, 80, 30, TOWER_TYPE_WALL, "Wall");
195 DrawBuildingBuildButton(level, 10, 50, 80, 30, TOWER_TYPE_GUN, "Gun");
196
197 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
198 {
199 level->nextState = LEVEL_STATE_RESET;
200 }
201
202 if (Button("Begin waves", GetScreenWidth() - 160, GetScreenHeight() - 40, 160, 30, 0))
203 {
204 level->nextState = LEVEL_STATE_BATTLE;
205 }
206
207 const char *text = "Building phase";
208 int textWidth = MeasureText(text, 20);
209 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
210 }
211
212 void InitBattleStateConditions(Level *level)
213 {
214 level->state = LEVEL_STATE_BATTLE;
215 level->nextState = LEVEL_STATE_NONE;
216 level->waveEndTimer = 0.0f;
217 for (int i = 0; i < 10; i++)
218 {
219 EnemyWave *wave = &level->waves[i];
220 wave->spawned = 0;
221 wave->timeToSpawnNext = wave->delay;
222 }
223 }
224
225 void DrawLevelBattleState(Level *level)
226 {
227 BeginMode3D(level->camera);
228 DrawGrid(10, 1.0f);
229 TowerDraw();
230 EnemyDraw();
231 ProjectileDraw();
232 ParticleDraw();
233 guiState.isBlocked = 0;
234 EndMode3D();
235
236 if (Button("Reset level", 20, GetScreenHeight() - 40, 160, 30, 0))
237 {
238 level->nextState = LEVEL_STATE_RESET;
239 }
240
241 int maxCount = 0;
242 int remainingCount = 0;
243 for (int i = 0; i < 10; i++)
244 {
245 EnemyWave *wave = &level->waves[i];
246 if (wave->wave != level->currentWave)
247 {
248 continue;
249 }
250 maxCount += wave->count;
251 remainingCount += wave->count - wave->spawned;
252 }
253 int aliveCount = EnemyCount();
254 remainingCount += aliveCount;
255
256 const char *text = TextFormat("Battle phase: %03d%%", 100 - remainingCount * 100 / maxCount);
257 int textWidth = MeasureText(text, 20);
258 DrawText(text, (GetScreenWidth() - textWidth) * 0.5f, 20, 20, WHITE);
259 }
260
261 void DrawLevel(Level *level)
262 {
263 switch (level->state)
264 {
265 case LEVEL_STATE_BUILDING: DrawLevelBuildingState(level); break;
266 case LEVEL_STATE_BATTLE: DrawLevelBattleState(level); break;
267 case LEVEL_STATE_WON_WAVE: DrawLevelReportWonWave(level); break;
268 case LEVEL_STATE_LOST_WAVE: DrawLevelReportLostWave(level); break;
269 default: break;
270 }
271
272 DrawLevelHud(level);
273 }
274
275 void UpdateLevel(Level *level)
276 {
277 if (level->state == LEVEL_STATE_BATTLE)
278 {
279 int activeWaves = 0;
280 for (int i = 0; i < 10; i++)
281 {
282 EnemyWave *wave = &level->waves[i];
283 if (wave->spawned >= wave->count || wave->wave != level->currentWave)
284 {
285 continue;
286 }
287 activeWaves++;
288 wave->timeToSpawnNext -= gameTime.deltaTime;
289 if (wave->timeToSpawnNext <= 0.0f)
290 {
291 Enemy *enemy = EnemyTryAdd(wave->enemyType, wave->spawnPosition.x, wave->spawnPosition.y);
292 if (enemy)
293 {
294 wave->timeToSpawnNext = wave->interval;
295 wave->spawned++;
296 }
297 }
298 }
299 if (GetTowerByType(TOWER_TYPE_BASE) == 0) {
300 level->waveEndTimer += gameTime.deltaTime;
301 if (level->waveEndTimer >= 2.0f)
302 {
303 level->nextState = LEVEL_STATE_LOST_WAVE;
304 }
305 }
306 else if (activeWaves == 0 && EnemyCount() == 0)
307 {
308 level->waveEndTimer += gameTime.deltaTime;
309 if (level->waveEndTimer >= 2.0f)
310 {
311 level->nextState = LEVEL_STATE_WON_WAVE;
312 }
313 }
314 }
315
316 PathFindingMapUpdate();
317 EnemyUpdate();
318 TowerUpdate();
319 ProjectileUpdate();
320 ParticleUpdate();
321
322 if (level->nextState == LEVEL_STATE_RESET)
323 {
324 InitLevel(level);
325 }
326
327 if (level->nextState == LEVEL_STATE_BATTLE)
328 {
329 InitBattleStateConditions(level);
330 }
331
332 if (level->nextState == LEVEL_STATE_WON_WAVE)
333 {
334 level->currentWave++;
335 level->state = LEVEL_STATE_WON_WAVE;
336 }
337
338 if (level->nextState == LEVEL_STATE_LOST_WAVE)
339 {
340 level->state = LEVEL_STATE_LOST_WAVE;
341 }
342
343 if (level->nextState == LEVEL_STATE_BUILDING)
344 {
345 level->state = LEVEL_STATE_BUILDING;
346 }
347
348 if (level->nextState == LEVEL_STATE_WON_LEVEL)
349 {
350 // make something of this later
351 InitLevel(level);
352 }
353
354 level->nextState = LEVEL_STATE_NONE;
355 }
356
357 float nextSpawnTime = 0.0f;
358
359 void ResetGame()
360 {
361 InitLevel(currentLevel);
362 }
363
364 void InitGame()
365 {
366 TowerInit();
367 EnemyInit();
368 ProjectileInit();
369 ParticleInit();
370 PathfindingMapInit(20, 20, (Vector3){-10.0f, 0.0f, -10.0f}, 1.0f);
371
372 currentLevel = levels;
373 InitLevel(currentLevel);
374 }
375
376 //# Immediate GUI functions
377
378 int Button(const char *text, int x, int y, int width, int height, ButtonState *state)
379 {
380 Rectangle bounds = {x, y, width, height};
381 int isPressed = 0;
382 int isSelected = state && state->isSelected;
383 if (CheckCollisionPointRec(GetMousePosition(), bounds) && !guiState.isBlocked && !state->isDisabled)
384 {
385 Color color = isSelected ? DARKGRAY : GRAY;
386 DrawRectangle(x, y, width, height, color);
387 if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
388 {
389 isPressed = 1;
390 }
391 guiState.isBlocked = 1;
392 }
393 else
394 {
395 Color color = isSelected ? WHITE : LIGHTGRAY;
396 DrawRectangle(x, y, width, height, color);
397 }
398 Font font = GetFontDefault();
399 Vector2 textSize = MeasureTextEx(font, text, font.baseSize * 2.0f, 1);
400 Color textColor = state->isDisabled ? GRAY : BLACK;
401 DrawTextEx(font, text, (Vector2){x + width / 2 - textSize.x / 2, y + height / 2 - textSize.y / 2}, font.baseSize * 2.0f, 1, textColor);
402 return isPressed;
403 }
404
405 //# Main game loop
406
407 void GameUpdate()
408 {
409 float dt = GetFrameTime();
410 // cap maximum delta time to 0.1 seconds to prevent large time steps
411 if (dt > 0.1f) dt = 0.1f;
412 gameTime.time += dt;
413 gameTime.deltaTime = dt;
414
415 UpdateLevel(currentLevel);
416 }
417
418 int main(void)
419 {
420 int screenWidth, screenHeight;
421 GetPreferredSize(&screenWidth, &screenHeight);
422 InitWindow(screenWidth, screenHeight, "Tower defense");
423 SetTargetFPS(30);
424
425 InitGame();
426
427 while (!WindowShouldClose())
428 {
429 if (IsPaused()) {
430 // canvas is not visible in browser - do nothing
431 continue;
432 }
433
434 BeginDrawing();
435 ClearBackground(DARKBLUE);
436
437 GameUpdate();
438 DrawLevel(currentLevel);
439
440 EndDrawing();
441 }
442
443 CloseWindow();
444
445 return 0;
446 }
1 #ifndef TD_TUT_2_MAIN_H
2 #define TD_TUT_2_MAIN_H
3
4 #include <inttypes.h>
5
6 #include "raylib.h"
7 #include "preferred_size.h"
8
9 //# Declarations
10
11 #define ENEMY_MAX_PATH_COUNT 8
12 #define ENEMY_MAX_COUNT 400
13 #define ENEMY_TYPE_NONE 0
14 #define ENEMY_TYPE_MINION 1
15
16 #define PARTICLE_MAX_COUNT 400
17 #define PARTICLE_TYPE_NONE 0
18 #define PARTICLE_TYPE_EXPLOSION 1
19
20 typedef struct Particle
21 {
22 uint8_t particleType;
23 float spawnTime;
24 float lifetime;
25 Vector3 position;
26 Vector3 velocity;
27 } Particle;
28
29 #define TOWER_MAX_COUNT 400
30 #define TOWER_TYPE_NONE 0
31 #define TOWER_TYPE_BASE 1
32 #define TOWER_TYPE_GUN 2
33 #define TOWER_TYPE_WALL 3
34
35 typedef struct Tower
36 {
37 int16_t x, y;
38 uint8_t towerType;
39 float cooldown;
40 float damage;
41 } Tower;
42
43 typedef struct GameTime
44 {
45 float time;
46 float deltaTime;
47 } GameTime;
48
49 typedef struct ButtonState {
50 char isSelected;
51 char isDisabled;
52 } ButtonState;
53
54 typedef struct GUIState {
55 int isBlocked;
56 } GUIState;
57
58 typedef enum LevelState
59 {
60 LEVEL_STATE_NONE,
61 LEVEL_STATE_BUILDING,
62 LEVEL_STATE_BATTLE,
63 LEVEL_STATE_WON_WAVE,
64 LEVEL_STATE_LOST_WAVE,
65 LEVEL_STATE_WON_LEVEL,
66 LEVEL_STATE_RESET,
67 } LevelState;
68
69 typedef struct EnemyWave {
70 uint8_t enemyType;
71 uint8_t wave;
72 uint16_t count;
73 float interval;
74 float delay;
75 Vector2 spawnPosition;
76
77 uint16_t spawned;
78 float timeToSpawnNext;
79 } EnemyWave;
80
81 typedef struct Level
82 {
83 LevelState state;
84 LevelState nextState;
85 Camera3D camera;
86 int placementMode;
87
88 int initialGold;
89 int playerGold;
90
91 EnemyWave waves[10];
92 int currentWave;
93 float waveEndTimer;
94 } Level;
95
96 typedef struct DeltaSrc
97 {
98 char x, y;
99 } DeltaSrc;
100
101 typedef struct PathfindingMap
102 {
103 int width, height;
104 float scale;
105 float *distances;
106 long *towerIndex;
107 DeltaSrc *deltaSrc;
108 float maxDistance;
109 Matrix toMapSpace;
110 Matrix toWorldSpace;
111 } PathfindingMap;
112
113 // when we execute the pathfinding algorithm, we need to store the active nodes
114 // in a queue. Each node has a position, a distance from the start, and the
115 // position of the node that we came from.
116 typedef struct PathfindingNode
117 {
118 int16_t x, y, fromX, fromY;
119 float distance;
120 } PathfindingNode;
121
122 typedef struct EnemyId
123 {
124 uint16_t index;
125 uint16_t generation;
126 } EnemyId;
127
128 typedef struct EnemyClassConfig
129 {
130 float speed;
131 float health;
132 float radius;
133 float maxAcceleration;
134 float requiredContactTime;
135 float explosionDamage;
136 float explosionRange;
137 float explosionPushbackPower;
138 int goldValue;
139 } EnemyClassConfig;
140
141 typedef struct Enemy
142 {
143 int16_t currentX, currentY;
144 int16_t nextX, nextY;
145 Vector2 simPosition;
146 Vector2 simVelocity;
147 uint16_t generation;
148 float startMovingTime;
149 float damage, futureDamage;
150 float contactTime;
151 uint8_t enemyType;
152 uint8_t movePathCount;
153 Vector2 movePath[ENEMY_MAX_PATH_COUNT];
154 } Enemy;
155
156 #define PROJECTILE_MAX_COUNT 1200
157 #define PROJECTILE_TYPE_NONE 0
158 #define PROJECTILE_TYPE_BULLET 1
159
160 typedef struct Projectile
161 {
162 uint8_t projectileType;
163 float shootTime;
164 float arrivalTime;
165 float damage;
166 Vector2 position;
167 Vector2 target;
168 Vector2 directionNormal;
169 EnemyId targetEnemy;
170 } Projectile;
171
172 //# Function declarations
173 float TowerGetMaxHealth(Tower *tower);
174 int Button(const char *text, int x, int y, int width, int height, ButtonState *state);
175 int EnemyAddDamage(Enemy *enemy, float damage);
176
177 //# Enemy functions
178 void EnemyInit();
179 void EnemyDraw();
180 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource);
181 void EnemyUpdate();
182 float EnemyGetCurrentMaxSpeed(Enemy *enemy);
183 float EnemyGetMaxHealth(Enemy *enemy);
184 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY);
185 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount);
186 EnemyId EnemyGetId(Enemy *enemy);
187 Enemy *EnemyTryResolve(EnemyId enemyId);
188 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY);
189 int EnemyAddDamage(Enemy *enemy, float damage);
190 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range);
191 int EnemyCount();
192
193 //# Tower functions
194 void TowerInit();
195 Tower *TowerGetAt(int16_t x, int16_t y);
196 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y);
197 Tower *GetTowerByType(uint8_t towerType);
198 int GetTowerCosts(uint8_t towerType);
199 float TowerGetMaxHealth(Tower *tower);
200 void TowerDraw();
201 void TowerUpdate();
202
203 //# Particles
204 void ParticleInit();
205 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime);
206 void ParticleUpdate();
207 void ParticleDraw();
208
209 //# Projectiles
210 void ProjectileInit();
211 void ProjectileDraw();
212 void ProjectileUpdate();
213 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage);
214
215 //# Pathfinding map
216 void PathfindingMapInit(int width, int height, Vector3 translate, float scale);
217 float PathFindingGetDistance(int mapX, int mapY);
218 Vector2 PathFindingGetGradient(Vector3 world);
219 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY);
220 void PathFindingMapUpdate();
221 void PathFindingMapDraw();
222
223 //# variables
224 extern Level *currentLevel;
225 extern Enemy enemies[ENEMY_MAX_COUNT];
226 extern int enemyCount;
227 extern EnemyClassConfig enemyClassConfigs[];
228
229 extern GUIState guiState;
230 extern GameTime gameTime;
231 extern Tower towers[TOWER_MAX_COUNT];
232 extern int towerCount;
233
234 #endif
1 #include "td_main.h"
2 #include <raymath.h>
3 #include <stdlib.h>
4 #include <math.h>
5
6 EnemyClassConfig enemyClassConfigs[] = {
7 [ENEMY_TYPE_MINION] = {
8 .health = 3.0f,
9 .speed = 1.0f,
10 .radius = 0.25f,
11 .maxAcceleration = 1.0f,
12 .explosionDamage = 1.0f,
13 .requiredContactTime = 0.5f,
14 .explosionRange = 1.0f,
15 .explosionPushbackPower = 0.25f,
16 .goldValue = 1,
17 },
18 };
19
20 Enemy enemies[ENEMY_MAX_COUNT];
21 int enemyCount = 0;
22
23 void EnemyInit()
24 {
25 for (int i = 0; i < ENEMY_MAX_COUNT; i++)
26 {
27 enemies[i] = (Enemy){0};
28 }
29 enemyCount = 0;
30 }
31
32 float EnemyGetCurrentMaxSpeed(Enemy *enemy)
33 {
34 return enemyClassConfigs[enemy->enemyType].speed;
35 }
36
37 float EnemyGetMaxHealth(Enemy *enemy)
38 {
39 return enemyClassConfigs[enemy->enemyType].health;
40 }
41
42 int EnemyGetNextPosition(int16_t currentX, int16_t currentY, int16_t *nextX, int16_t *nextY)
43 {
44 int16_t castleX = 0;
45 int16_t castleY = 0;
46 int16_t dx = castleX - currentX;
47 int16_t dy = castleY - currentY;
48 if (dx == 0 && dy == 0)
49 {
50 *nextX = currentX;
51 *nextY = currentY;
52 return 1;
53 }
54 Vector2 gradient = PathFindingGetGradient((Vector3){currentX, 0, currentY});
55
56 if (gradient.x == 0 && gradient.y == 0)
57 {
58 *nextX = currentX;
59 *nextY = currentY;
60 return 1;
61 }
62
63 if (fabsf(gradient.x) > fabsf(gradient.y))
64 {
65 *nextX = currentX + (int16_t)(gradient.x > 0.0f ? 1 : -1);
66 *nextY = currentY;
67 return 0;
68 }
69 *nextX = currentX;
70 *nextY = currentY + (int16_t)(gradient.y > 0.0f ? 1 : -1);
71 return 0;
72 }
73
74
75 // this function predicts the movement of the unit for the next deltaT seconds
76 Vector2 EnemyGetPosition(Enemy *enemy, float deltaT, Vector2 *velocity, int *waypointPassedCount)
77 {
78 const float pointReachedDistance = 0.25f;
79 const float pointReachedDistance2 = pointReachedDistance * pointReachedDistance;
80 const float maxSimStepTime = 0.015625f;
81
82 float maxAcceleration = enemyClassConfigs[enemy->enemyType].maxAcceleration;
83 float maxSpeed = EnemyGetCurrentMaxSpeed(enemy);
84 int16_t nextX = enemy->nextX;
85 int16_t nextY = enemy->nextY;
86 Vector2 position = enemy->simPosition;
87 int passedCount = 0;
88 for (float t = 0.0f; t < deltaT; t += maxSimStepTime)
89 {
90 float stepTime = fminf(deltaT - t, maxSimStepTime);
91 Vector2 target = (Vector2){nextX, nextY};
92 float speed = Vector2Length(*velocity);
93 // draw the target position for debugging
94 DrawCubeWires((Vector3){target.x, 0.2f, target.y}, 0.1f, 0.4f, 0.1f, RED);
95 Vector2 lookForwardPos = Vector2Add(position, Vector2Scale(*velocity, speed));
96 if (Vector2DistanceSqr(target, lookForwardPos) <= pointReachedDistance2)
97 {
98 // we reached the target position, let's move to the next waypoint
99 EnemyGetNextPosition(nextX, nextY, &nextX, &nextY);
100 target = (Vector2){nextX, nextY};
101 // track how many waypoints we passed
102 passedCount++;
103 }
104
105 // acceleration towards the target
106 Vector2 unitDirection = Vector2Normalize(Vector2Subtract(target, lookForwardPos));
107 Vector2 acceleration = Vector2Scale(unitDirection, maxAcceleration * stepTime);
108 *velocity = Vector2Add(*velocity, acceleration);
109
110 // limit the speed to the maximum speed
111 if (speed > maxSpeed)
112 {
113 *velocity = Vector2Scale(*velocity, maxSpeed / speed);
114 }
115
116 // move the enemy
117 position = Vector2Add(position, Vector2Scale(*velocity, stepTime));
118 }
119
120 if (waypointPassedCount)
121 {
122 (*waypointPassedCount) = passedCount;
123 }
124
125 return position;
126 }
127
128 void EnemyDraw()
129 {
130 for (int i = 0; i < enemyCount; i++)
131 {
132 Enemy enemy = enemies[i];
133 if (enemy.enemyType == ENEMY_TYPE_NONE)
134 {
135 continue;
136 }
137
138 Vector2 position = EnemyGetPosition(&enemy, gameTime.time - enemy.startMovingTime, &enemy.simVelocity, 0);
139
140 if (enemy.movePathCount > 0)
141 {
142 Vector3 p = {enemy.movePath[0].x, 0.2f, enemy.movePath[0].y};
143 DrawLine3D(p, (Vector3){position.x, 0.2f, position.y}, GREEN);
144 }
145 for (int j = 1; j < enemy.movePathCount; j++)
146 {
147 Vector3 p = {enemy.movePath[j - 1].x, 0.2f, enemy.movePath[j - 1].y};
148 Vector3 q = {enemy.movePath[j].x, 0.2f, enemy.movePath[j].y};
149 DrawLine3D(p, q, GREEN);
150 }
151
152 switch (enemy.enemyType)
153 {
154 case ENEMY_TYPE_MINION:
155 DrawCubeWires((Vector3){position.x, 0.2f, position.y}, 0.4f, 0.4f, 0.4f, GREEN);
156 break;
157 }
158 }
159 }
160
161 void EnemyTriggerExplode(Enemy *enemy, Tower *tower, Vector3 explosionSource)
162 {
163 // damage the tower
164 float explosionDamge = enemyClassConfigs[enemy->enemyType].explosionDamage;
165 float explosionRange = enemyClassConfigs[enemy->enemyType].explosionRange;
166 float explosionPushbackPower = enemyClassConfigs[enemy->enemyType].explosionPushbackPower;
167 float explosionRange2 = explosionRange * explosionRange;
168 tower->damage += enemyClassConfigs[enemy->enemyType].explosionDamage;
169 // explode the enemy
170 if (tower->damage >= TowerGetMaxHealth(tower))
171 {
172 tower->towerType = TOWER_TYPE_NONE;
173 }
174
175 ParticleAdd(PARTICLE_TYPE_EXPLOSION,
176 explosionSource,
177 (Vector3){0, 0.1f, 0}, 1.0f);
178
179 enemy->enemyType = ENEMY_TYPE_NONE;
180
181 // push back enemies & dealing damage
182 for (int i = 0; i < enemyCount; i++)
183 {
184 Enemy *other = &enemies[i];
185 if (other->enemyType == ENEMY_TYPE_NONE)
186 {
187 continue;
188 }
189 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, other->simPosition);
190 if (distanceSqr > 0 && distanceSqr < explosionRange2)
191 {
192 Vector2 direction = Vector2Normalize(Vector2Subtract(other->simPosition, enemy->simPosition));
193 other->simPosition = Vector2Add(other->simPosition, Vector2Scale(direction, explosionPushbackPower));
194 EnemyAddDamage(other, explosionDamge);
195 }
196 }
197 }
198
199 void EnemyUpdate()
200 {
201 const float castleX = 0;
202 const float castleY = 0;
203 const float maxPathDistance2 = 0.25f * 0.25f;
204
205 for (int i = 0; i < enemyCount; i++)
206 {
207 Enemy *enemy = &enemies[i];
208 if (enemy->enemyType == ENEMY_TYPE_NONE)
209 {
210 continue;
211 }
212
213 int waypointPassedCount = 0;
214 enemy->simPosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &enemy->simVelocity, &waypointPassedCount);
215 enemy->startMovingTime = gameTime.time;
216 // track path of unit
217 if (enemy->movePathCount == 0 || Vector2DistanceSqr(enemy->simPosition, enemy->movePath[0]) > maxPathDistance2)
218 {
219 for (int j = ENEMY_MAX_PATH_COUNT - 1; j > 0; j--)
220 {
221 enemy->movePath[j] = enemy->movePath[j - 1];
222 }
223 enemy->movePath[0] = enemy->simPosition;
224 if (++enemy->movePathCount > ENEMY_MAX_PATH_COUNT)
225 {
226 enemy->movePathCount = ENEMY_MAX_PATH_COUNT;
227 }
228 }
229
230 if (waypointPassedCount > 0)
231 {
232 enemy->currentX = enemy->nextX;
233 enemy->currentY = enemy->nextY;
234 if (EnemyGetNextPosition(enemy->currentX, enemy->currentY, &enemy->nextX, &enemy->nextY) &&
235 Vector2DistanceSqr(enemy->simPosition, (Vector2){castleX, castleY}) <= 0.25f * 0.25f)
236 {
237 // enemy reached the castle; remove it
238 enemy->enemyType = ENEMY_TYPE_NONE;
239 continue;
240 }
241 }
242 }
243
244 // handle collisions between enemies
245 for (int i = 0; i < enemyCount - 1; i++)
246 {
247 Enemy *enemyA = &enemies[i];
248 if (enemyA->enemyType == ENEMY_TYPE_NONE)
249 {
250 continue;
251 }
252 for (int j = i + 1; j < enemyCount; j++)
253 {
254 Enemy *enemyB = &enemies[j];
255 if (enemyB->enemyType == ENEMY_TYPE_NONE)
256 {
257 continue;
258 }
259 float distanceSqr = Vector2DistanceSqr(enemyA->simPosition, enemyB->simPosition);
260 float radiusA = enemyClassConfigs[enemyA->enemyType].radius;
261 float radiusB = enemyClassConfigs[enemyB->enemyType].radius;
262 float radiusSum = radiusA + radiusB;
263 if (distanceSqr < radiusSum * radiusSum && distanceSqr > 0.001f)
264 {
265 // collision
266 float distance = sqrtf(distanceSqr);
267 float overlap = radiusSum - distance;
268 // move the enemies apart, but softly; if we have a clog of enemies,
269 // moving them perfectly apart can cause them to jitter
270 float positionCorrection = overlap / 5.0f;
271 Vector2 direction = (Vector2){
272 (enemyB->simPosition.x - enemyA->simPosition.x) / distance * positionCorrection,
273 (enemyB->simPosition.y - enemyA->simPosition.y) / distance * positionCorrection};
274 enemyA->simPosition = Vector2Subtract(enemyA->simPosition, direction);
275 enemyB->simPosition = Vector2Add(enemyB->simPosition, direction);
276 }
277 }
278 }
279
280 // handle collisions between enemies and towers
281 for (int i = 0; i < enemyCount; i++)
282 {
283 Enemy *enemy = &enemies[i];
284 if (enemy->enemyType == ENEMY_TYPE_NONE)
285 {
286 continue;
287 }
288 enemy->contactTime -= gameTime.deltaTime;
289 if (enemy->contactTime < 0.0f)
290 {
291 enemy->contactTime = 0.0f;
292 }
293
294 float enemyRadius = enemyClassConfigs[enemy->enemyType].radius;
295 // linear search over towers; could be optimized by using path finding tower map,
296 // but for now, we keep it simple
297 for (int j = 0; j < towerCount; j++)
298 {
299 Tower *tower = &towers[j];
300 if (tower->towerType == TOWER_TYPE_NONE)
301 {
302 continue;
303 }
304 float distanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){tower->x, tower->y});
305 float combinedRadius = enemyRadius + 0.708; // sqrt(0.5^2 + 0.5^2), corner-center distance of square with side length 1
306 if (distanceSqr > combinedRadius * combinedRadius)
307 {
308 continue;
309 }
310 // potential collision; square / circle intersection
311 float dx = tower->x - enemy->simPosition.x;
312 float dy = tower->y - enemy->simPosition.y;
313 float absDx = fabsf(dx);
314 float absDy = fabsf(dy);
315 Vector3 contactPoint = {0};
316 if (absDx <= 0.5f && absDx <= absDy) {
317 // vertical collision; push the enemy out horizontally
318 float overlap = enemyRadius + 0.5f - absDy;
319 if (overlap < 0.0f)
320 {
321 continue;
322 }
323 float direction = dy > 0.0f ? -1.0f : 1.0f;
324 enemy->simPosition.y += direction * overlap;
325 contactPoint = (Vector3){enemy->simPosition.x, 0.2f, tower->x + direction * 0.5f};
326 }
327 else if (absDy <= 0.5f && absDy <= absDx)
328 {
329 // horizontal collision; push the enemy out vertically
330 float overlap = enemyRadius + 0.5f - absDx;
331 if (overlap < 0.0f)
332 {
333 continue;
334 }
335 float direction = dx > 0.0f ? -1.0f : 1.0f;
336 enemy->simPosition.x += direction * overlap;
337 contactPoint = (Vector3){tower->x + direction * 0.5f, 0.2f, enemy->simPosition.y};
338 }
339 else
340 {
341 // possible collision with a corner
342 float cornerDX = dx > 0.0f ? -0.5f : 0.5f;
343 float cornerDY = dy > 0.0f ? -0.5f : 0.5f;
344 float cornerX = tower->x + cornerDX;
345 float cornerY = tower->y + cornerDY;
346 float cornerDistanceSqr = Vector2DistanceSqr(enemy->simPosition, (Vector2){cornerX, cornerY});
347 if (cornerDistanceSqr > enemyRadius * enemyRadius)
348 {
349 continue;
350 }
351 // push the enemy out along the diagonal
352 float cornerDistance = sqrtf(cornerDistanceSqr);
353 float overlap = enemyRadius - cornerDistance;
354 float directionX = cornerDistance > 0.0f ? (cornerX - enemy->simPosition.x) / cornerDistance : -cornerDX;
355 float directionY = cornerDistance > 0.0f ? (cornerY - enemy->simPosition.y) / cornerDistance : -cornerDY;
356 enemy->simPosition.x -= directionX * overlap;
357 enemy->simPosition.y -= directionY * overlap;
358 contactPoint = (Vector3){cornerX, 0.2f, cornerY};
359 }
360
361 if (enemyClassConfigs[enemy->enemyType].explosionDamage > 0.0f)
362 {
363 enemy->contactTime += gameTime.deltaTime * 2.0f; // * 2 to undo the subtraction above
364 if (enemy->contactTime >= enemyClassConfigs[enemy->enemyType].requiredContactTime)
365 {
366 EnemyTriggerExplode(enemy, tower, contactPoint);
367 }
368 }
369 }
370 }
371 }
372
373 EnemyId EnemyGetId(Enemy *enemy)
374 {
375 return (EnemyId){enemy - enemies, enemy->generation};
376 }
377
378 Enemy *EnemyTryResolve(EnemyId enemyId)
379 {
380 if (enemyId.index >= ENEMY_MAX_COUNT)
381 {
382 return 0;
383 }
384 Enemy *enemy = &enemies[enemyId.index];
385 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
386 {
387 return 0;
388 }
389 return enemy;
390 }
391
392 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
393 {
394 Enemy *spawn = 0;
395 for (int i = 0; i < enemyCount; i++)
396 {
397 Enemy *enemy = &enemies[i];
398 if (enemy->enemyType == ENEMY_TYPE_NONE)
399 {
400 spawn = enemy;
401 break;
402 }
403 }
404
405 if (enemyCount < ENEMY_MAX_COUNT && !spawn)
406 {
407 spawn = &enemies[enemyCount++];
408 }
409
410 if (spawn)
411 {
412 spawn->currentX = currentX;
413 spawn->currentY = currentY;
414 spawn->nextX = currentX;
415 spawn->nextY = currentY;
416 spawn->simPosition = (Vector2){currentX, currentY};
417 spawn->simVelocity = (Vector2){0, 0};
418 spawn->enemyType = enemyType;
419 spawn->startMovingTime = gameTime.time;
420 spawn->damage = 0.0f;
421 spawn->futureDamage = 0.0f;
422 spawn->generation++;
423 spawn->movePathCount = 0;
424 }
425
426 return spawn;
427 }
428
429 int EnemyAddDamage(Enemy *enemy, float damage)
430 {
431 enemy->damage += damage;
432 if (enemy->damage >= EnemyGetMaxHealth(enemy))
433 {
434 currentLevel->playerGold += enemyClassConfigs[enemy->enemyType].goldValue;
435 enemy->enemyType = ENEMY_TYPE_NONE;
436 return 1;
437 }
438
439 return 0;
440 }
441
442 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range)
443 {
444 int16_t castleX = 0;
445 int16_t castleY = 0;
446 Enemy* closest = 0;
447 int16_t closestDistance = 0;
448 float range2 = range * range;
449 for (int i = 0; i < enemyCount; i++)
450 {
451 Enemy* enemy = &enemies[i];
452 if (enemy->enemyType == ENEMY_TYPE_NONE)
453 {
454 continue;
455 }
456 float maxHealth = EnemyGetMaxHealth(enemy);
457 if (enemy->futureDamage >= maxHealth)
458 {
459 // ignore enemies that will die soon
460 continue;
461 }
462 int16_t dx = castleX - enemy->currentX;
463 int16_t dy = castleY - enemy->currentY;
464 int16_t distance = abs(dx) + abs(dy);
465 if (!closest || distance < closestDistance)
466 {
467 float tdx = towerX - enemy->currentX;
468 float tdy = towerY - enemy->currentY;
469 float tdistance2 = tdx * tdx + tdy * tdy;
470 if (tdistance2 <= range2)
471 {
472 closest = enemy;
473 closestDistance = distance;
474 }
475 }
476 }
477 return closest;
478 }
479
480 int EnemyCount()
481 {
482 int count = 0;
483 for (int i = 0; i < enemyCount; i++)
484 {
485 if (enemies[i].enemyType != ENEMY_TYPE_NONE)
486 {
487 count++;
488 }
489 }
490 return count;
491 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 // The queue is a simple array of nodes, we add nodes to the end and remove
5 // nodes from the front. We keep the array around to avoid unnecessary allocations
6 static PathfindingNode *pathfindingNodeQueue = 0;
7 static int pathfindingNodeQueueCount = 0;
8 static int pathfindingNodeQueueCapacity = 0;
9
10 // The pathfinding map stores the distances from the castle to each cell in the map.
11 static PathfindingMap pathfindingMap = {0};
12
13 void PathfindingMapInit(int width, int height, Vector3 translate, float scale)
14 {
15 // transforming between map space and world space allows us to adapt
16 // position and scale of the map without changing the pathfinding data
17 pathfindingMap.toWorldSpace = MatrixTranslate(translate.x, translate.y, translate.z);
18 pathfindingMap.toWorldSpace = MatrixMultiply(pathfindingMap.toWorldSpace, MatrixScale(scale, scale, scale));
19 pathfindingMap.toMapSpace = MatrixInvert(pathfindingMap.toWorldSpace);
20 pathfindingMap.width = width;
21 pathfindingMap.height = height;
22 pathfindingMap.scale = scale;
23 pathfindingMap.distances = (float *)MemAlloc(width * height * sizeof(float));
24 for (int i = 0; i < width * height; i++)
25 {
26 pathfindingMap.distances[i] = -1.0f;
27 }
28
29 pathfindingMap.towerIndex = (long *)MemAlloc(width * height * sizeof(long));
30 pathfindingMap.deltaSrc = (DeltaSrc *)MemAlloc(width * height * sizeof(DeltaSrc));
31 }
32
33 static void PathFindingNodePush(int16_t x, int16_t y, int16_t fromX, int16_t fromY, float distance)
34 {
35 if (pathfindingNodeQueueCount >= pathfindingNodeQueueCapacity)
36 {
37 pathfindingNodeQueueCapacity = pathfindingNodeQueueCapacity == 0 ? 256 : pathfindingNodeQueueCapacity * 2;
38 // we use MemAlloc/MemRealloc to allocate memory for the queue
39 // I am not entirely sure if MemRealloc allows passing a null pointer
40 // so we check if the pointer is null and use MemAlloc in that case
41 if (pathfindingNodeQueue == 0)
42 {
43 pathfindingNodeQueue = (PathfindingNode *)MemAlloc(pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
44 }
45 else
46 {
47 pathfindingNodeQueue = (PathfindingNode *)MemRealloc(pathfindingNodeQueue, pathfindingNodeQueueCapacity * sizeof(PathfindingNode));
48 }
49 }
50
51 PathfindingNode *node = &pathfindingNodeQueue[pathfindingNodeQueueCount++];
52 node->x = x;
53 node->y = y;
54 node->fromX = fromX;
55 node->fromY = fromY;
56 node->distance = distance;
57 }
58
59 static PathfindingNode *PathFindingNodePop()
60 {
61 if (pathfindingNodeQueueCount == 0)
62 {
63 return 0;
64 }
65 // we return the first node in the queue; we want to return a pointer to the node
66 // so we can return 0 if the queue is empty.
67 // We should _not_ return a pointer to the element in the list, because the list
68 // may be reallocated and the pointer would become invalid. Or the
69 // popped element is overwritten by the next push operation.
70 // Using static here means that the variable is permanently allocated.
71 static PathfindingNode node;
72 node = pathfindingNodeQueue[0];
73 // we shift all nodes one position to the front
74 for (int i = 1; i < pathfindingNodeQueueCount; i++)
75 {
76 pathfindingNodeQueue[i - 1] = pathfindingNodeQueue[i];
77 }
78 --pathfindingNodeQueueCount;
79 return &node;
80 }
81
82 float PathFindingGetDistance(int mapX, int mapY)
83 {
84 if (mapX < 0 || mapX >= pathfindingMap.width || mapY < 0 || mapY >= pathfindingMap.height)
85 {
86 // when outside the map, we return the manhattan distance to the castle (0,0)
87 return fabsf((float)mapX) + fabsf((float)mapY);
88 }
89
90 return pathfindingMap.distances[mapY * pathfindingMap.width + mapX];
91 }
92
93 // transform a world position to a map position in the array;
94 // returns true if the position is inside the map
95 int PathFindingFromWorldToMapPosition(Vector3 worldPosition, int16_t *mapX, int16_t *mapY)
96 {
97 Vector3 mapPosition = Vector3Transform(worldPosition, pathfindingMap.toMapSpace);
98 *mapX = (int16_t)mapPosition.x;
99 *mapY = (int16_t)mapPosition.z;
100 return *mapX >= 0 && *mapX < pathfindingMap.width && *mapY >= 0 && *mapY < pathfindingMap.height;
101 }
102
103 void PathFindingMapUpdate()
104 {
105 const int castleX = 0, castleY = 0;
106 int16_t castleMapX, castleMapY;
107 if (!PathFindingFromWorldToMapPosition((Vector3){castleX, 0.0f, castleY}, &castleMapX, &castleMapY))
108 {
109 return;
110 }
111 int width = pathfindingMap.width, height = pathfindingMap.height;
112
113 // reset the distances to -1
114 for (int i = 0; i < width * height; i++)
115 {
116 pathfindingMap.distances[i] = -1.0f;
117 }
118 // reset the tower indices
119 for (int i = 0; i < width * height; i++)
120 {
121 pathfindingMap.towerIndex[i] = -1;
122 }
123 // reset the delta src
124 for (int i = 0; i < width * height; i++)
125 {
126 pathfindingMap.deltaSrc[i].x = 0;
127 pathfindingMap.deltaSrc[i].y = 0;
128 }
129
130 for (int i = 0; i < towerCount; i++)
131 {
132 Tower *tower = &towers[i];
133 if (tower->towerType == TOWER_TYPE_NONE || tower->towerType == TOWER_TYPE_BASE)
134 {
135 continue;
136 }
137 int16_t mapX, mapY;
138 // technically, if the tower cell scale is not in sync with the pathfinding map scale,
139 // this would not work correctly and needs to be refined to allow towers covering multiple cells
140 // or having multiple towers in one cell; for simplicity, we assume that the tower covers exactly
141 // one cell. For now.
142 if (!PathFindingFromWorldToMapPosition((Vector3){tower->x, 0.0f, tower->y}, &mapX, &mapY))
143 {
144 continue;
145 }
146 int index = mapY * width + mapX;
147 pathfindingMap.towerIndex[index] = i;
148 }
149
150 // we start at the castle and add the castle to the queue
151 pathfindingMap.maxDistance = 0.0f;
152 pathfindingNodeQueueCount = 0;
153 PathFindingNodePush(castleMapX, castleMapY, castleMapX, castleMapY, 0.0f);
154 PathfindingNode *node = 0;
155 while ((node = PathFindingNodePop()))
156 {
157 if (node->x < 0 || node->x >= width || node->y < 0 || node->y >= height)
158 {
159 continue;
160 }
161 int index = node->y * width + node->x;
162 if (pathfindingMap.distances[index] >= 0 && pathfindingMap.distances[index] <= node->distance)
163 {
164 continue;
165 }
166
167 int deltaX = node->x - node->fromX;
168 int deltaY = node->y - node->fromY;
169 // even if the cell is blocked by a tower, we still may want to store the direction
170 // (though this might not be needed, IDK right now)
171 pathfindingMap.deltaSrc[index].x = (char) deltaX;
172 pathfindingMap.deltaSrc[index].y = (char) deltaY;
173
174 // we skip nodes that are blocked by towers
175 if (pathfindingMap.towerIndex[index] >= 0)
176 {
177 node->distance += 8.0f;
178 }
179 pathfindingMap.distances[index] = node->distance;
180 pathfindingMap.maxDistance = fmaxf(pathfindingMap.maxDistance, node->distance);
181 PathFindingNodePush(node->x, node->y + 1, node->x, node->y, node->distance + 1.0f);
182 PathFindingNodePush(node->x, node->y - 1, node->x, node->y, node->distance + 1.0f);
183 PathFindingNodePush(node->x + 1, node->y, node->x, node->y, node->distance + 1.0f);
184 PathFindingNodePush(node->x - 1, node->y, node->x, node->y, node->distance + 1.0f);
185 }
186 }
187
188 void PathFindingMapDraw()
189 {
190 float cellSize = pathfindingMap.scale * 0.9f;
191 float highlightDistance = fmodf(GetTime() * 4.0f, pathfindingMap.maxDistance);
192 for (int x = 0; x < pathfindingMap.width; x++)
193 {
194 for (int y = 0; y < pathfindingMap.height; y++)
195 {
196 float distance = pathfindingMap.distances[y * pathfindingMap.width + x];
197 float colorV = distance < 0 ? 0 : fminf(distance / pathfindingMap.maxDistance, 1.0f);
198 Color color = distance < 0 ? BLUE : (Color){fminf(colorV, 1.0f) * 255, 0, 0, 255};
199 Vector3 position = Vector3Transform((Vector3){x, -0.25f, y}, pathfindingMap.toWorldSpace);
200 // animate the distance "wave" to show how the pathfinding algorithm expands
201 // from the castle
202 if (distance + 0.5f > highlightDistance && distance - 0.5f < highlightDistance)
203 {
204 color = BLACK;
205 }
206 DrawCube(position, cellSize, 0.1f, cellSize, color);
207 }
208 }
209 }
210
211 Vector2 PathFindingGetGradient(Vector3 world)
212 {
213 int16_t mapX, mapY;
214 if (PathFindingFromWorldToMapPosition(world, &mapX, &mapY))
215 {
216 DeltaSrc delta = pathfindingMap.deltaSrc[mapY * pathfindingMap.width + mapX];
217 return (Vector2){(float)-delta.x, (float)-delta.y};
218 }
219 // fallback to a simple gradient calculation
220 float n = PathFindingGetDistance(mapX, mapY - 1);
221 float s = PathFindingGetDistance(mapX, mapY + 1);
222 float w = PathFindingGetDistance(mapX - 1, mapY);
223 float e = PathFindingGetDistance(mapX + 1, mapY);
224 return (Vector2){w - e + 0.25f, n - s + 0.125f};
225 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Projectile projectiles[PROJECTILE_MAX_COUNT];
5 static int projectileCount = 0;
6
7 void ProjectileInit()
8 {
9 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
10 {
11 projectiles[i] = (Projectile){0};
12 }
13 }
14
15 void ProjectileDraw()
16 {
17 for (int i = 0; i < projectileCount; i++)
18 {
19 Projectile projectile = projectiles[i];
20 if (projectile.projectileType == PROJECTILE_TYPE_NONE)
21 {
22 continue;
23 }
24 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime);
25 if (transition >= 1.0f)
26 {
27 continue;
28 }
29 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition);
30 float x = position.x;
31 float y = position.y;
32 float dx = projectile.directionNormal.x;
33 float dy = projectile.directionNormal.y;
34 for (float d = 1.0f; d > 0.0f; d -= 0.25f)
35 {
36 x -= dx * 0.1f;
37 y -= dy * 0.1f;
38 float size = 0.1f * d;
39 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED);
40 }
41 }
42 }
43
44 void ProjectileUpdate()
45 {
46 for (int i = 0; i < projectileCount; i++)
47 {
48 Projectile *projectile = &projectiles[i];
49 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
50 {
51 continue;
52 }
53 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime);
54 if (transition >= 1.0f)
55 {
56 projectile->projectileType = PROJECTILE_TYPE_NONE;
57 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy);
58 if (enemy)
59 {
60 EnemyAddDamage(enemy, projectile->damage);
61 }
62 continue;
63 }
64 }
65 }
66
67 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage)
68 {
69 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++)
70 {
71 Projectile *projectile = &projectiles[i];
72 if (projectile->projectileType == PROJECTILE_TYPE_NONE)
73 {
74 projectile->projectileType = projectileType;
75 projectile->shootTime = gameTime.time;
76 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed;
77 projectile->damage = damage;
78 projectile->position = position;
79 projectile->target = target;
80 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position));
81 projectile->targetEnemy = EnemyGetId(enemy);
82 projectileCount = projectileCount <= i ? i + 1 : projectileCount;
83 return projectile;
84 }
85 }
86 return 0;
87 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 static Particle particles[PARTICLE_MAX_COUNT];
5 static int particleCount = 0;
6
7 void ParticleInit()
8 {
9 for (int i = 0; i < PARTICLE_MAX_COUNT; i++)
10 {
11 particles[i] = (Particle){0};
12 }
13 particleCount = 0;
14 }
15
16 static void DrawExplosionParticle(Particle *particle, float transition)
17 {
18 float size = 1.2f * (1.0f - transition);
19 Color startColor = WHITE;
20 Color endColor = RED;
21 Color color = ColorLerp(startColor, endColor, transition);
22 DrawCube(particle->position, size, size, size, color);
23 }
24
25 void ParticleAdd(uint8_t particleType, Vector3 position, Vector3 velocity, float lifetime)
26 {
27 if (particleCount >= PARTICLE_MAX_COUNT)
28 {
29 return;
30 }
31
32 int index = -1;
33 for (int i = 0; i < particleCount; i++)
34 {
35 if (particles[i].particleType == PARTICLE_TYPE_NONE)
36 {
37 index = i;
38 break;
39 }
40 }
41
42 if (index == -1)
43 {
44 index = particleCount++;
45 }
46
47 Particle *particle = &particles[index];
48 particle->particleType = particleType;
49 particle->spawnTime = gameTime.time;
50 particle->lifetime = lifetime;
51 particle->position = position;
52 particle->velocity = velocity;
53 }
54
55 void ParticleUpdate()
56 {
57 for (int i = 0; i < particleCount; i++)
58 {
59 Particle *particle = &particles[i];
60 if (particle->particleType == PARTICLE_TYPE_NONE)
61 {
62 continue;
63 }
64
65 float age = gameTime.time - particle->spawnTime;
66
67 if (particle->lifetime > age)
68 {
69 particle->position = Vector3Add(particle->position, Vector3Scale(particle->velocity, gameTime.deltaTime));
70 }
71 else {
72 particle->particleType = PARTICLE_TYPE_NONE;
73 }
74 }
75 }
76
77 void ParticleDraw()
78 {
79 for (int i = 0; i < particleCount; i++)
80 {
81 Particle particle = particles[i];
82 if (particle.particleType == PARTICLE_TYPE_NONE)
83 {
84 continue;
85 }
86
87 float age = gameTime.time - particle.spawnTime;
88 float transition = age / particle.lifetime;
89 switch (particle.particleType)
90 {
91 case PARTICLE_TYPE_EXPLOSION:
92 DrawExplosionParticle(&particle, transition);
93 break;
94 default:
95 DrawCube(particle.position, 0.3f, 0.5f, 0.3f, RED);
96 break;
97 }
98 }
99 }
1 #include "td_main.h"
2 #include <raymath.h>
3
4 Tower towers[TOWER_MAX_COUNT];
5 int towerCount = 0;
6
7 void TowerInit()
8 {
9 for (int i = 0; i < TOWER_MAX_COUNT; i++)
10 {
11 towers[i] = (Tower){0};
12 }
13 towerCount = 0;
14 }
15
16 static void TowerGunUpdate(Tower *tower)
17 {
18 if (tower->cooldown <= 0)
19 {
20 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f);
21 if (enemy)
22 {
23 tower->cooldown = 0.125f;
24 // shoot the enemy; determine future position of the enemy
25 float bulletSpeed = 1.0f;
26 float bulletDamage = 3.0f;
27 Vector2 velocity = enemy->simVelocity;
28 Vector2 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime, &velocity, 0);
29 Vector2 towerPosition = {tower->x, tower->y};
30 float eta = Vector2Distance(towerPosition, futurePosition) / bulletSpeed;
31 for (int i = 0; i < 8; i++) {
32 velocity = enemy->simVelocity;
33 futurePosition = EnemyGetPosition(enemy, gameTime.time - enemy->startMovingTime + eta, &velocity, 0);
34 float distance = Vector2Distance(towerPosition, futurePosition);
35 float eta2 = distance / bulletSpeed;
36 if (fabs(eta - eta2) < 0.01f) {
37 break;
38 }
39 eta = (eta2 + eta) * 0.5f;
40 }
41 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, towerPosition, futurePosition,
42 bulletSpeed, bulletDamage);
43 enemy->futureDamage += bulletDamage;
44 }
45 }
46 else
47 {
48 tower->cooldown -= gameTime.deltaTime;
49 }
50 }
51
52 Tower *TowerGetAt(int16_t x, int16_t y)
53 {
54 for (int i = 0; i < towerCount; i++)
55 {
56 if (towers[i].x == x && towers[i].y == y)
57 {
58 return &towers[i];
59 }
60 }
61 return 0;
62 }
63
64 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y)
65 {
66 if (towerCount >= TOWER_MAX_COUNT)
67 {
68 return 0;
69 }
70
71 Tower *tower = TowerGetAt(x, y);
72 if (tower)
73 {
74 return 0;
75 }
76
77 tower = &towers[towerCount++];
78 tower->x = x;
79 tower->y = y;
80 tower->towerType = towerType;
81 tower->cooldown = 0.0f;
82 tower->damage = 0.0f;
83 return tower;
84 }
85
86 Tower *GetTowerByType(uint8_t towerType)
87 {
88 for (int i = 0; i < towerCount; i++)
89 {
90 if (towers[i].towerType == towerType)
91 {
92 return &towers[i];
93 }
94 }
95 return 0;
96 }
97
98 int GetTowerCosts(uint8_t towerType)
99 {
100 switch (towerType)
101 {
102 case TOWER_TYPE_BASE:
103 return 0;
104 case TOWER_TYPE_GUN:
105 return 6;
106 case TOWER_TYPE_WALL:
107 return 2;
108 }
109 return 0;
110 }
111
112 float TowerGetMaxHealth(Tower *tower)
113 {
114 switch (tower->towerType)
115 {
116 case TOWER_TYPE_BASE:
117 return 10.0f;
118 case TOWER_TYPE_GUN:
119 return 3.0f;
120 case TOWER_TYPE_WALL:
121 return 5.0f;
122 }
123 return 0.0f;
124 }
125
126 void TowerDraw()
127 {
128 for (int i = 0; i < towerCount; i++)
129 {
130 Tower tower = towers[i];
131 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY);
132 switch (tower.towerType)
133 {
134 case TOWER_TYPE_BASE:
135 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON);
136 break;
137 case TOWER_TYPE_GUN:
138 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE);
139 break;
140 case TOWER_TYPE_WALL:
141 DrawCube((Vector3){tower.x, 0.5f, tower.y}, 1.0f, 1.0f, 1.0f, LIGHTGRAY);
142 break;
143 }
144 }
145 }
146
147 void TowerUpdate()
148 {
149 for (int i = 0; i < towerCount; i++)
150 {
151 Tower *tower = &towers[i];
152 switch (tower->towerType)
153 {
154 case TOWER_TYPE_GUN:
155 TowerGunUpdate(tower);
156 break;
157 }
158 }
159 }
1 #include "raylib.h"
2 #include "preferred_size.h"
3
4 // Since the canvas size is not known at compile time, we need to query it at runtime;
5 // the following platform specific code obtains the canvas size and we will use this
6 // size as the preferred size for the window at init time. We're ignoring here the
7 // possibility of the canvas size changing during runtime - this would require to
8 // poll the canvas size in the game loop or establishing a callback to be notified
9
10 #ifdef PLATFORM_WEB
11 #include <emscripten.h>
12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
13
14 void GetPreferredSize(int *screenWidth, int *screenHeight)
15 {
16 double canvasWidth, canvasHeight;
17 emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
18 *screenWidth = (int)canvasWidth;
19 *screenHeight = (int)canvasHeight;
20 TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
21 }
22
23 int IsPaused()
24 {
25 const char *js = "(function(){\n"
26 " var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
27 " var rect = canvas.getBoundingClientRect();\n"
28 " var isVisible = (\n"
29 " rect.top >= 0 &&\n"
30 " rect.left >= 0 &&\n"
31 " rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
32 " rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
33 " );\n"
34 " return isVisible ? 0 : 1;\n"
35 "})()";
36 return emscripten_run_script_int(js);
37 }
38
39 #else
40 void GetPreferredSize(int *screenWidth, int *screenHeight)
41 {
42 *screenWidth = 600;
43 *screenHeight = 240;
44 }
45 int IsPaused()
46 {
47 return 0;
48 }
49 #endif
1 #ifndef PREFERRED_SIZE_H
2 #define PREFERRED_SIZE_H
3
4 void GetPreferredSize(int *screenWidth, int *screenHeight);
5 int IsPaused();
6
7 #endif
The tower system is now extracted as well. The main file contains now the main loop and the game state handling with its drawing and update functions. It's now a little less than 450 lines long, which is a lot shorter than the original 1500 lines. The other files have between 100 and 500 lines, the enemy system being the largest one.
In an actual project, the headers could be split as well - for example, the enemy system could have its own enemy.h file. Other modules that don't require the enemy system would then not include the enemy.h file. This reduces the dependencies between the files and makes the project more manageable. Since this is a tutorial and keeping the number of files small for the sake of making it easier to follow, I won't split the main header file. It isn't that big anyway.
Wrap up
I would now stop the refactoring at this point. The code is now split into multiple files, none being bigger than 500 lines. There's now some structure and hopefully the features we are going to add are easier to follow - but let's see how this turns out in the next parts of the tutorial.
Speaking of: For the next part, I want to add damage meter overlays for enemies and towers and graphics! This'll be more fun than this refactoring step, I promise.
Here's a screenshot of what it will look like then:
So stay tuned for the next part!