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:

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).

Micro optimizations like declaring functions and variables static are often declared to be 'premature optimization'. There are 2 things to consider about this argument:
  1. 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.
  2. 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!

🍪