Simple tower defense tutorial, part 1

With the last blog update, I added a straight forward way to my blog to build raylib WASM files from C code as part of my blog build process. Having this now as a tool, I would like to use this system to show how to write a simple tower defense game in C using raylib. This is going to be a series of blog posts, where I will show how to add features to the game step by step. I will try to keep the code as simple and short as possible and will explain the concepts behind the code.

Difficult wise, you need to be able to understand C and know a bit how raylib works. If you managed to compile a hello world project and maybe doing a bit more programming already, you should be able to follow. If anything is unclear, please let me know and I will try to improve the blog posts.

That being said, here's how the game looks and runs at the end of this blog post:

The game should work as following:

Let's start

  • 💾
  1 #include "td-tut-1-main.h"
  2 
  3 int main(void)
  4 {
  5   int screenWidth, screenHeight;
  6   GetPreferredSize(&screenWidth, &screenHeight);
  7   InitWindow(screenWidth, screenHeight, "Tower defense");
  8   SetTargetFPS(30);
  9 
 10   while (!WindowShouldClose())
 11   {
 12     if (IsPaused()) {
 13       // canvas is not visible in browser - do nothing
 14       continue;
 15     }
 16     BeginDrawing();
 17     ClearBackground(DARKBLUE);
 18     DrawText("Hello Tower Defense Tutorial", 190, 200, 20, WHITE);
 19     EndDrawing();
 20   }
 21 
 22   CloseWindow();
 23 
 24   return 0;
 25 }
  1 #ifndef TD_TUT_1_MAIN_H
  2 #define TD_TUT_1_MAIN_H
  3 
  4 #include "raylib.h"
  5 #include "preferred_size.h"
  6 
  7 #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

We start out with a classic hello world program. Once this runs, we can start adding what we need for the game. A 3d grid and top down view makes sense to do next; note how the newly added or modified parts are highlighted in green. When you read the tutorial samples one after another, you can focus on those parts to see what has changed:

  • 💾
1 #include "td-tut-2-main.h"
2 3 int main(void) 4 { 5 int screenWidth, screenHeight; 6 GetPreferredSize(&screenWidth, &screenHeight); 7 InitWindow(screenWidth, screenHeight, "Tower defense");
8 SetTargetFPS(30); 9 10 Camera3D camera = {0}; 11 camera.position = (Vector3){0.0f, 10.0f, 5.0f}; 12 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 13 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 14 camera.fovy = 45.0f; 15 camera.projection = CAMERA_PERSPECTIVE;
16 17 while (!WindowShouldClose()) 18 { 19 if (IsPaused()) { 20 // canvas is not visible in browser - do nothing 21 continue; 22 } 23 BeginDrawing(); 24 ClearBackground(DARKBLUE);
25 26 BeginMode3D(camera); 27 DrawGrid(10, 1.0f); 28 EndMode3D(); 29 30 DrawText("Tower defense tutorial", 5, 5, 20, WHITE);
31 EndDrawing(); 32 } 33 34 CloseWindow(); 35 36 return 0; 37 }
  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 #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

We start out with raylib's grid drawing functionality, which is quite straight forward. We will later replace it with our own grid drawing function. But it helps us to get a quick orientation of where we are looking at.

Now we need to think forward a little;

The grid.

The Grid. A digital frontier. I tried to picture clusters of information as they moved through the computer. What did they look like? Ships, motorcycles? Were the circuits like freeways? I kept dreaming of a world I thought I'd never see. And then, one day I got in...
- Kevin Flynn, Tron

Just like in the movie Tron, we have to picture what our information looks like. The process is important, since early decisions tend to have much impact on the later development.

There is much to consider and inevitably, we will make wrong decisions. This is something we should embrace - it's part of the process of developing games. In my experience, we should not try to write our code in a way that it can handle all future cases; instead, we should focus on making the code easily replaceable: Looking at code as modules and APIs between them, minimizing the dependencies between them. This reduces the amount of change needed when we have to replace a module.

But this is something that is to be learned over time. For now, let's look at what we need:

For the towers, let's consider two ways how to implement it: Either we have the grid array and each cell contains the tower data or we have a separate array that contains the towers with their position. The first approach has the problem that we always have to iterate over the whole grid to update each tower. If our grid is empty and is 20x20, we have to iterate over 400 cells just to do nothing. The second approach is more efficient, but we have to make sure that we don't place towers on top of each other and we also have to manage the list - deletion and insertion.

Another point to consider is, if a tower can occupy multiple cells. In that case, the first approach is more complicated: We'd need to block all cells the tower occupies, but only one cell should contain the tower data. The second approach is more straight forward: We just have to make sure that the tower is not placed on top of another tower.

We will go with the second approach: An array that contains the towers and their position. The idea that our towers could be tetris like shapes is interesting and I would like to keep the option open to explore this idea further. We may come back later to the idea to have a 2d array that contains all our towers so we can quickly look up towers by position, but starting out with a simple array is simple, also because we don't have too overthink things.

To keep things simple and short, let's use global variables for our data structures:

  1 #define TOWER_MAX_COUNT 400
  2 #define TOWER_TYPE_NONE 0
  3 #define TOWER_TYPE_BASE 1
  4 
  5 typedef struct Tower {
  6   uint16_t x, y;
  7   uint8_t towerType;
  8 } Tower;
  9 
 10 Tower towers[TOWER_MAX_COUNT];
 11 int towerCount = 0;

Our first goal is to place a tower on the grid, making sure that we don't place it on top of another tower and render a cube at the tower's position:

  • 💾
  1 #include "td-tut-2-main.h"
  2 
3 #define TOWER_MAX_COUNT 400 4 #define TOWER_TYPE_NONE 0 5 #define TOWER_TYPE_BASE 1 6 #define TOWER_TYPE_GUN 2 7 8 typedef struct Tower 9 { 10 int16_t x, y; 11 uint8_t towerType; 12 } Tower; 13 14 Tower towers[TOWER_MAX_COUNT]; 15 int towerCount = 0; 16 17 void TowerInit() 18 { 19 for (int i = 0; i < TOWER_MAX_COUNT; i++) 20 { 21 towers[i] = (Tower){0}; 22 } 23 towerCount = 0; 24 } 25 26 Tower *TowerGetAt(int16_t x, int16_t y) 27 { 28 for (int i = 0; i < towerCount; i++) 29 { 30 if (towers[i].x == x && towers[i].y == y) 31 { 32 return &towers[i]; 33 } 34 } 35 return 0; 36 } 37 38 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 39 { 40 if (towerCount >= TOWER_MAX_COUNT) 41 { 42 return 0; 43 } 44 45 Tower *tower = TowerGetAt(x, y); 46 if (tower) 47 { 48 return 0; 49 } 50 51 tower = &towers[towerCount++]; 52 tower->x = x; 53 tower->y = y; 54 tower->towerType = towerType; 55 return tower; 56 } 57 58 void TowerDraw() 59 { 60 for (int i = 0; i < towerCount; i++) 61 { 62 Tower tower = towers[i]; 63 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 64 switch (tower.towerType) 65 { 66 case TOWER_TYPE_BASE: 67 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 68 break; 69 case TOWER_TYPE_GUN: 70 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 71 break; 72 } 73 } 74 } 75 76 void InitGame() 77 { 78 TowerInit(); 79 80 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 81 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 82 } 83
84 int main(void) 85 { 86 int screenWidth, screenHeight; 87 GetPreferredSize(&screenWidth, &screenHeight); 88 InitWindow(screenWidth, screenHeight, "Tower defense"); 89 SetTargetFPS(30); 90 91 Camera3D camera = {0}; 92 camera.position = (Vector3){0.0f, 10.0f, 5.0f}; 93 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 94 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 95 camera.fovy = 45.0f;
96 camera.projection = CAMERA_PERSPECTIVE; 97 98 InitGame();
99 100 while (!WindowShouldClose()) 101 { 102 if (IsPaused()) { 103 // canvas is not visible in browser - do nothing 104 continue; 105 } 106 BeginDrawing(); 107 ClearBackground(DARKBLUE); 108 109 BeginMode3D(camera);
110 DrawGrid(10, 1.0f); 111 TowerDraw();
112 EndMode3D(); 113 114 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 115 EndDrawing(); 116 } 117 118 CloseWindow(); 119 120 return 0; 121 }
  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 #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

For the enemies, we'll do something very similar; we'll have a separate array that contains the enemies and their position. To see where things are going, we'll just render a cube at the enemy's position. The enemy isn't moving yet:

  • 💾
  1 #include "td-tut-2-main.h"
  2 
3 //# Enemies 4 5 #define ENEMY_MAX_COUNT 400 6 #define ENEMY_TYPE_NONE 0 7 #define ENEMY_TYPE_MINION 1 8 9 typedef struct Enemy 10 { 11 int16_t currentX, currentY; 12 uint8_t enemyType; 13 } Enemy; 14 15 Enemy enemies[ENEMY_MAX_COUNT]; 16 int enemyCount = 0; 17 18 void EnemyInit() 19 { 20 for (int i = 0; i < ENEMY_MAX_COUNT; i++) 21 { 22 enemies[i] = (Enemy){0}; 23 } 24 enemyCount = 0; 25 } 26 27 void EnemyDraw() 28 { 29 for (int i = 0; i < enemyCount; i++) 30 { 31 Enemy enemy = enemies[i]; 32 switch (enemy.enemyType) 33 { 34 case ENEMY_TYPE_MINION: 35 DrawCube((Vector3){enemy.currentX, 0.2f, enemy.currentY}, 0.4f, 0.4f, 0.4f, GREEN); 36 break; 37 } 38 } 39 } 40 41 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 42 { 43 if (enemyCount >= ENEMY_MAX_COUNT) 44 { 45 return 0; 46 } 47 48 Enemy *enemy = &enemies[enemyCount++]; 49 enemy->currentX = currentX; 50 enemy->currentY = currentY; 51 enemy->enemyType = enemyType; 52 return enemy; 53 } 54 55 //# Towers 56
57 #define TOWER_MAX_COUNT 400 58 #define TOWER_TYPE_NONE 0 59 #define TOWER_TYPE_BASE 1 60 #define TOWER_TYPE_GUN 2 61 62 typedef struct Tower 63 { 64 int16_t x, y; 65 uint8_t towerType; 66 } Tower; 67 68 Tower towers[TOWER_MAX_COUNT]; 69 int towerCount = 0; 70 71 void TowerInit() 72 { 73 for (int i = 0; i < TOWER_MAX_COUNT; i++) 74 { 75 towers[i] = (Tower){0}; 76 } 77 towerCount = 0; 78 } 79 80 Tower *TowerGetAt(int16_t x, int16_t y) 81 { 82 for (int i = 0; i < towerCount; i++) 83 { 84 if (towers[i].x == x && towers[i].y == y) 85 { 86 return &towers[i]; 87 } 88 } 89 return 0; 90 } 91 92 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 93 { 94 if (towerCount >= TOWER_MAX_COUNT) 95 { 96 return 0; 97 } 98 99 Tower *tower = TowerGetAt(x, y); 100 if (tower) 101 { 102 return 0; 103 } 104 105 tower = &towers[towerCount++]; 106 tower->x = x; 107 tower->y = y; 108 tower->towerType = towerType; 109 return tower; 110 } 111 112 void TowerDraw() 113 { 114 for (int i = 0; i < towerCount; i++) 115 { 116 Tower tower = towers[i]; 117 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 118 switch (tower.towerType) 119 { 120 case TOWER_TYPE_BASE: 121 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 122 break; 123 case TOWER_TYPE_GUN: 124 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 125 break; 126 } 127 }
128 } 129 130 //# Game
131 132 void InitGame() 133 {
134 TowerInit(); 135 EnemyInit();
136 137 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
138 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 139 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4);
140 } 141 142 int main(void) 143 { 144 int screenWidth, screenHeight; 145 GetPreferredSize(&screenWidth, &screenHeight); 146 InitWindow(screenWidth, screenHeight, "Tower defense"); 147 SetTargetFPS(30); 148 149 Camera3D camera = {0}; 150 camera.position = (Vector3){0.0f, 10.0f, 5.0f}; 151 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 152 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 153 camera.fovy = 45.0f; 154 camera.projection = CAMERA_PERSPECTIVE; 155 156 InitGame(); 157 158 while (!WindowShouldClose()) 159 { 160 if (IsPaused()) { 161 // canvas is not visible in browser - do nothing 162 continue; 163 } 164 BeginDrawing(); 165 ClearBackground(DARKBLUE); 166 167 BeginMode3D(camera); 168 DrawGrid(10, 1.0f);
169 TowerDraw(); 170 EnemyDraw();
171 EndMode3D(); 172 173 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 174 EndDrawing(); 175 } 176 177 CloseWindow(); 178 179 return 0; 180 }
  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 #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

For the next step, we want to have the enemies move towards the castle, despawn them when they reach the castle and spawn new enemies at a random coordinate:

  • 💾
  1 #include "td-tut-2-main.h"
2 #include <stdlib.h> 3 #include <math.h> 4 5 typedef struct GameTime 6 { 7 float time; 8 float deltaTime; 9 } GameTime; 10 11 GameTime gameTime = {0};
12 13 //# Enemies 14 15 #define ENEMY_MAX_COUNT 400 16 #define ENEMY_TYPE_NONE 0 17 #define ENEMY_TYPE_MINION 1 18 19 typedef struct Enemy 20 {
21 int16_t currentX, currentY; 22 int16_t nextX, nextY; 23 float startMovingTime;
24 uint8_t enemyType; 25 } Enemy; 26 27 Enemy enemies[ENEMY_MAX_COUNT]; 28 int enemyCount = 0; 29 30 void EnemyInit() 31 { 32 for (int i = 0; i < ENEMY_MAX_COUNT; i++) 33 { 34 enemies[i] = (Enemy){0}; 35 } 36 enemyCount = 0; 37 } 38 39 void EnemyDraw() 40 { 41 for (int i = 0; i < enemyCount; i++) 42 { 43 Enemy enemy = enemies[i]; 44 switch (enemy.enemyType) 45 { 46 case ENEMY_TYPE_MINION: 47 DrawCube((Vector3){enemy.currentX, 0.2f, enemy.currentY}, 0.4f, 0.4f, 0.4f, GREEN);
48 break; 49 } 50 } 51 } 52 53 void EnemyUpdate() 54 { 55 const int16_t castleX = 0; 56 const int16_t castleY = 0; 57 58 for (int i = 0; i < enemyCount; i++) 59 { 60 Enemy *enemy = &enemies[i]; 61 if (enemy->enemyType == ENEMY_TYPE_NONE) 62 { 63 continue; 64 } 65 float speed = 1.0f; 66 float transition = (gameTime.time - enemy->startMovingTime) * speed; 67 if (transition >= 1.0f) 68 { 69 enemy->startMovingTime = gameTime.time; 70 enemy->currentX = enemy->nextX; 71 enemy->currentY = enemy->nextY; 72 int16_t dx = castleX - enemy->currentX; 73 int16_t dy = castleY - enemy->currentY; 74 if (dx == 0 && dy == 0) 75 { 76 // enemy reached the castle; remove it 77 enemy->enemyType = ENEMY_TYPE_NONE; 78 continue; 79 } 80 if (abs(dx) > abs(dy)) 81 { 82 enemy->nextX = enemy->currentX + (dx > 0 ? 1 : -1); 83 enemy->nextY = enemy->currentY; 84 } 85 else 86 { 87 enemy->nextX = enemy->currentX; 88 enemy->nextY = enemy->currentY + (dy > 0 ? 1 : -1); 89 }
90 } 91 } 92 } 93 94 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 95 {
96 Enemy *spawn = 0; 97 for (int i = 0; i < enemyCount; i++)
98 {
99 Enemy *enemy = &enemies[i]; 100 if (enemy->enemyType == ENEMY_TYPE_NONE) 101 { 102 spawn = enemy; 103 break; 104 }
105 } 106
107 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 108 { 109 spawn = &enemies[enemyCount++]; 110 } 111 112 if (spawn) 113 { 114 spawn->currentX = currentX; 115 spawn->currentY = currentY; 116 spawn->nextX = currentX; 117 spawn->nextY = currentY; 118 spawn->enemyType = enemyType; 119 spawn->startMovingTime = 0; 120 } 121 122 return spawn;
123 } 124 125 //# Towers 126 127 #define TOWER_MAX_COUNT 400 128 #define TOWER_TYPE_NONE 0 129 #define TOWER_TYPE_BASE 1 130 #define TOWER_TYPE_GUN 2 131 132 typedef struct Tower 133 { 134 int16_t x, y; 135 uint8_t towerType; 136 } Tower; 137 138 Tower towers[TOWER_MAX_COUNT]; 139 int towerCount = 0; 140 141 void TowerInit() 142 { 143 for (int i = 0; i < TOWER_MAX_COUNT; i++) 144 { 145 towers[i] = (Tower){0}; 146 } 147 towerCount = 0; 148 } 149 150 Tower *TowerGetAt(int16_t x, int16_t y) 151 { 152 for (int i = 0; i < towerCount; i++) 153 { 154 if (towers[i].x == x && towers[i].y == y) 155 { 156 return &towers[i]; 157 } 158 } 159 return 0; 160 } 161 162 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 163 { 164 if (towerCount >= TOWER_MAX_COUNT) 165 { 166 return 0; 167 } 168 169 Tower *tower = TowerGetAt(x, y); 170 if (tower) 171 { 172 return 0; 173 } 174 175 tower = &towers[towerCount++]; 176 tower->x = x; 177 tower->y = y; 178 tower->towerType = towerType; 179 return tower; 180 } 181 182 void TowerDraw() 183 { 184 for (int i = 0; i < towerCount; i++) 185 { 186 Tower tower = towers[i]; 187 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 188 switch (tower.towerType) 189 { 190 case TOWER_TYPE_BASE: 191 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 192 break; 193 case TOWER_TYPE_GUN: 194 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 195 break; 196 } 197 } 198 } 199
200 //# Game 201 202 float nextSpawnTime = 0.0f;
203 204 void InitGame() 205 { 206 TowerInit(); 207 EnemyInit(); 208 209 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 210 TowerTryAdd(TOWER_TYPE_GUN, 2, 0);
211 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4); 212 } 213 214 void GameUpdate() 215 { 216 float dt = GetFrameTime(); 217 // cap maximum delta time to 0.1 seconds to prevent large time steps 218 if (dt > 0.1f) dt = 0.1f; 219 gameTime.time += dt; 220 gameTime.deltaTime = dt; 221 EnemyUpdate(); 222 223 // spawn a new enemy every second 224 if (gameTime.time >= nextSpawnTime) 225 { 226 nextSpawnTime = gameTime.time + 1.0f; 227 // add a new enemy at the boundary of the map 228 int randValue = GetRandomValue(-5, 5); 229 int randSide = GetRandomValue(0, 3); 230 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue; 231 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue; 232 EnemyTryAdd(ENEMY_TYPE_MINION, x, y); 233 }
234 } 235 236 int main(void) 237 { 238 int screenWidth, screenHeight; 239 GetPreferredSize(&screenWidth, &screenHeight); 240 InitWindow(screenWidth, screenHeight, "Tower defense"); 241 SetTargetFPS(30); 242 243 Camera3D camera = {0}; 244 camera.position = (Vector3){0.0f, 10.0f, 5.0f}; 245 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 246 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 247 camera.fovy = 45.0f; 248 camera.projection = CAMERA_PERSPECTIVE; 249 250 InitGame(); 251 252 while (!WindowShouldClose()) 253 { 254 if (IsPaused()) { 255 // canvas is not visible in browser - do nothing 256 continue; 257 } 258 BeginDrawing(); 259 ClearBackground(DARKBLUE); 260 261 BeginMode3D(camera); 262 DrawGrid(10, 1.0f); 263 TowerDraw();
264 EnemyDraw(); 265 GameUpdate();
266 EndMode3D(); 267 268 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 269 EndDrawing(); 270 } 271 272 CloseWindow(); 273 274 return 0; 275 }
  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 #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

The enemy movement is now very rough: They jump from field to field. This makes it hard to see where they are going or coming from. The way we've set it up, each enemy has the time when when they moved to the new field and the information, where they are going. We can use this information now to interpolate the enemy's position between the two fields. This way, the enemy moves smoothly from one field to the next:

  • 💾
  1 #include "td-tut-2-main.h"
  2 #include <stdlib.h>
  3 #include <math.h>
  4 
  5 typedef struct GameTime
  6 {
  7   float time;
  8   float deltaTime;
  9 } GameTime;
 10 
 11 GameTime gameTime = {0};
 12 
 13 //# Enemies
 14 
 15 #define ENEMY_MAX_COUNT 400
 16 #define ENEMY_TYPE_NONE 0
 17 #define ENEMY_TYPE_MINION 1
 18 
 19 typedef struct Enemy
 20 {
 21   int16_t currentX, currentY;
 22   int16_t nextX, nextY;
 23   float startMovingTime;
 24   uint8_t enemyType;
 25 } Enemy;
 26 
 27 Enemy enemies[ENEMY_MAX_COUNT];
 28 int enemyCount = 0;
 29 
 30 void EnemyInit()
 31 {
 32   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 33   {
 34     enemies[i] = (Enemy){0};
 35   }
 36   enemyCount = 0;
 37 }
 38 
39 float EnemyGetCurrentSpeed(Enemy *enemy) 40 { 41 switch (enemy->enemyType) 42 { 43 case ENEMY_TYPE_MINION: 44 return 1.0f; 45 } 46 return 1.0f; 47 } 48
49 void EnemyDraw() 50 { 51 for (int i = 0; i < enemyCount; i++) 52 {
53 Enemy enemy = enemies[i]; 54 float speed = EnemyGetCurrentSpeed(&enemy); 55 float transition = (gameTime.time - enemy.startMovingTime) * speed; 56 57 float x = enemy.currentX + (enemy.nextX - enemy.currentX) * transition; 58 float y = enemy.currentY + (enemy.nextY - enemy.currentY) * transition; 59
60 switch (enemy.enemyType) 61 { 62 case ENEMY_TYPE_MINION:
63 DrawCube((Vector3){x, 0.2f, y}, 0.4f, 0.4f, 0.4f, GREEN);
64 break; 65 } 66 } 67 } 68 69 void EnemyUpdate() 70 { 71 const int16_t castleX = 0; 72 const int16_t castleY = 0; 73 74 for (int i = 0; i < enemyCount; i++) 75 { 76 Enemy *enemy = &enemies[i]; 77 if (enemy->enemyType == ENEMY_TYPE_NONE) 78 { 79 continue; 80 }
81 float speed = EnemyGetCurrentSpeed(enemy);
82 float transition = (gameTime.time - enemy->startMovingTime) * speed; 83 if (transition >= 1.0f) 84 { 85 enemy->startMovingTime = gameTime.time; 86 enemy->currentX = enemy->nextX; 87 enemy->currentY = enemy->nextY; 88 int16_t dx = castleX - enemy->currentX; 89 int16_t dy = castleY - enemy->currentY; 90 if (dx == 0 && dy == 0) 91 { 92 // enemy reached the castle; remove it 93 enemy->enemyType = ENEMY_TYPE_NONE; 94 continue; 95 } 96 if (abs(dx) > abs(dy)) 97 { 98 enemy->nextX = enemy->currentX + (dx > 0 ? 1 : -1); 99 enemy->nextY = enemy->currentY; 100 } 101 else 102 { 103 enemy->nextX = enemy->currentX; 104 enemy->nextY = enemy->currentY + (dy > 0 ? 1 : -1); 105 } 106 } 107 } 108 } 109 110 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 111 { 112 Enemy *spawn = 0; 113 for (int i = 0; i < enemyCount; i++) 114 { 115 Enemy *enemy = &enemies[i]; 116 if (enemy->enemyType == ENEMY_TYPE_NONE) 117 { 118 spawn = enemy; 119 break; 120 } 121 } 122 123 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 124 { 125 spawn = &enemies[enemyCount++]; 126 } 127 128 if (spawn) 129 { 130 spawn->currentX = currentX; 131 spawn->currentY = currentY; 132 spawn->nextX = currentX; 133 spawn->nextY = currentY; 134 spawn->enemyType = enemyType; 135 spawn->startMovingTime = 0; 136 } 137 138 return spawn; 139 } 140 141 //# Towers 142 143 #define TOWER_MAX_COUNT 400 144 #define TOWER_TYPE_NONE 0 145 #define TOWER_TYPE_BASE 1 146 #define TOWER_TYPE_GUN 2 147 148 typedef struct Tower 149 { 150 int16_t x, y; 151 uint8_t towerType; 152 } Tower; 153 154 Tower towers[TOWER_MAX_COUNT]; 155 int towerCount = 0; 156 157 void TowerInit() 158 { 159 for (int i = 0; i < TOWER_MAX_COUNT; i++) 160 { 161 towers[i] = (Tower){0}; 162 } 163 towerCount = 0; 164 } 165 166 Tower *TowerGetAt(int16_t x, int16_t y) 167 { 168 for (int i = 0; i < towerCount; i++) 169 { 170 if (towers[i].x == x && towers[i].y == y) 171 { 172 return &towers[i]; 173 } 174 } 175 return 0; 176 } 177 178 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 179 { 180 if (towerCount >= TOWER_MAX_COUNT) 181 { 182 return 0; 183 } 184 185 Tower *tower = TowerGetAt(x, y); 186 if (tower) 187 { 188 return 0; 189 } 190 191 tower = &towers[towerCount++]; 192 tower->x = x; 193 tower->y = y; 194 tower->towerType = towerType; 195 return tower; 196 } 197 198 void TowerDraw() 199 { 200 for (int i = 0; i < towerCount; i++) 201 { 202 Tower tower = towers[i]; 203 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 204 switch (tower.towerType) 205 { 206 case TOWER_TYPE_BASE: 207 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 208 break; 209 case TOWER_TYPE_GUN: 210 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 211 break; 212 } 213 } 214 } 215 216 //# Game 217 218 float nextSpawnTime = 0.0f; 219 220 void InitGame() 221 { 222 TowerInit(); 223 EnemyInit(); 224 225 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 226 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 227 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4); 228 } 229 230 void GameUpdate() 231 { 232 float dt = GetFrameTime(); 233 // cap maximum delta time to 0.1 seconds to prevent large time steps 234 if (dt > 0.1f) dt = 0.1f; 235 gameTime.time += dt; 236 gameTime.deltaTime = dt; 237 EnemyUpdate(); 238 239 // spawn a new enemy every second 240 if (gameTime.time >= nextSpawnTime) 241 { 242 nextSpawnTime = gameTime.time + 1.0f; 243 // add a new enemy at the boundary of the map 244 int randValue = GetRandomValue(-5, 5); 245 int randSide = GetRandomValue(0, 3); 246 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue; 247 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue; 248 EnemyTryAdd(ENEMY_TYPE_MINION, x, y); 249 } 250 } 251 252 int main(void) 253 { 254 int screenWidth, screenHeight; 255 GetPreferredSize(&screenWidth, &screenHeight); 256 InitWindow(screenWidth, screenHeight, "Tower defense"); 257 SetTargetFPS(30); 258 259 Camera3D camera = {0}; 260 camera.position = (Vector3){0.0f, 10.0f, 5.0f}; 261 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 262 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 263 camera.fovy = 45.0f; 264 camera.projection = CAMERA_PERSPECTIVE; 265 266 InitGame(); 267 268 while (!WindowShouldClose()) 269 { 270 if (IsPaused()) { 271 // canvas is not visible in browser - do nothing 272 continue; 273 } 274 BeginDrawing(); 275 ClearBackground(DARKBLUE); 276 277 BeginMode3D(camera); 278 DrawGrid(10, 1.0f); 279 TowerDraw(); 280 EnemyDraw(); 281 GameUpdate(); 282 EndMode3D(); 283 284 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 285 EndDrawing(); 286 } 287 288 CloseWindow(); 289 290 return 0; 291 }
  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 #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

The movement is now quite smooth, even though they still move in straight lines. For now, this is good enough, since we no longer have problems to recognize the enemy movement. The next step is to have the towers shoot at the enemies. There are several properties we have to consider:

  • 💾
  1 #include "td-tut-2-main.h"
  2 #include <stdlib.h>
  3 #include <math.h>
  4 
  5 typedef struct GameTime
  6 {
  7   float time;
  8   float deltaTime;
  9 } GameTime;
 10 
 11 GameTime gameTime = {0};
 12 
 13 //# Enemies
 14 
 15 #define ENEMY_MAX_COUNT 400
 16 #define ENEMY_TYPE_NONE 0
 17 #define ENEMY_TYPE_MINION 1
 18 
 19 typedef struct Enemy
 20 {
 21   int16_t currentX, currentY;
 22   int16_t nextX, nextY;
 23   float startMovingTime;
 24   uint8_t enemyType;
 25 } Enemy;
 26 
 27 Enemy enemies[ENEMY_MAX_COUNT];
 28 int enemyCount = 0;
 29 
 30 void EnemyInit()
 31 {
 32   for (int i = 0; i < ENEMY_MAX_COUNT; i++)
 33   {
 34     enemies[i] = (Enemy){0};
 35   }
 36   enemyCount = 0;
 37 }
 38 
 39 float EnemyGetCurrentSpeed(Enemy *enemy)
 40 {
 41   switch (enemy->enemyType)
 42   {
 43   case ENEMY_TYPE_MINION:
 44     return 1.0f;
 45   }
 46   return 1.0f;
 47 }
 48 
 49 void EnemyDraw()
 50 {
 51   for (int i = 0; i < enemyCount; i++)
 52   {
 53     Enemy enemy = enemies[i];
 54     float speed = EnemyGetCurrentSpeed(&enemy);
 55     float transition = (gameTime.time - enemy.startMovingTime) * speed;
 56 
 57     float x = enemy.currentX + (enemy.nextX - enemy.currentX) * transition;
 58     float y = enemy.currentY + (enemy.nextY - enemy.currentY) * transition;
 59     
 60     switch (enemy.enemyType)
 61     {
 62     case ENEMY_TYPE_MINION:
 63       DrawCube((Vector3){x, 0.2f, y}, 0.4f, 0.4f, 0.4f, GREEN);
 64       break;
 65     }
 66   }
 67 }
 68 
 69 void EnemyUpdate()
 70 {
 71   const int16_t castleX = 0;
 72   const int16_t castleY = 0;
 73 
 74   for (int i = 0; i < enemyCount; i++)
 75   {
 76     Enemy *enemy = &enemies[i];
 77     if (enemy->enemyType == ENEMY_TYPE_NONE)
 78     {
 79       continue;
 80     }
 81     float speed = EnemyGetCurrentSpeed(enemy);
 82     float transition = (gameTime.time - enemy->startMovingTime) * speed;
 83     if (transition >= 1.0f)
 84     {
 85       enemy->startMovingTime = gameTime.time;
 86       enemy->currentX = enemy->nextX;
 87       enemy->currentY = enemy->nextY;
 88       int16_t dx = castleX - enemy->currentX;
 89       int16_t dy = castleY - enemy->currentY;
 90       if (dx == 0 && dy == 0)
 91       {
 92         // enemy reached the castle; remove it
 93         enemy->enemyType = ENEMY_TYPE_NONE;
 94         continue;
 95       }
 96       if (abs(dx) > abs(dy))
 97       {
 98         enemy->nextX = enemy->currentX + (dx > 0 ? 1 : -1);
 99         enemy->nextY = enemy->currentY;
100       }
101       else
102       {
103         enemy->nextX = enemy->currentX;
104         enemy->nextY = enemy->currentY + (dy > 0 ? 1 : -1);
105       }
106     }
107   }
108 }
109 
110 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY)
111 {
112   Enemy *spawn = 0;
113   for (int i = 0; i < enemyCount; i++)
114   {
115     Enemy *enemy = &enemies[i];
116     if (enemy->enemyType == ENEMY_TYPE_NONE)
117     {
118       spawn = enemy;
119       break;
120     }
121   }
122 
123   if (enemyCount < ENEMY_MAX_COUNT && !spawn)
124   {
125     spawn = &enemies[enemyCount++];
126   }
127 
128   if (spawn)
129   {
130     spawn->currentX = currentX;
131     spawn->currentY = currentY;
132     spawn->nextX = currentX;
133     spawn->nextY = currentY;
134     spawn->enemyType = enemyType;
135     spawn->startMovingTime = 0;
136   }
137 
138   return spawn;
139 }
140 
141 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 142 { 143 int16_t castleX = 0; 144 int16_t castleY = 0; 145 Enemy* closest = 0; 146 int16_t closestDistance = 0; 147 float range2 = range * range; 148 for (int i = 0; i < enemyCount; i++) 149 { 150 Enemy* enemy = &enemies[i]; 151 if (enemy->enemyType == ENEMY_TYPE_NONE) 152 { 153 continue; 154 } 155 int16_t dx = castleX - enemy->currentX; 156 int16_t dy = castleY - enemy->currentY; 157 int16_t distance = abs(dx) + abs(dy); 158 if (!closest || distance < closestDistance) 159 { 160 float tdx = towerX - enemy->currentX; 161 float tdy = towerY - enemy->currentY; 162 float tdistance2 = tdx * tdx + tdy * tdy; 163 if (tdistance2 <= range2) 164 { 165 closest = enemy; 166 closestDistance = distance; 167 } 168 } 169 } 170 return closest; 171 } 172
173 //# Towers 174 175 #define TOWER_MAX_COUNT 400 176 #define TOWER_TYPE_NONE 0 177 #define TOWER_TYPE_BASE 1 178 #define TOWER_TYPE_GUN 2 179 180 typedef struct Tower 181 { 182 int16_t x, y;
183 uint8_t towerType; 184 float cooldown;
185 } Tower; 186 187 Tower towers[TOWER_MAX_COUNT]; 188 int towerCount = 0; 189 190 void TowerInit() 191 { 192 for (int i = 0; i < TOWER_MAX_COUNT; i++) 193 { 194 towers[i] = (Tower){0}; 195 } 196 towerCount = 0; 197 } 198 199 Tower *TowerGetAt(int16_t x, int16_t y) 200 { 201 for (int i = 0; i < towerCount; i++) 202 { 203 if (towers[i].x == x && towers[i].y == y) 204 { 205 return &towers[i]; 206 } 207 } 208 return 0; 209 } 210 211 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 212 { 213 if (towerCount >= TOWER_MAX_COUNT) 214 { 215 return 0; 216 } 217 218 Tower *tower = TowerGetAt(x, y); 219 if (tower) 220 { 221 return 0; 222 } 223 224 tower = &towers[towerCount++]; 225 tower->x = x; 226 tower->y = y; 227 tower->towerType = towerType; 228 return tower; 229 } 230 231 void TowerDraw() 232 { 233 for (int i = 0; i < towerCount; i++) 234 { 235 Tower tower = towers[i]; 236 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 237 switch (tower.towerType) 238 { 239 case TOWER_TYPE_BASE: 240 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 241 break; 242 case TOWER_TYPE_GUN:
243 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 244 break; 245 } 246 } 247 } 248 249 void TowerGunUpdate(Tower *tower) 250 { 251 if (tower->cooldown <= 0) 252 { 253 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 254 if (enemy) 255 { 256 tower->cooldown = 1.0f; 257 // shoot the enemy 258 enemy->enemyType = ENEMY_TYPE_NONE; 259 } 260 } 261 else 262 { 263 tower->cooldown -= gameTime.deltaTime; 264 } 265 } 266 267 void TowerUpdate() 268 { 269 for (int i = 0; i < towerCount; i++) 270 { 271 Tower *tower = &towers[i]; 272 switch (tower->towerType) 273 { 274 case TOWER_TYPE_GUN: 275 TowerGunUpdate(tower);
276 break; 277 } 278 } 279 } 280 281 //# Game 282 283 float nextSpawnTime = 0.0f; 284 285 void InitGame() 286 { 287 TowerInit(); 288 EnemyInit(); 289 290 TowerTryAdd(TOWER_TYPE_BASE, 0, 0);
291 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 292 TowerTryAdd(TOWER_TYPE_GUN, -2, 0);
293 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4); 294 } 295 296 void GameUpdate() 297 { 298 float dt = GetFrameTime(); 299 // cap maximum delta time to 0.1 seconds to prevent large time steps 300 if (dt > 0.1f) dt = 0.1f; 301 gameTime.time += dt; 302 gameTime.deltaTime = dt;
303 EnemyUpdate(); 304 TowerUpdate();
305 306 // spawn a new enemy every second 307 if (gameTime.time >= nextSpawnTime) 308 { 309 nextSpawnTime = gameTime.time + 1.0f; 310 // add a new enemy at the boundary of the map 311 int randValue = GetRandomValue(-5, 5); 312 int randSide = GetRandomValue(0, 3); 313 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue; 314 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue; 315 EnemyTryAdd(ENEMY_TYPE_MINION, x, y); 316 } 317 } 318 319 int main(void) 320 { 321 int screenWidth, screenHeight; 322 GetPreferredSize(&screenWidth, &screenHeight); 323 InitWindow(screenWidth, screenHeight, "Tower defense"); 324 SetTargetFPS(30); 325 326 Camera3D camera = {0}; 327 camera.position = (Vector3){0.0f, 10.0f, 5.0f}; 328 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 329 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 330 camera.fovy = 45.0f; 331 camera.projection = CAMERA_PERSPECTIVE; 332 333 InitGame(); 334 335 while (!WindowShouldClose()) 336 { 337 if (IsPaused()) { 338 // canvas is not visible in browser - do nothing 339 continue; 340 } 341 BeginDrawing(); 342 ClearBackground(DARKBLUE); 343 344 BeginMode3D(camera); 345 DrawGrid(10, 1.0f); 346 TowerDraw(); 347 EnemyDraw(); 348 GameUpdate(); 349 EndMode3D(); 350 351 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 352 EndDrawing(); 353 } 354 355 CloseWindow(); 356 357 return 0; 358 }
  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 #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

Once the enemies come into range of our tower, they currently just magically disappear.

This is highly unsatisfying. We want to see the enemies getting hit and dying.

There are several ways to implement this. In most games, the projectiles have a fairly slow movement speed. If that's the case, we'd need to predict where the enemy is going to be when the projectile hits the enemy - sometimes the projectiles can even miss the enemy.

Projectiles add more complexity, but they also take some off: If we don't handle the shooting and damage handling via projectiles, we have to set up alternative systems that does the same - like timers that trigger the damage over time, phases for handling the shooting and so on.

So let's take now the more thorny path and implement projectiles. A quick overview of what that means:

  • 💾
  1 #include "td-tut-2-main.h"
2 #include <raymath.h>
3 #include <stdlib.h> 4 #include <math.h> 5 6 typedef struct GameTime 7 { 8 float time; 9 float deltaTime; 10 } GameTime; 11 12 GameTime gameTime = {0}; 13 14 //# Enemies 15 16 #define ENEMY_MAX_COUNT 400 17 #define ENEMY_TYPE_NONE 0
18 #define ENEMY_TYPE_MINION 1 19 20 typedef struct EnemyId 21 { 22 uint16_t index; 23 uint16_t generation; 24 } EnemyId;
25 26 typedef struct Enemy 27 { 28 int16_t currentX, currentY;
29 int16_t nextX, nextY; 30 uint16_t generation;
31 float startMovingTime; 32 uint8_t enemyType; 33 } Enemy; 34 35 Enemy enemies[ENEMY_MAX_COUNT]; 36 int enemyCount = 0; 37 38 void EnemyInit() 39 { 40 for (int i = 0; i < ENEMY_MAX_COUNT; i++) 41 { 42 enemies[i] = (Enemy){0}; 43 } 44 enemyCount = 0; 45 } 46 47 float EnemyGetCurrentSpeed(Enemy *enemy) 48 { 49 switch (enemy->enemyType) 50 { 51 case ENEMY_TYPE_MINION: 52 return 1.0f; 53 } 54 return 1.0f; 55 } 56 57 void EnemyDraw() 58 { 59 for (int i = 0; i < enemyCount; i++) 60 { 61 Enemy enemy = enemies[i]; 62 float speed = EnemyGetCurrentSpeed(&enemy); 63 float transition = (gameTime.time - enemy.startMovingTime) * speed; 64 65 float x = enemy.currentX + (enemy.nextX - enemy.currentX) * transition; 66 float y = enemy.currentY + (enemy.nextY - enemy.currentY) * transition; 67 68 switch (enemy.enemyType) 69 { 70 case ENEMY_TYPE_MINION: 71 DrawCube((Vector3){x, 0.2f, y}, 0.4f, 0.4f, 0.4f, GREEN); 72 break; 73 } 74 } 75 } 76 77 void EnemyUpdate() 78 { 79 const int16_t castleX = 0; 80 const int16_t castleY = 0; 81 82 for (int i = 0; i < enemyCount; i++) 83 { 84 Enemy *enemy = &enemies[i]; 85 if (enemy->enemyType == ENEMY_TYPE_NONE) 86 { 87 continue; 88 } 89 float speed = EnemyGetCurrentSpeed(enemy); 90 float transition = (gameTime.time - enemy->startMovingTime) * speed; 91 if (transition >= 1.0f) 92 { 93 enemy->startMovingTime = gameTime.time; 94 enemy->currentX = enemy->nextX; 95 enemy->currentY = enemy->nextY; 96 int16_t dx = castleX - enemy->currentX; 97 int16_t dy = castleY - enemy->currentY; 98 if (dx == 0 && dy == 0) 99 { 100 // enemy reached the castle; remove it 101 enemy->enemyType = ENEMY_TYPE_NONE; 102 continue; 103 } 104 if (abs(dx) > abs(dy)) 105 { 106 enemy->nextX = enemy->currentX + (dx > 0 ? 1 : -1); 107 enemy->nextY = enemy->currentY; 108 } 109 else 110 { 111 enemy->nextX = enemy->currentX;
112 enemy->nextY = enemy->currentY + (dy > 0 ? 1 : -1); 113 } 114 } 115 } 116 } 117 118 EnemyId EnemyGetId(Enemy *enemy) 119 { 120 return (EnemyId){enemy - enemies, enemy->generation}; 121 } 122 123 Enemy *EnemyTryResolve(EnemyId enemyId) 124 { 125 if (enemyId.index >= ENEMY_MAX_COUNT) 126 { 127 return 0; 128 } 129 Enemy *enemy = &enemies[enemyId.index]; 130 if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE) 131 { 132 return 0; 133 } 134 return enemy;
135 } 136 137 Enemy *EnemyTryAdd(uint8_t enemyType, int16_t currentX, int16_t currentY) 138 { 139 Enemy *spawn = 0; 140 for (int i = 0; i < enemyCount; i++) 141 { 142 Enemy *enemy = &enemies[i]; 143 if (enemy->enemyType == ENEMY_TYPE_NONE) 144 { 145 spawn = enemy; 146 break; 147 } 148 } 149 150 if (enemyCount < ENEMY_MAX_COUNT && !spawn) 151 { 152 spawn = &enemies[enemyCount++]; 153 } 154 155 if (spawn) 156 { 157 spawn->currentX = currentX; 158 spawn->currentY = currentY; 159 spawn->nextX = currentX; 160 spawn->nextY = currentY; 161 spawn->enemyType = enemyType;
162 spawn->startMovingTime = 0; 163 spawn->generation++;
164 } 165 166 return spawn; 167 } 168 169 Enemy* EnemyGetClosestToCastle(int16_t towerX, int16_t towerY, float range) 170 { 171 int16_t castleX = 0; 172 int16_t castleY = 0; 173 Enemy* closest = 0; 174 int16_t closestDistance = 0; 175 float range2 = range * range; 176 for (int i = 0; i < enemyCount; i++) 177 { 178 Enemy* enemy = &enemies[i]; 179 if (enemy->enemyType == ENEMY_TYPE_NONE) 180 { 181 continue; 182 } 183 int16_t dx = castleX - enemy->currentX; 184 int16_t dy = castleY - enemy->currentY; 185 int16_t distance = abs(dx) + abs(dy); 186 if (!closest || distance < closestDistance) 187 { 188 float tdx = towerX - enemy->currentX; 189 float tdy = towerY - enemy->currentY; 190 float tdistance2 = tdx * tdx + tdy * tdy; 191 if (tdistance2 <= range2) 192 { 193 closest = enemy; 194 closestDistance = distance; 195 } 196 } 197 }
198 return closest; 199 } 200 201 //# Projectiles 202 #define PROJECTILE_MAX_COUNT 1200 203 #define PROJECTILE_TYPE_NONE 0 204 #define PROJECTILE_TYPE_BULLET 1 205 206 typedef struct Projectile 207 { 208 uint8_t projectileType; 209 float shootTime; 210 float arrivalTime; 211 float damage; 212 Vector2 position; 213 Vector2 target; 214 Vector2 directionNormal; 215 EnemyId targetEnemy; 216 } Projectile; 217 218 Projectile projectiles[PROJECTILE_MAX_COUNT]; 219 int projectileCount = 0; 220 221 void ProjectileInit() 222 { 223 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 224 { 225 projectiles[i] = (Projectile){0}; 226 } 227 } 228 229 void ProjectileDraw() 230 { 231 for (int i = 0; i < projectileCount; i++) 232 { 233 Projectile projectile = projectiles[i]; 234 if (projectile.projectileType == PROJECTILE_TYPE_NONE) 235 { 236 continue; 237 } 238 float transition = (gameTime.time - projectile.shootTime) / (projectile.arrivalTime - projectile.shootTime); 239 if (transition >= 1.0f) 240 { 241 continue; 242 } 243 Vector2 position = Vector2Lerp(projectile.position, projectile.target, transition); 244 float x = position.x; 245 float y = position.y; 246 float dx = projectile.directionNormal.x; 247 float dy = projectile.directionNormal.y; 248 for (float d = 1.0f; d > 0.0f; d -= 0.25f) 249 { 250 x -= dx * 0.1f; 251 y -= dy * 0.1f; 252 float size = 0.1f * d; 253 DrawCube((Vector3){x, 0.2f, y}, size, size, size, RED); 254 } 255 } 256 } 257 258 void ProjectileUpdate() 259 { 260 for (int i = 0; i < projectileCount; i++) 261 { 262 Projectile *projectile = &projectiles[i]; 263 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 264 { 265 continue; 266 } 267 float transition = (gameTime.time - projectile->shootTime) / (projectile->arrivalTime - projectile->shootTime); 268 if (transition >= 1.0f) 269 { 270 projectile->projectileType = PROJECTILE_TYPE_NONE; 271 Enemy *enemy = EnemyTryResolve(projectile->targetEnemy); 272 if (enemy) 273 { 274 enemy->enemyType = ENEMY_TYPE_NONE; 275 } 276 continue; 277 } 278 } 279 } 280 281 Projectile *ProjectileTryAdd(uint8_t projectileType, Enemy *enemy, Vector2 position, Vector2 target, float speed, float damage) 282 { 283 for (int i = 0; i < PROJECTILE_MAX_COUNT; i++) 284 { 285 Projectile *projectile = &projectiles[i]; 286 if (projectile->projectileType == PROJECTILE_TYPE_NONE) 287 { 288 projectile->projectileType = projectileType; 289 projectile->shootTime = gameTime.time; 290 projectile->arrivalTime = gameTime.time + Vector2Distance(position, target) / speed; 291 projectile->damage = damage; 292 projectile->position = position; 293 projectile->target = target; 294 projectile->directionNormal = Vector2Normalize(Vector2Subtract(target, position)); 295 projectile->targetEnemy = EnemyGetId(enemy); 296 projectileCount = projectileCount <= i ? i + 1 : projectileCount; 297 return projectile; 298 } 299 } 300 return 0;
301 } 302 303 //# Towers 304 305 #define TOWER_MAX_COUNT 400 306 #define TOWER_TYPE_NONE 0 307 #define TOWER_TYPE_BASE 1 308 #define TOWER_TYPE_GUN 2 309 310 typedef struct Tower 311 { 312 int16_t x, y; 313 uint8_t towerType; 314 float cooldown; 315 } Tower; 316 317 Tower towers[TOWER_MAX_COUNT]; 318 int towerCount = 0; 319 320 void TowerInit() 321 { 322 for (int i = 0; i < TOWER_MAX_COUNT; i++) 323 { 324 towers[i] = (Tower){0}; 325 } 326 towerCount = 0; 327 } 328 329 Tower *TowerGetAt(int16_t x, int16_t y) 330 { 331 for (int i = 0; i < towerCount; i++) 332 { 333 if (towers[i].x == x && towers[i].y == y) 334 { 335 return &towers[i]; 336 } 337 } 338 return 0; 339 } 340 341 Tower *TowerTryAdd(uint8_t towerType, int16_t x, int16_t y) 342 { 343 if (towerCount >= TOWER_MAX_COUNT) 344 { 345 return 0; 346 } 347 348 Tower *tower = TowerGetAt(x, y); 349 if (tower) 350 { 351 return 0; 352 } 353 354 tower = &towers[towerCount++]; 355 tower->x = x; 356 tower->y = y; 357 tower->towerType = towerType; 358 return tower; 359 } 360 361 void TowerDraw() 362 { 363 for (int i = 0; i < towerCount; i++) 364 { 365 Tower tower = towers[i]; 366 DrawCube((Vector3){tower.x, 0.125f, tower.y}, 1.0f, 0.25f, 1.0f, GRAY); 367 switch (tower.towerType) 368 { 369 case TOWER_TYPE_BASE: 370 DrawCube((Vector3){tower.x, 0.4f, tower.y}, 0.8f, 0.8f, 0.8f, MAROON); 371 break; 372 case TOWER_TYPE_GUN: 373 DrawCube((Vector3){tower.x, 0.2f, tower.y}, 0.8f, 0.4f, 0.8f, DARKPURPLE); 374 break; 375 } 376 } 377 } 378 379 void TowerGunUpdate(Tower *tower) 380 { 381 if (tower->cooldown <= 0) 382 { 383 Enemy *enemy = EnemyGetClosestToCastle(tower->x, tower->y, 3.0f); 384 if (enemy) 385 {
386 tower->cooldown = 0.5f;
387 // shoot the enemy
388 ProjectileTryAdd(PROJECTILE_TYPE_BULLET, enemy, (Vector2){tower->x, tower->y}, (Vector2){enemy->currentX, enemy->currentY}, 5.0f, 1.0f);
389 } 390 } 391 else 392 { 393 tower->cooldown -= gameTime.deltaTime; 394 } 395 } 396 397 void TowerUpdate() 398 { 399 for (int i = 0; i < towerCount; i++) 400 { 401 Tower *tower = &towers[i]; 402 switch (tower->towerType) 403 { 404 case TOWER_TYPE_GUN: 405 TowerGunUpdate(tower); 406 break; 407 } 408 } 409 } 410 411 //# Game 412 413 float nextSpawnTime = 0.0f; 414 415 void InitGame() 416 { 417 TowerInit();
418 EnemyInit(); 419 ProjectileInit();
420 421 TowerTryAdd(TOWER_TYPE_BASE, 0, 0); 422 TowerTryAdd(TOWER_TYPE_GUN, 2, 0); 423 TowerTryAdd(TOWER_TYPE_GUN, -2, 0); 424 EnemyTryAdd(ENEMY_TYPE_MINION, 5, 4); 425 } 426 427 void GameUpdate() 428 { 429 float dt = GetFrameTime(); 430 // cap maximum delta time to 0.1 seconds to prevent large time steps 431 if (dt > 0.1f) dt = 0.1f; 432 gameTime.time += dt; 433 gameTime.deltaTime = dt; 434 EnemyUpdate();
435 TowerUpdate(); 436 ProjectileUpdate();
437 438 // spawn a new enemy every second 439 if (gameTime.time >= nextSpawnTime) 440 { 441 nextSpawnTime = gameTime.time + 1.0f; 442 // add a new enemy at the boundary of the map 443 int randValue = GetRandomValue(-5, 5); 444 int randSide = GetRandomValue(0, 3); 445 int16_t x = randSide == 0 ? -5 : randSide == 1 ? 5 : randValue; 446 int16_t y = randSide == 2 ? -5 : randSide == 3 ? 5 : randValue; 447 EnemyTryAdd(ENEMY_TYPE_MINION, x, y); 448 } 449 } 450 451 int main(void) 452 { 453 int screenWidth, screenHeight; 454 GetPreferredSize(&screenWidth, &screenHeight); 455 InitWindow(screenWidth, screenHeight, "Tower defense"); 456 SetTargetFPS(30); 457 458 Camera3D camera = {0}; 459 camera.position = (Vector3){0.0f, 10.0f, 5.0f}; 460 camera.target = (Vector3){0.0f, 0.0f, 0.0f}; 461 camera.up = (Vector3){0.0f, 0.0f, -1.0f}; 462 camera.fovy = 45.0f; 463 camera.projection = CAMERA_PERSPECTIVE; 464 465 InitGame(); 466 467 while (!WindowShouldClose()) 468 { 469 if (IsPaused()) { 470 // canvas is not visible in browser - do nothing 471 continue; 472 } 473 BeginDrawing(); 474 ClearBackground(DARKBLUE); 475 476 BeginMode3D(camera); 477 DrawGrid(10, 1.0f); 478 TowerDraw();
479 EnemyDraw(); 480 ProjectileDraw();
481 GameUpdate(); 482 EndMode3D(); 483 484 DrawText("Tower defense tutorial", 5, 5, 20, WHITE); 485 EndDrawing(); 486 } 487 488 CloseWindow(); 489 490 return 0; 491 }
  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 #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

While the newly added code is very similar to the existing code, I added one new concept:

  1 typedef struct EnemyId
  2 {
  3   uint16_t index;
  4   uint16_t generation;
  5 } EnemyId;
  6 
  7 EnemyId EnemyGetId(Enemy *enemy)
  8 {
  9   return (EnemyId){enemy - enemies, enemy->generation};
 10 }
 11 
 12 Enemy *EnemyTryResolve(EnemyId enemyId)
 13 {
 14   if (enemyId.index >= ENEMY_MAX_COUNT)
 15   {
 16     return 0;
 17   }
 18   Enemy *enemy = &enemies[enemyId.index];
 19   if (enemy->generation != enemyId.generation || enemy->enemyType == ENEMY_TYPE_NONE)
 20   {
 21     return 0;
 22   }
 23   return enemy;
 24 }

It is important to understand why this is needed here: The first solution to refer to an enemy is to use a simple pointer. Since we use only fixed size arrays, there's no chance to run into pointer invalidation issues. But we still do have "object" lifetimes: When an enemy dies, the slot is free to be used by another enemy. If we use a pointer, we do not know if it still is the very same enemy that existed when the projectile was created and the result would be that newly created enemies could take damage from projectiles that were meant for the enemy that died before and used the same index.

The generation counter solves this problem (in theory, it could still happen that the generation overflows, but the chances for that are very slim; for a 16bit generation and 400 enemies, we'd need to spawn 400 * 65536 enemies within the lifetime of a bullet to produce this kind of bug).

By increasing the generation integer each time we spawn an enemy, we can be sure that any existing reference will become invalid when the enemy dies and a new enemy is spawned. Moreover, this also handles the case to check if the enemy is still alive when the bullet hits, since the resolving function already does this check for us.

Generational indices are a very useful concept to deal with object lifetime invalidation and since I adopted this concept, I have reduced the amount of memory corruption bugs by a significant amount.

Wrapup

The basic structure is established: We have a grid, enemies that move towards the castle, towers that shoot at the enemies and projectiles that deal damage to the enemies.

The next part will cover the following topics:

I hope you enjoyed the tutorial so far. If you have any questions or suggestions, please let me know. I will publish the next part next Friday, the 22nd of November.

đŸĒ