├── .gitignore ├── images ├── step_001.png ├── step_002.png ├── step_011.png ├── step_012.png ├── step_015.gif ├── step_021.gif ├── step_022.gif ├── step_022b.gif ├── step_023.gif ├── step_023b.gif ├── step_031.gif ├── step_032.gif ├── step_032b.gif ├── step_033.gif ├── step_034.gif ├── step_043.gif ├── step_051.gif ├── step_052.gif ├── step_071.gif ├── step_072.gif ├── step_081.gif ├── step_082.gif ├── step_082b.gif └── step_083a.gif ├── docs ├── _template │ ├── jsconfig.json │ └── main.js ├── chargerush │ ├── jsconfig.json │ └── main.js ├── step_00 │ ├── jsconfig.json │ └── main.js ├── step_01 │ ├── jsconfig.json │ └── main.js ├── step_02 │ ├── jsconfig.json │ └── main.js ├── step_03 │ ├── jsconfig.json │ └── main.js ├── step_04 │ ├── jsconfig.json │ └── main.js ├── step_05 │ ├── jsconfig.json │ └── main.js ├── step_06 │ ├── jsconfig.json │ └── main.js ├── step_07 │ ├── jsconfig.json │ └── main.js ├── step_08 │ ├── jsconfig.json │ └── main.js ├── index.html ├── characters.js └── bundle.d.ts ├── package.json ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | .cache/ 4 | tmp/ 5 | package-lock.json 6 | npm-debug.log -------------------------------------------------------------------------------- /images/step_001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_001.png -------------------------------------------------------------------------------- /images/step_002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_002.png -------------------------------------------------------------------------------- /images/step_011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_011.png -------------------------------------------------------------------------------- /images/step_012.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_012.png -------------------------------------------------------------------------------- /images/step_015.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_015.gif -------------------------------------------------------------------------------- /images/step_021.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_021.gif -------------------------------------------------------------------------------- /images/step_022.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_022.gif -------------------------------------------------------------------------------- /images/step_022b.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_022b.gif -------------------------------------------------------------------------------- /images/step_023.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_023.gif -------------------------------------------------------------------------------- /images/step_023b.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_023b.gif -------------------------------------------------------------------------------- /images/step_031.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_031.gif -------------------------------------------------------------------------------- /images/step_032.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_032.gif -------------------------------------------------------------------------------- /images/step_032b.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_032b.gif -------------------------------------------------------------------------------- /images/step_033.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_033.gif -------------------------------------------------------------------------------- /images/step_034.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_034.gif -------------------------------------------------------------------------------- /images/step_043.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_043.gif -------------------------------------------------------------------------------- /images/step_051.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_051.gif -------------------------------------------------------------------------------- /images/step_052.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_052.gif -------------------------------------------------------------------------------- /images/step_071.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_071.gif -------------------------------------------------------------------------------- /images/step_072.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_072.gif -------------------------------------------------------------------------------- /images/step_081.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_081.gif -------------------------------------------------------------------------------- /images/step_082.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_082.gif -------------------------------------------------------------------------------- /images/step_082b.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_082b.gif -------------------------------------------------------------------------------- /images/step_083a.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/HEAD/images/step_083a.gif -------------------------------------------------------------------------------- /docs/_template/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "checkJs": true 5 | }, 6 | "include": ["*.js", "../bundle.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/chargerush/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "checkJs": true 5 | }, 6 | "include": ["*.js", "../bundle.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/step_00/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "checkJs": true 5 | }, 6 | "include": ["*.js", "../bundle.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/step_01/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "checkJs": true 5 | }, 6 | "include": ["*.js", "../bundle.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/step_02/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "checkJs": true 5 | }, 6 | "include": ["*.js", "../bundle.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/step_03/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "checkJs": true 5 | }, 6 | "include": ["*.js", "../bundle.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/step_04/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "checkJs": true 5 | }, 6 | "include": ["*.js", "../bundle.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/step_05/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "checkJs": true 5 | }, 6 | "include": ["*.js", "../bundle.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/step_06/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "checkJs": true 5 | }, 6 | "include": ["*.js", "../bundle.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/step_07/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "checkJs": true 5 | }, 6 | "include": ["*.js", "../bundle.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/step_08/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "checkJs": true 5 | }, 6 | "include": ["*.js", "../bundle.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/_template/main.js: -------------------------------------------------------------------------------- 1 | title = ""; 2 | 3 | description = ` 4 | `; 5 | 6 | characters = []; 7 | 8 | options = {}; 9 | 10 | function update() { 11 | if (!ticks) { 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/step_00/main.js: -------------------------------------------------------------------------------- 1 | title = ""; 2 | 3 | description = ` 4 | `; 5 | 6 | characters = []; 7 | 8 | options = {}; 9 | 10 | function update() { 11 | if (!ticks) { 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crisp-game-lib-tutorial", 3 | "version": "1.0.0", 4 | "description": "A tutorial for CrispGameLib", 5 | "scripts": { 6 | "watch_games": "light-server -s docs -w \"docs/**/* # # reload\"" 7 | }, 8 | "author": "abagames", 9 | "license": "MIT", 10 | "devDependencies": { 11 | "light-server": "^2.7.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | crisp-game-lib 6 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Juno Nguyen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/step_01/main.js: -------------------------------------------------------------------------------- 1 | // The title of the game to be displayed on the title screen 2 | title = "CHARGE RUSH"; 3 | 4 | // The description, which is also displayed on the title screen 5 | description = ` 6 | `; 7 | 8 | // The array of custom sprites 9 | characters = []; 10 | 11 | // Game design variable container 12 | const G = { 13 | WIDTH: 100, 14 | HEIGHT: 150, 15 | 16 | STAR_SPEED_MIN: 0.5, 17 | STAR_SPEED_MAX: 1.0 18 | }; 19 | 20 | // Game runtime options 21 | // Refer to the official documentation for all available options 22 | options = { 23 | viewSize: {x: G.WIDTH, y: G.HEIGHT}, 24 | isCapturing: true, 25 | isCapturingGameCanvasOnly: true, 26 | captureCanvasScale: 2 27 | }; 28 | 29 | // JSDoc comments for typing 30 | /** 31 | * @typedef {{ 32 | * pos: Vector, 33 | * speed: number 34 | * }} Star 35 | */ 36 | 37 | /** 38 | * @type { Star [] } 39 | */ 40 | let stars; 41 | 42 | // The game loop function 43 | function update() { 44 | // The init function running at startup 45 | if (!ticks) { 46 | // A CrispGameLib function 47 | // First argument (number): number of times to run the second argument 48 | // Second argument (function): a function that returns an object. This 49 | // object is then added to an array. This array will eventually be 50 | // returned as output of the times() function. 51 | stars = times(20, () => { 52 | // Random number generator function 53 | // rnd( min, max ) 54 | const posX = rnd(0, G.WIDTH); 55 | const posY = rnd(0, G.HEIGHT); 56 | // An object of type Star with appropriate properties 57 | return { 58 | // Creates a Vector 59 | pos: vec(posX, posY), 60 | // More RNG 61 | speed: rnd(G.STAR_SPEED_MIN, G.STAR_SPEED_MAX) 62 | }; 63 | }); 64 | } 65 | 66 | // Update for Star 67 | stars.forEach((s) => { 68 | // Move the star downwards 69 | s.pos.y += s.speed; 70 | // Bring the star back to top once it's past the bottom of the screen 71 | if (s.pos.y > G.HEIGHT) s.pos.y = 0; 72 | 73 | // Choose a color to draw 74 | color("light_black"); 75 | // Draw the star as a square of size 1 76 | box(s.pos, 1); 77 | }); 78 | } -------------------------------------------------------------------------------- /docs/step_02/main.js: -------------------------------------------------------------------------------- 1 | // The title of the game to be displayed on the title screen 2 | title = "CHARGE RUSH"; 3 | 4 | // The description, which is also displayed on the title screen 5 | description = ` 6 | `; 7 | 8 | // The array of custom sprites 9 | characters = [ 10 | ` 11 | ll 12 | ll 13 | ccllcc 14 | ccllcc 15 | ccllcc 16 | cc cc 17 | ` 18 | ]; 19 | 20 | // Game design variable container 21 | const G = { 22 | WIDTH: 100, 23 | HEIGHT: 150, 24 | 25 | STAR_SPEED_MIN: 0.5, 26 | STAR_SPEED_MAX: 1.0 27 | }; 28 | 29 | // Game runtime options 30 | // Refer to the official documentation for all available options 31 | options = { 32 | viewSize: {x: G.WIDTH, y: G.HEIGHT}, 33 | isCapturing: true, 34 | isCapturingGameCanvasOnly: true, 35 | captureCanvasScale: 2 36 | }; 37 | 38 | // JSDoc comments for typing 39 | /** 40 | * @typedef {{ 41 | * pos: Vector, 42 | * speed: number 43 | * }} Star 44 | */ 45 | 46 | /** 47 | * @type { Star [] } 48 | */ 49 | let stars; 50 | 51 | /** 52 | * @typedef {{ 53 | * pos: Vector, 54 | * }} Player 55 | */ 56 | 57 | /** 58 | * @type { Player } 59 | */ 60 | let player; 61 | 62 | // The game loop function 63 | function update() { 64 | // The init function running at startup 65 | if (!ticks) { 66 | // A CrispGameLib function 67 | // First argument (number): number of times to run the second argument 68 | // Second argument (function): a function that returns an object. This 69 | // object is then added to an array. This array will eventually be 70 | // returned as output of the times() function. 71 | stars = times(20, () => { 72 | // Random number generator function 73 | // rnd( min, max ) 74 | const posX = rnd(0, G.WIDTH); 75 | const posY = rnd(0, G.HEIGHT); 76 | // An object of type Star with appropriate properties 77 | return { 78 | // Creates a Vector 79 | pos: vec(posX, posY), 80 | // More RNG 81 | speed: rnd(G.STAR_SPEED_MIN, G.STAR_SPEED_MAX) 82 | }; 83 | }); 84 | 85 | player = { 86 | pos: vec(G.WIDTH * 0.5, G.HEIGHT * 0.5) 87 | }; 88 | } 89 | 90 | // Update for Star 91 | stars.forEach((s) => { 92 | // Move the star downwards 93 | s.pos.y += s.speed; 94 | // Bring the star back to top once it's past the bottom of the screen 95 | if (s.pos.y > G.HEIGHT) s.pos.y = 0; 96 | 97 | // Choose a color to draw 98 | color("light_black"); 99 | // Draw the star as a square of size 1 100 | box(s.pos, 1); 101 | }); 102 | 103 | player.pos = vec(input.pos.x, input.pos.y); 104 | player.pos.clamp(0, G.WIDTH, 0, G.HEIGHT); 105 | 106 | // color("cyan"); 107 | color ("black"); 108 | // box(player.pos, 4); 109 | char("a", player.pos); 110 | } -------------------------------------------------------------------------------- /docs/characters.js: -------------------------------------------------------------------------------- 1 | const _ = [ 2 | // human 3 | ` 4 | llllll 5 | ll l l 6 | ll l l 7 | llllll 8 | l l 9 | l l 10 | `, 11 | ` 12 | llllll 13 | ll l l 14 | ll l l 15 | llllll 16 | ll ll 17 | `, 18 | ` 19 | lll 20 | ll l l 21 | llll 22 | l l 23 | ll ll 24 | `, 25 | ` 26 | lll 27 | ll l l 28 | llll 29 | ll 30 | l l 31 | l l 32 | `, 33 | ` 34 | lllll 35 | llllll 36 | ll l l 37 | lllll 38 | l l 39 | l l 40 | `, 41 | ` 42 | lllll 43 | ll l l 44 | llllll 45 | lllll 46 | l l 47 | l l 48 | `, 49 | ` 50 | ll 51 | ll 52 | llllll 53 | ll 54 | l l 55 | l l 56 | `, 57 | // arrow 58 | ` 59 | l 60 | l 61 | lllll 62 | l 63 | l 64 | `, 65 | ` 66 | l 67 | l 68 | l l 69 | ll 70 | lll 71 | `, 72 | ` 73 | ll l 74 | llllll 75 | ll l 76 | `, 77 | ` 78 | ll ll 79 | lllll 80 | ll ll 81 | `, 82 | // coin 83 | ` 84 | l 85 | l 86 | l 87 | `, 88 | ` 89 | l 90 | lll 91 | l 92 | `, 93 | // heart 94 | ` 95 | l l 96 | lllll 97 | lllll 98 | lll 99 | l 100 | `, 101 | // face 102 | ` 103 | llll 104 | l l 105 | ll ll 106 | l l 107 | l ll l 108 | llll 109 | `, 110 | // circle 111 | ` 112 | ll 113 | llll 114 | llll 115 | ll 116 | `, 117 | ` 118 | ll 119 | l l 120 | l l 121 | ll 122 | `, 123 | ` 124 | lll 125 | lll 126 | ll 127 | 128 | 129 | 130 | `, 131 | ` 132 | llllll 133 | llllll 134 | lllll 135 | lllll 136 | llll 137 | ll 138 | `, 139 | ` 140 | l 141 | l 142 | ll 143 | 144 | 145 | 146 | `, 147 | ` 148 | l 149 | l 150 | l 151 | l 152 | ll 153 | ll 154 | `, 155 | // square 156 | ` 157 | llll 158 | llll 159 | llll 160 | llll 161 | `, 162 | ` 163 | llll 164 | l l 165 | l l 166 | llll 167 | `, 168 | ` 169 | l l l 170 | l l l 171 | l l l 172 | l l l 173 | l l l 174 | l l l 175 | `, 176 | // triangle 177 | ` 178 | l 179 | lll 180 | lll 181 | lllll 182 | lllll 183 | `, 184 | ` 185 | l 186 | l l 187 | l l 188 | l l 189 | lllll 190 | `, 191 | ` 192 | lll 193 | ll 194 | l 195 | 196 | 197 | 198 | `, 199 | ` 200 | llllll 201 | lllll 202 | llll 203 | lll 204 | ll 205 | l 206 | `, 207 | ` 208 | l l l 209 | l l 210 | l l 211 | l 212 | l 213 | `, 214 | ` 215 | l l l 216 | l l l 217 | l l 218 | l l 219 | l 220 | l 221 | `, 222 | ` 223 | l 224 | l 225 | l 226 | 227 | 228 | 229 | `, 230 | ` 231 | l 232 | l 233 | l 234 | l 235 | l 236 | l 237 | `, 238 | // X 239 | ` 240 | l l 241 | l l 242 | ll 243 | ll 244 | l l 245 | l l 246 | `, 247 | // line 248 | ` 249 | l 250 | l 251 | l 252 | l 253 | l 254 | l 255 | `, 256 | ` 257 | l 258 | l 259 | lll 260 | 261 | 262 | 263 | `, 264 | ` 265 | l 266 | l 267 | llllll 268 | 269 | 270 | 271 | `, 272 | ` 273 | l 274 | l 275 | lllllll 276 | l 277 | l 278 | l 279 | ` // dots 280 | ` 281 | lll 282 | lll 283 | lll 284 | 285 | 286 | 287 | `, 288 | ` 289 | llllll 290 | llllll 291 | llllll 292 | 293 | 294 | 295 | `, 296 | ` 297 | lll 298 | lll 299 | lll 300 | lll 301 | lll 302 | lll 303 | ` 304 | ]; 305 | -------------------------------------------------------------------------------- /docs/step_03/main.js: -------------------------------------------------------------------------------- 1 | // The title of the game to be displayed on the title screen 2 | title = "CHARGE RUSH"; 3 | 4 | // The description, which is also displayed on the title screen 5 | description = ` 6 | `; 7 | 8 | // The array of custom sprites 9 | characters = [ 10 | ` 11 | ll 12 | ll 13 | ccllcc 14 | ccllcc 15 | ccllcc 16 | cc cc 17 | ` 18 | ]; 19 | 20 | // Game design variable container 21 | const G = { 22 | WIDTH: 100, 23 | HEIGHT: 150, 24 | 25 | STAR_SPEED_MIN: 0.5, 26 | STAR_SPEED_MAX: 1.0, 27 | 28 | PLAYER_FIRE_RATE: 4, 29 | PLAYER_GUN_OFFSET: 3, 30 | 31 | FBULLET_SPEED: 5 32 | }; 33 | 34 | // Game runtime options 35 | // Refer to the official documentation for all available options 36 | options = { 37 | viewSize: {x: G.WIDTH, y: G.HEIGHT}, 38 | isCapturing: true, 39 | isCapturingGameCanvasOnly: true, 40 | captureCanvasScale: 2 41 | }; 42 | 43 | // JSDoc comments for typing 44 | /** 45 | * @typedef {{ 46 | * pos: Vector, 47 | * speed: number 48 | * }} Star 49 | */ 50 | 51 | /** 52 | * @type { Star [] } 53 | */ 54 | let stars; 55 | 56 | /** 57 | * @typedef {{ 58 | * pos: Vector, 59 | * firingCooldown: number, 60 | * isFiringLeft: boolean 61 | * }} Player 62 | */ 63 | 64 | /** 65 | * @type { Player } 66 | */ 67 | let player; 68 | 69 | /** 70 | * @typedef {{ 71 | * pos: Vector 72 | * }} FBullet 73 | */ 74 | 75 | /** 76 | * @type { FBullet [] } 77 | */ 78 | let fBullets; 79 | 80 | // The game loop function 81 | function update() { 82 | // The init function running at startup 83 | if (!ticks) { 84 | // A CrispGameLib function 85 | // First argument (number): number of times to run the second argument 86 | // Second argument (function): a function that returns an object. This 87 | // object is then added to an array. This array will eventually be 88 | // returned as output of the times() function. 89 | stars = times(20, () => { 90 | // Random number generator function 91 | // rnd( min, max ) 92 | const posX = rnd(0, G.WIDTH); 93 | const posY = rnd(0, G.HEIGHT); 94 | // An object of type Star with appropriate properties 95 | return { 96 | // Creates a Vector 97 | pos: vec(posX, posY), 98 | // More RNG 99 | speed: rnd(G.STAR_SPEED_MIN, G.STAR_SPEED_MAX) 100 | }; 101 | }); 102 | 103 | player = { 104 | pos: vec(G.WIDTH * 0.5, G.HEIGHT * 0.5), 105 | firingCooldown: G.PLAYER_FIRE_RATE, 106 | isFiringLeft: true 107 | }; 108 | 109 | fBullets = []; 110 | } 111 | 112 | // Update for Star 113 | stars.forEach((s) => { 114 | // Move the star downwards 115 | s.pos.y += s.speed; 116 | // Bring the star back to top once it's past the bottom of the screen 117 | if (s.pos.y > G.HEIGHT) s.pos.y = 0; 118 | 119 | // Choose a color to draw 120 | color("light_black"); 121 | // Draw the star as a square of size 1 122 | box(s.pos, 1); 123 | }); 124 | 125 | // Updating and drawing the player 126 | player.pos = vec(input.pos.x, input.pos.y); 127 | player.pos.clamp(0, G.WIDTH, 0, G.HEIGHT); 128 | // Cooling down for the next shot 129 | player.firingCooldown--; 130 | // Time to fire the next shot 131 | if (player.firingCooldown <= 0) { 132 | // Get the side from which the bullet is fired 133 | const offset = (player.isFiringLeft) 134 | ? -G.PLAYER_GUN_OFFSET 135 | : G.PLAYER_GUN_OFFSET; 136 | // Create the bullet 137 | fBullets.push({ 138 | pos: vec(player.pos.x + offset, player.pos.y) 139 | }); 140 | // Reset the firing cooldown 141 | player.firingCooldown = G.PLAYER_FIRE_RATE; 142 | // Switch the side of the firing gun by flipping the boolean value 143 | player.isFiringLeft = !player.isFiringLeft; 144 | 145 | color("yellow"); 146 | // Generate particles 147 | particle( 148 | player.pos.x + offset, // x coordinate 149 | player.pos.y, // y coordinate 150 | 4, // The number of particles 151 | 1, // The speed of the particles 152 | -PI/2, // The emitting angle 153 | PI/4 // The emitting width 154 | ); 155 | } 156 | color ("black"); 157 | char("a", player.pos); 158 | 159 | // text(fBullets.length.toString(), 3, 10); 160 | 161 | // Updating and drawing bullets 162 | fBullets.forEach((fb) => { 163 | // Move the bullets upwards 164 | fb.pos.y -= G.FBULLET_SPEED; 165 | 166 | // Drawing 167 | color("yellow"); 168 | box(fb.pos, 2); 169 | }); 170 | 171 | remove(fBullets, (fb) => { 172 | return fb.pos.y < 0; 173 | }); 174 | } 175 | 176 | -------------------------------------------------------------------------------- /docs/step_04/main.js: -------------------------------------------------------------------------------- 1 | // The title of the game to be displayed on the title screen 2 | title = "CHARGE RUSH"; 3 | 4 | // The description, which is also displayed on the title screen 5 | description = ` 6 | `; 7 | 8 | // The array of custom sprites 9 | characters = [ 10 | ` 11 | ll 12 | ll 13 | ccllcc 14 | ccllcc 15 | ccllcc 16 | cc cc 17 | `,` 18 | rr rr 19 | rrrrrr 20 | rrpprr 21 | rrrrrr 22 | rr 23 | rr 24 | `, 25 | ]; 26 | 27 | // Game design variable container 28 | const G = { 29 | WIDTH: 100, 30 | HEIGHT: 150, 31 | 32 | STAR_SPEED_MIN: 0.5, 33 | STAR_SPEED_MAX: 1.0, 34 | 35 | PLAYER_FIRE_RATE: 4, 36 | PLAYER_GUN_OFFSET: 3, 37 | 38 | FBULLET_SPEED: 5, 39 | 40 | ENEMY_MIN_BASE_SPEED: 1.0, 41 | ENEMY_MAX_BASE_SPEED: 2.0 42 | }; 43 | 44 | // Game runtime options 45 | // Refer to the official documentation for all available options 46 | options = { 47 | viewSize: {x: G.WIDTH, y: G.HEIGHT}, 48 | isCapturing: true, 49 | isCapturingGameCanvasOnly: true, 50 | captureCanvasScale: 2 51 | }; 52 | 53 | // JSDoc comments for typing 54 | /** 55 | * @typedef {{ 56 | * pos: Vector, 57 | * speed: number 58 | * }} Star 59 | */ 60 | 61 | /** 62 | * @type { Star [] } 63 | */ 64 | let stars; 65 | 66 | /** 67 | * @typedef {{ 68 | * pos: Vector, 69 | * firingCooldown: number, 70 | * isFiringLeft: boolean 71 | * }} Player 72 | */ 73 | 74 | /** 75 | * @type { Player } 76 | */ 77 | let player; 78 | 79 | /** 80 | * @typedef {{ 81 | * pos: Vector 82 | * }} FBullet 83 | */ 84 | 85 | /** 86 | * @type { FBullet [] } 87 | */ 88 | let fBullets; 89 | 90 | /** 91 | * @typedef {{ 92 | * pos: Vector 93 | * }} Enemy 94 | */ 95 | 96 | /** 97 | * @type { Enemy [] } 98 | */ 99 | let enemies; 100 | 101 | /** 102 | * @type { number } 103 | */ 104 | let currentEnemySpeed; 105 | 106 | /** 107 | * @type { number } 108 | */ 109 | let waveCount; 110 | 111 | // The game loop function 112 | function update() { 113 | // The init function running at startup 114 | if (!ticks) { 115 | // A CrispGameLib function 116 | // First argument (number): number of times to run the second argument 117 | // Second argument (function): a function that returns an object. This 118 | // object is then added to an array. This array will eventually be 119 | // returned as output of the times() function. 120 | stars = times(20, () => { 121 | // Random number generator function 122 | // rnd( min, max ) 123 | const posX = rnd(0, G.WIDTH); 124 | const posY = rnd(0, G.HEIGHT); 125 | // An object of type Star with appropriate properties 126 | return { 127 | // Creates a Vector 128 | pos: vec(posX, posY), 129 | // More RNG 130 | speed: rnd(G.STAR_SPEED_MIN, G.STAR_SPEED_MAX) 131 | }; 132 | }); 133 | 134 | player = { 135 | pos: vec(G.WIDTH * 0.5, G.HEIGHT * 0.5), 136 | firingCooldown: G.PLAYER_FIRE_RATE, 137 | isFiringLeft: true 138 | }; 139 | 140 | fBullets = []; 141 | enemies = []; 142 | } 143 | 144 | // Spawning enemies 145 | if (enemies.length === 0) { 146 | currentEnemySpeed = 147 | rnd(G.ENEMY_MIN_BASE_SPEED, G.ENEMY_MAX_BASE_SPEED) * difficulty; 148 | for (let i = 0; i < 9; i++) { 149 | const posX = rnd(0, G.WIDTH); 150 | const posY = -rnd(i * G.HEIGHT * 0.1); 151 | enemies.push({ pos: vec(posX, posY) }) 152 | } 153 | } 154 | 155 | // Update for Star 156 | stars.forEach((s) => { 157 | // Move the star downwards 158 | s.pos.y += s.speed; 159 | // Bring the star back to top once it's past the bottom of the screen 160 | if (s.pos.y > G.HEIGHT) s.pos.y = 0; 161 | 162 | // Choose a color to draw 163 | color("light_black"); 164 | // Draw the star as a square of size 1 165 | box(s.pos, 1); 166 | }); 167 | 168 | // Updating and drawing the player 169 | player.pos = vec(input.pos.x, input.pos.y); 170 | player.pos.clamp(0, G.WIDTH, 0, G.HEIGHT); 171 | // Cooling down for the next shot 172 | player.firingCooldown--; 173 | // Time to fire the next shot 174 | if (player.firingCooldown <= 0) { 175 | // Get the side from which the bullet is fired 176 | const offset = (player.isFiringLeft) 177 | ? -G.PLAYER_GUN_OFFSET 178 | : G.PLAYER_GUN_OFFSET; 179 | // Create the bullet 180 | fBullets.push({ 181 | pos: vec(player.pos.x + offset, player.pos.y) 182 | }); 183 | // Reset the firing cooldown 184 | player.firingCooldown = G.PLAYER_FIRE_RATE; 185 | // Switch the side of the firing gun by flipping the boolean value 186 | player.isFiringLeft = !player.isFiringLeft; 187 | 188 | color("yellow"); 189 | // Generate particles 190 | particle( 191 | player.pos.x + offset, // x coordinate 192 | player.pos.y, // y coordinate 193 | 4, // The number of particles 194 | 1, // The speed of the particles 195 | -PI/2, // The emitting angle 196 | PI/4 // The emitting width 197 | ); 198 | } 199 | color ("black"); 200 | char("a", player.pos); 201 | 202 | // text(fBullets.length.toString(), 3, 10); 203 | 204 | // Updating and drawing bullets 205 | fBullets.forEach((fb) => { 206 | // Move the bullets upwards 207 | fb.pos.y -= G.FBULLET_SPEED; 208 | 209 | // Drawing 210 | color("yellow"); 211 | box(fb.pos, 2); 212 | }); 213 | 214 | remove(fBullets, (fb) => { 215 | return fb.pos.y < 0; 216 | }); 217 | 218 | remove(enemies, (e) => { 219 | e.pos.y += currentEnemySpeed; 220 | color("black"); 221 | char("b", e.pos); 222 | 223 | return (e.pos.y > G.HEIGHT); 224 | }); 225 | } 226 | 227 | -------------------------------------------------------------------------------- /docs/step_05/main.js: -------------------------------------------------------------------------------- 1 | // The title of the game to be displayed on the title screen 2 | title = "CHARGE RUSH"; 3 | 4 | // The description, which is also displayed on the title screen 5 | description = ` 6 | `; 7 | 8 | // The array of custom sprites 9 | characters = [ 10 | ` 11 | ll 12 | ll 13 | ccllcc 14 | ccllcc 15 | ccllcc 16 | cc cc 17 | `,` 18 | rr rr 19 | rrrrrr 20 | rrpprr 21 | rrrrrr 22 | rr 23 | rr 24 | `, 25 | ]; 26 | 27 | // Game design variable container 28 | const G = { 29 | WIDTH: 100, 30 | HEIGHT: 150, 31 | 32 | STAR_SPEED_MIN: 0.5, 33 | STAR_SPEED_MAX: 1.0, 34 | 35 | PLAYER_FIRE_RATE: 4, 36 | PLAYER_GUN_OFFSET: 3, 37 | 38 | FBULLET_SPEED: 5, 39 | 40 | ENEMY_MIN_BASE_SPEED: 1.0, 41 | ENEMY_MAX_BASE_SPEED: 2.0 42 | }; 43 | 44 | // Game runtime options 45 | // Refer to the official documentation for all available options 46 | options = { 47 | viewSize: {x: G.WIDTH, y: G.HEIGHT}, 48 | isCapturing: true, 49 | isCapturingGameCanvasOnly: true, 50 | captureCanvasScale: 2 51 | }; 52 | 53 | // JSDoc comments for typing 54 | /** 55 | * @typedef {{ 56 | * pos: Vector, 57 | * speed: number 58 | * }} Star 59 | */ 60 | 61 | /** 62 | * @type { Star [] } 63 | */ 64 | let stars; 65 | 66 | /** 67 | * @typedef {{ 68 | * pos: Vector, 69 | * firingCooldown: number, 70 | * isFiringLeft: boolean 71 | * }} Player 72 | */ 73 | 74 | /** 75 | * @type { Player } 76 | */ 77 | let player; 78 | 79 | /** 80 | * @typedef {{ 81 | * pos: Vector 82 | * }} FBullet 83 | */ 84 | 85 | /** 86 | * @type { FBullet [] } 87 | */ 88 | let fBullets; 89 | 90 | /** 91 | * @typedef {{ 92 | * pos: Vector 93 | * }} Enemy 94 | */ 95 | 96 | /** 97 | * @type { Enemy [] } 98 | */ 99 | let enemies; 100 | 101 | /** 102 | * @type { number } 103 | */ 104 | let currentEnemySpeed; 105 | 106 | /** 107 | * @type { number } 108 | */ 109 | let waveCount; 110 | 111 | // The game loop function 112 | function update() { 113 | // The init function running at startup 114 | if (!ticks) { 115 | // A CrispGameLib function 116 | // First argument (number): number of times to run the second argument 117 | // Second argument (function): a function that returns an object. This 118 | // object is then added to an array. This array will eventually be 119 | // returned as output of the times() function. 120 | stars = times(20, () => { 121 | // Random number generator function 122 | // rnd( min, max ) 123 | const posX = rnd(0, G.WIDTH); 124 | const posY = rnd(0, G.HEIGHT); 125 | // An object of type Star with appropriate properties 126 | return { 127 | // Creates a Vector 128 | pos: vec(posX, posY), 129 | // More RNG 130 | speed: rnd(G.STAR_SPEED_MIN, G.STAR_SPEED_MAX) 131 | }; 132 | }); 133 | 134 | player = { 135 | pos: vec(G.WIDTH * 0.5, G.HEIGHT * 0.5), 136 | firingCooldown: G.PLAYER_FIRE_RATE, 137 | isFiringLeft: true 138 | }; 139 | 140 | fBullets = []; 141 | enemies = []; 142 | } 143 | 144 | // Spawning enemies 145 | if (enemies.length === 0) { 146 | currentEnemySpeed = 147 | rnd(G.ENEMY_MIN_BASE_SPEED, G.ENEMY_MAX_BASE_SPEED) * difficulty; 148 | for (let i = 0; i < 9; i++) { 149 | const posX = rnd(0, G.WIDTH); 150 | const posY = -rnd(i * G.HEIGHT * 0.1); 151 | enemies.push({ pos: vec(posX, posY) }) 152 | } 153 | } 154 | 155 | // Update for Star 156 | stars.forEach((s) => { 157 | // Move the star downwards 158 | s.pos.y += s.speed; 159 | // Bring the star back to top once it's past the bottom of the screen 160 | if (s.pos.y > G.HEIGHT) s.pos.y = 0; 161 | 162 | // Choose a color to draw 163 | color("light_black"); 164 | // Draw the star as a square of size 1 165 | box(s.pos, 1); 166 | }); 167 | 168 | // Updating and drawing the player 169 | player.pos = vec(input.pos.x, input.pos.y); 170 | player.pos.clamp(0, G.WIDTH, 0, G.HEIGHT); 171 | // Cooling down for the next shot 172 | player.firingCooldown--; 173 | // Time to fire the next shot 174 | if (player.firingCooldown <= 0) { 175 | // Get the side from which the bullet is fired 176 | const offset = (player.isFiringLeft) 177 | ? -G.PLAYER_GUN_OFFSET 178 | : G.PLAYER_GUN_OFFSET; 179 | // Create the bullet 180 | fBullets.push({ 181 | pos: vec(player.pos.x + offset, player.pos.y) 182 | }); 183 | // Reset the firing cooldown 184 | player.firingCooldown = G.PLAYER_FIRE_RATE; 185 | // Switch the side of the firing gun by flipping the boolean value 186 | player.isFiringLeft = !player.isFiringLeft; 187 | 188 | color("yellow"); 189 | // Generate particles 190 | particle( 191 | player.pos.x + offset, // x coordinate 192 | player.pos.y, // y coordinate 193 | 4, // The number of particles 194 | 1, // The speed of the particles 195 | -PI/2, // The emitting angle 196 | PI/4 // The emitting width 197 | ); 198 | } 199 | color ("black"); 200 | char("a", player.pos); 201 | 202 | // text(fBullets.length.toString(), 3, 10); 203 | 204 | // Updating and drawing bullets 205 | fBullets.forEach((fb) => { 206 | fb.pos.y -= G.FBULLET_SPEED; 207 | 208 | // Drawing fBullets for the first time, allowing interaction from enemies 209 | color("yellow"); 210 | box(fb.pos, 2); 211 | }); 212 | 213 | remove(enemies, (e) => { 214 | e.pos.y += currentEnemySpeed; 215 | color("black"); 216 | // Interaction from enemies to fBullets 217 | // Shorthand to check for collision against another specific type 218 | // Also draw the sprits 219 | const isCollidingWithFBullets = char("b", e.pos).isColliding.rect.yellow; 220 | 221 | if (isCollidingWithFBullets) { 222 | color("yellow"); 223 | particle(e.pos); 224 | } 225 | 226 | // Also another condition to remove the object 227 | return (isCollidingWithFBullets || e.pos.y > G.HEIGHT); 228 | }); 229 | 230 | remove(fBullets, (fb) => { 231 | // Interaction from fBullets to enemies, after enemies have been drawn 232 | color("yellow"); 233 | const isCollidingWithEnemies = box(fb.pos, 2).isColliding.char.b; 234 | return (isCollidingWithEnemies || fb.pos.y < 0); 235 | }); 236 | } -------------------------------------------------------------------------------- /docs/step_06/main.js: -------------------------------------------------------------------------------- 1 | // The title of the game to be displayed on the title screen 2 | title = "CHARGE RUSH"; 3 | 4 | // The description, which is also displayed on the title screen 5 | description = ` 6 | Destroy enemies. 7 | `; 8 | 9 | // The array of custom sprites 10 | characters = [ 11 | ` 12 | ll 13 | ll 14 | ccllcc 15 | ccllcc 16 | ccllcc 17 | cc cc 18 | `,` 19 | rr rr 20 | rrrrrr 21 | rrpprr 22 | rrrrrr 23 | rr 24 | rr 25 | `, 26 | ]; 27 | 28 | // Game design variable container 29 | const G = { 30 | WIDTH: 100, 31 | HEIGHT: 150, 32 | 33 | STAR_SPEED_MIN: 0.5, 34 | STAR_SPEED_MAX: 1.0, 35 | 36 | PLAYER_FIRE_RATE: 4, 37 | PLAYER_GUN_OFFSET: 3, 38 | 39 | FBULLET_SPEED: 5, 40 | 41 | ENEMY_MIN_BASE_SPEED: 1.0, 42 | ENEMY_MAX_BASE_SPEED: 2.0 43 | }; 44 | 45 | // Game runtime options 46 | // Refer to the official documentation for all available options 47 | options = { 48 | viewSize: {x: G.WIDTH, y: G.HEIGHT}, 49 | isCapturing: true, 50 | isCapturingGameCanvasOnly: true, 51 | captureCanvasScale: 2, 52 | seed: 1, 53 | isPlayingBgm: true 54 | }; 55 | 56 | // JSDoc comments for typing 57 | /** 58 | * @typedef {{ 59 | * pos: Vector, 60 | * speed: number 61 | * }} Star 62 | */ 63 | 64 | /** 65 | * @type { Star [] } 66 | */ 67 | let stars; 68 | 69 | /** 70 | * @typedef {{ 71 | * pos: Vector, 72 | * firingCooldown: number, 73 | * isFiringLeft: boolean 74 | * }} Player 75 | */ 76 | 77 | /** 78 | * @type { Player } 79 | */ 80 | let player; 81 | 82 | /** 83 | * @typedef {{ 84 | * pos: Vector 85 | * }} FBullet 86 | */ 87 | 88 | /** 89 | * @type { FBullet [] } 90 | */ 91 | let fBullets; 92 | 93 | /** 94 | * @typedef {{ 95 | * pos: Vector 96 | * }} Enemy 97 | */ 98 | 99 | /** 100 | * @type { Enemy [] } 101 | */ 102 | let enemies; 103 | 104 | /** 105 | * @type { number } 106 | */ 107 | let currentEnemySpeed; 108 | 109 | /** 110 | * @type { number } 111 | */ 112 | let waveCount; 113 | 114 | // The game loop function 115 | function update() { 116 | // The init function running at startup 117 | if (!ticks) { 118 | // A CrispGameLib function 119 | // First argument (number): number of times to run the second argument 120 | // Second argument (function): a function that returns an object. This 121 | // object is then added to an array. This array will eventually be 122 | // returned as output of the times() function. 123 | stars = times(20, () => { 124 | // Random number generator function 125 | // rnd( min, max ) 126 | const posX = rnd(0, G.WIDTH); 127 | const posY = rnd(0, G.HEIGHT); 128 | // An object of type Star with appropriate properties 129 | return { 130 | // Creates a Vector 131 | pos: vec(posX, posY), 132 | // More RNG 133 | speed: rnd(G.STAR_SPEED_MIN, G.STAR_SPEED_MAX) 134 | }; 135 | }); 136 | 137 | player = { 138 | pos: vec(G.WIDTH * 0.5, G.HEIGHT * 0.5), 139 | firingCooldown: G.PLAYER_FIRE_RATE, 140 | isFiringLeft: true 141 | }; 142 | 143 | fBullets = []; 144 | enemies = []; 145 | } 146 | 147 | // Spawning enemies 148 | if (enemies.length === 0) { 149 | currentEnemySpeed = 150 | rnd(G.ENEMY_MIN_BASE_SPEED, G.ENEMY_MAX_BASE_SPEED) * difficulty; 151 | for (let i = 0; i < 9; i++) { 152 | const posX = rnd(0, G.WIDTH); 153 | const posY = -rnd(i * G.HEIGHT * 0.1); 154 | enemies.push({ pos: vec(posX, posY) }) 155 | } 156 | } 157 | 158 | // Update for Star 159 | stars.forEach((s) => { 160 | // Move the star downwards 161 | s.pos.y += s.speed; 162 | // Bring the star back to top once it's past the bottom of the screen 163 | if (s.pos.y > G.HEIGHT) s.pos.y = 0; 164 | 165 | // Choose a color to draw 166 | color("light_black"); 167 | // Draw the star as a square of size 1 168 | box(s.pos, 1); 169 | }); 170 | 171 | // Updating and drawing the player 172 | player.pos = vec(input.pos.x, input.pos.y); 173 | player.pos.clamp(0, G.WIDTH, 0, G.HEIGHT); 174 | // Cooling down for the next shot 175 | player.firingCooldown--; 176 | // Time to fire the next shot 177 | if (player.firingCooldown <= 0) { 178 | // Get the side from which the bullet is fired 179 | const offset = (player.isFiringLeft) 180 | ? -G.PLAYER_GUN_OFFSET 181 | : G.PLAYER_GUN_OFFSET; 182 | // Create the bullet 183 | fBullets.push({ 184 | pos: vec(player.pos.x + offset, player.pos.y) 185 | }); 186 | // Reset the firing cooldown 187 | player.firingCooldown = G.PLAYER_FIRE_RATE; 188 | // Switch the side of the firing gun by flipping the boolean value 189 | player.isFiringLeft = !player.isFiringLeft; 190 | 191 | color("yellow"); 192 | // Generate particles 193 | particle( 194 | player.pos.x + offset, // x coordinate 195 | player.pos.y, // y coordinate 196 | 4, // The number of particles 197 | 1, // The speed of the particles 198 | -PI/2, // The emitting angle 199 | PI/4 // The emitting width 200 | ); 201 | } 202 | color ("black"); 203 | char("a", player.pos); 204 | 205 | // text(fBullets.length.toString(), 3, 10); 206 | 207 | // Updating and drawing bullets 208 | fBullets.forEach((fb) => { 209 | fb.pos.y -= G.FBULLET_SPEED; 210 | 211 | // Drawing fBullets for the first time, allowing interaction from enemies 212 | color("yellow"); 213 | box(fb.pos, 2); 214 | }); 215 | 216 | remove(enemies, (e) => { 217 | e.pos.y += currentEnemySpeed; 218 | color("black"); 219 | // Interaction from enemies to fBullets 220 | // Shorthand to check for collision against another specific type 221 | // Also draw the sprits 222 | const isCollidingWithFBullets = char("b", e.pos).isColliding.rect.yellow; 223 | 224 | if (isCollidingWithFBullets) { 225 | color("yellow"); 226 | particle(e.pos); 227 | play("explosion"); 228 | } 229 | 230 | // Also another condition to remove the object 231 | return (isCollidingWithFBullets || e.pos.y > G.HEIGHT); 232 | }); 233 | 234 | remove(fBullets, (fb) => { 235 | // Interaction from fBullets to enemies, after enemies have been drawn 236 | color("yellow"); 237 | const isCollidingWithEnemies = box(fb.pos, 2).isColliding.char.b; 238 | return (isCollidingWithEnemies || fb.pos.y < 0); 239 | }); 240 | } -------------------------------------------------------------------------------- /docs/chargerush/main.js: -------------------------------------------------------------------------------- 1 | // The title of the game to be displayed on the title screen 2 | title = "CHARGE RUSH"; 3 | 4 | // The description, which is also displayed on the title screen 5 | description = ` 6 | Destroy enemies. 7 | `; 8 | 9 | // The array of custom sprites 10 | characters = [ 11 | ` 12 | ll 13 | ll 14 | ccllcc 15 | ccllcc 16 | ccllcc 17 | cc cc 18 | `,` 19 | rr rr 20 | rrrrrr 21 | rrpprr 22 | rrrrrr 23 | rr 24 | rr 25 | `,` 26 | y y 27 | yyyyyy 28 | y y 29 | yyyyyy 30 | y y 31 | ` 32 | ]; 33 | 34 | // Game design variable container 35 | const G = { 36 | WIDTH: 100, 37 | HEIGHT: 150, 38 | 39 | STAR_SPEED_MIN: 0.5, 40 | STAR_SPEED_MAX: 1.0, 41 | 42 | PLAYER_FIRE_RATE: 4, 43 | PLAYER_GUN_OFFSET: 3, 44 | 45 | FBULLET_SPEED: 5, 46 | 47 | ENEMY_MIN_BASE_SPEED: 1.0, 48 | ENEMY_MAX_BASE_SPEED: 2.0, 49 | ENEMY_FIRE_RATE: 45, 50 | 51 | EBULLET_SPEED: 2.0, 52 | EBULLET_ROTATION_SPD: 0.1 53 | }; 54 | 55 | // Game runtime options 56 | // Refer to the official documentation for all available options 57 | options = { 58 | viewSize: {x: G.WIDTH, y: G.HEIGHT}, 59 | isCapturing: true, 60 | isCapturingGameCanvasOnly: true, 61 | captureCanvasScale: 2, 62 | seed: 1, 63 | isPlayingBgm: true, 64 | isReplayEnabled: true, 65 | theme: "dark" 66 | }; 67 | 68 | // JSDoc comments for typing 69 | /** 70 | * @typedef {{ 71 | * pos: Vector, 72 | * speed: number 73 | * }} Star 74 | */ 75 | 76 | /** 77 | * @type { Star [] } 78 | */ 79 | let stars; 80 | 81 | /** 82 | * @typedef {{ 83 | * pos: Vector, 84 | * firingCooldown: number, 85 | * isFiringLeft: boolean 86 | * }} Player 87 | */ 88 | 89 | /** 90 | * @type { Player } 91 | */ 92 | let player; 93 | 94 | /** 95 | * @typedef {{ 96 | * pos: Vector 97 | * }} FBullet 98 | */ 99 | 100 | /** 101 | * @type { FBullet [] } 102 | */ 103 | let fBullets; 104 | 105 | /** 106 | * @typedef {{ 107 | * pos: Vector, 108 | * firingCooldown: number 109 | * }} Enemy 110 | */ 111 | 112 | /** 113 | * @type { Enemy [] } 114 | */ 115 | let enemies; 116 | 117 | /** 118 | * @typedef {{ 119 | * pos: Vector, 120 | * angle: number, 121 | * rotation: number 122 | * }} EBullet 123 | */ 124 | 125 | /** 126 | * @type { EBullet [] } 127 | */ 128 | let eBullets; 129 | 130 | /** 131 | * @type { number } 132 | */ 133 | let currentEnemySpeed; 134 | 135 | /** 136 | * @type { number } 137 | */ 138 | let waveCount; 139 | 140 | /** 141 | * 142 | */ 143 | 144 | // The game loop function 145 | function update() { 146 | // The init function running at startup 147 | if (!ticks) { 148 | stars = times(20, () => { 149 | const posX = rnd(0, G.WIDTH); 150 | const posY = rnd(0, G.HEIGHT); 151 | return { 152 | pos: vec(posX, posY), 153 | speed: rnd(G.STAR_SPEED_MIN, G.STAR_SPEED_MAX) 154 | }; 155 | }); 156 | 157 | player = { 158 | pos: vec(G.WIDTH * 0.5, G.HEIGHT * 0.5), 159 | firingCooldown: G.PLAYER_FIRE_RATE, 160 | isFiringLeft: true 161 | }; 162 | 163 | fBullets = []; 164 | enemies = []; 165 | eBullets = []; 166 | 167 | waveCount = 0; 168 | } 169 | 170 | // Spawning enemies 171 | if (enemies.length === 0) { 172 | currentEnemySpeed = 173 | rnd(G.ENEMY_MIN_BASE_SPEED, G.ENEMY_MAX_BASE_SPEED) * difficulty; 174 | for (let i = 0; i < 9; i++) { 175 | const posX = rnd(0, G.WIDTH); 176 | const posY = -rnd(i * G.HEIGHT * 0.1); 177 | enemies.push({ 178 | pos: vec(posX, posY), 179 | firingCooldown: G.ENEMY_FIRE_RATE 180 | }); 181 | } 182 | 183 | waveCount++; // Increase the tracking variable by one 184 | } 185 | 186 | // Update for Star 187 | stars.forEach((s) => { 188 | s.pos.y += s.speed; 189 | if (s.pos.y > G.HEIGHT) s.pos.y = 0; 190 | color("light_black"); 191 | box(s.pos, 1); 192 | }); 193 | 194 | // Updating and drawing the player 195 | player.pos = vec(input.pos.x, input.pos.y); 196 | player.pos.clamp(0, G.WIDTH, 0, G.HEIGHT); 197 | player.firingCooldown--; 198 | if (player.firingCooldown <= 0) { 199 | const offset = (player.isFiringLeft) 200 | ? -G.PLAYER_GUN_OFFSET 201 | : G.PLAYER_GUN_OFFSET; 202 | fBullets.push({ 203 | pos: vec(player.pos.x + offset, player.pos.y) 204 | }); 205 | player.firingCooldown = G.PLAYER_FIRE_RATE; 206 | player.isFiringLeft = !player.isFiringLeft; 207 | 208 | color("yellow"); 209 | particle( 210 | player.pos.x + offset, // x coordinate 211 | player.pos.y, // y coordinate 212 | 4, // The number of particles 213 | 1, // The speed of the particles 214 | -PI/2, // The emitting angle 215 | PI/4 // The emitting width 216 | ); 217 | } 218 | color ("black"); 219 | char("a", player.pos); 220 | 221 | fBullets.forEach((fb) => { 222 | fb.pos.y -= G.FBULLET_SPEED; 223 | color("yellow"); 224 | box(fb.pos, 2); 225 | }); 226 | 227 | 228 | remove(enemies, (e) => { 229 | e.pos.y += currentEnemySpeed; 230 | e.firingCooldown--; 231 | if (e.firingCooldown <= 0) { 232 | eBullets.push({ 233 | pos: vec(e.pos.x, e.pos.y), 234 | angle: e.pos.angleTo(player.pos), 235 | rotation: rnd() 236 | }); 237 | e.firingCooldown = G.ENEMY_FIRE_RATE; 238 | play("select"); 239 | } 240 | 241 | color("black"); 242 | const isCollidingWithFBullets = char("b", e.pos).isColliding.rect.yellow; 243 | const isCollidingWithPlayer = char("b", e.pos).isColliding.char.a; 244 | if (isCollidingWithPlayer) { 245 | end(); 246 | play("powerUp"); 247 | } 248 | 249 | if (isCollidingWithFBullets) { 250 | color("yellow"); 251 | particle(e.pos); 252 | play("explosion"); 253 | addScore(10 * waveCount, e.pos); 254 | } 255 | 256 | return (isCollidingWithFBullets || e.pos.y > G.HEIGHT); 257 | }); 258 | 259 | remove(fBullets, (fb) => { 260 | color("yellow"); 261 | const isCollidingWithEnemies = box(fb.pos, 2).isColliding.char.b; 262 | return (isCollidingWithEnemies || fb.pos.y < 0); 263 | }); 264 | 265 | remove(eBullets, (eb) => { 266 | eb.pos.x += G.EBULLET_SPEED * Math.cos(eb.angle); 267 | eb.pos.y += G.EBULLET_SPEED * Math.sin(eb.angle); 268 | eb.rotation += G.EBULLET_ROTATION_SPD; 269 | 270 | color("red"); 271 | const isCollidingWithPlayer 272 | = char("c", eb.pos, {rotation: eb.rotation}).isColliding.char.a; 273 | if (isCollidingWithPlayer) { 274 | end(); 275 | play("powerUp"); 276 | } 277 | const isCollidingWithFBullets 278 | = char("c", eb.pos, {rotation: eb.rotation}).isColliding.rect.yellow; 279 | if (isCollidingWithFBullets) addScore(1, eb.pos); 280 | 281 | return (!eb.pos.isInRect(0, 0, G.WIDTH, G.HEIGHT)); 282 | }); 283 | } -------------------------------------------------------------------------------- /docs/step_07/main.js: -------------------------------------------------------------------------------- 1 | // The title of the game to be displayed on the title screen 2 | title = "CHARGE RUSH"; 3 | 4 | // The description, which is also displayed on the title screen 5 | description = ` 6 | Destroy enemies. 7 | `; 8 | 9 | // The array of custom sprites 10 | characters = [ 11 | ` 12 | ll 13 | ll 14 | ccllcc 15 | ccllcc 16 | ccllcc 17 | cc cc 18 | `,` 19 | rr rr 20 | rrrrrr 21 | rrpprr 22 | rrrrrr 23 | rr 24 | rr 25 | `,` 26 | y y 27 | yyyyyy 28 | y y 29 | yyyyyy 30 | y y 31 | ` 32 | ]; 33 | 34 | // Game design variable container 35 | const G = { 36 | WIDTH: 100, 37 | HEIGHT: 150, 38 | 39 | STAR_SPEED_MIN: 0.5, 40 | STAR_SPEED_MAX: 1.0, 41 | 42 | PLAYER_FIRE_RATE: 4, 43 | PLAYER_GUN_OFFSET: 3, 44 | 45 | FBULLET_SPEED: 5, 46 | 47 | ENEMY_MIN_BASE_SPEED: 1.0, 48 | ENEMY_MAX_BASE_SPEED: 2.0, 49 | ENEMY_FIRE_RATE: 45, 50 | 51 | EBULLET_SPEED: 2.0, 52 | EBULLET_ROTATION_SPD: 0.1 53 | }; 54 | 55 | // Game runtime options 56 | // Refer to the official documentation for all available options 57 | options = { 58 | viewSize: {x: G.WIDTH, y: G.HEIGHT}, 59 | isCapturing: true, 60 | isCapturingGameCanvasOnly: true, 61 | captureCanvasScale: 2, 62 | seed: 1, 63 | isPlayingBgm: true 64 | }; 65 | 66 | // JSDoc comments for typing 67 | /** 68 | * @typedef {{ 69 | * pos: Vector, 70 | * speed: number 71 | * }} Star 72 | */ 73 | 74 | /** 75 | * @type { Star [] } 76 | */ 77 | let stars; 78 | 79 | /** 80 | * @typedef {{ 81 | * pos: Vector, 82 | * firingCooldown: number, 83 | * isFiringLeft: boolean 84 | * }} Player 85 | */ 86 | 87 | /** 88 | * @type { Player } 89 | */ 90 | let player; 91 | 92 | /** 93 | * @typedef {{ 94 | * pos: Vector 95 | * }} FBullet 96 | */ 97 | 98 | /** 99 | * @type { FBullet [] } 100 | */ 101 | let fBullets; 102 | 103 | /** 104 | * @typedef {{ 105 | * pos: Vector, 106 | * firingCooldown: number 107 | * }} Enemy 108 | */ 109 | 110 | /** 111 | * @type { Enemy [] } 112 | */ 113 | let enemies; 114 | 115 | /** 116 | * @typedef {{ 117 | * pos: Vector, 118 | * angle: number, 119 | * rotation: number 120 | * }} EBullet 121 | */ 122 | 123 | /** 124 | * @type { EBullet [] } 125 | */ 126 | let eBullets; 127 | 128 | /** 129 | * @type { number } 130 | */ 131 | let currentEnemySpeed; 132 | 133 | /** 134 | * @type { number } 135 | */ 136 | let waveCount; 137 | 138 | /** 139 | * 140 | */ 141 | 142 | // The game loop function 143 | function update() { 144 | // The init function running at startup 145 | if (!ticks) { 146 | // A CrispGameLib function 147 | // First argument (number): number of times to run the second argument 148 | // Second argument (function): a function that returns an object. This 149 | // object is then added to an array. This array will eventually be 150 | // returned as output of the times() function. 151 | stars = times(20, () => { 152 | // Random number generator function 153 | // rnd( min, max ) 154 | const posX = rnd(0, G.WIDTH); 155 | const posY = rnd(0, G.HEIGHT); 156 | // An object of type Star with appropriate properties 157 | return { 158 | // Creates a Vector 159 | pos: vec(posX, posY), 160 | // More RNG 161 | speed: rnd(G.STAR_SPEED_MIN, G.STAR_SPEED_MAX) 162 | }; 163 | }); 164 | 165 | player = { 166 | pos: vec(G.WIDTH * 0.5, G.HEIGHT * 0.5), 167 | firingCooldown: G.PLAYER_FIRE_RATE, 168 | isFiringLeft: true 169 | }; 170 | 171 | fBullets = []; 172 | enemies = []; 173 | eBullets = []; 174 | 175 | waveCount = 0; 176 | } 177 | 178 | // Spawning enemies 179 | if (enemies.length === 0) { 180 | currentEnemySpeed = 181 | rnd(G.ENEMY_MIN_BASE_SPEED, G.ENEMY_MAX_BASE_SPEED) * difficulty; 182 | for (let i = 0; i < 9; i++) { 183 | const posX = rnd(0, G.WIDTH); 184 | const posY = -rnd(i * G.HEIGHT * 0.1); 185 | enemies.push({ 186 | pos: vec(posX, posY), 187 | firingCooldown: G.ENEMY_FIRE_RATE 188 | }); 189 | } 190 | 191 | waveCount++; // Increase the tracking variable by one 192 | } 193 | 194 | // Update for Star 195 | stars.forEach((s) => { 196 | // Move the star downwards 197 | s.pos.y += s.speed; 198 | // Bring the star back to top once it's past the bottom of the screen 199 | if (s.pos.y > G.HEIGHT) s.pos.y = 0; 200 | 201 | // Choose a color to draw 202 | color("light_black"); 203 | // Draw the star as a square of size 1 204 | box(s.pos, 1); 205 | }); 206 | 207 | // Updating and drawing the player 208 | player.pos = vec(input.pos.x, input.pos.y); 209 | player.pos.clamp(0, G.WIDTH, 0, G.HEIGHT); 210 | // Cooling down for the next shot 211 | player.firingCooldown--; 212 | // Time to fire the next shot 213 | if (player.firingCooldown <= 0) { 214 | // Get the side from which the bullet is fired 215 | const offset = (player.isFiringLeft) 216 | ? -G.PLAYER_GUN_OFFSET 217 | : G.PLAYER_GUN_OFFSET; 218 | // Create the bullet 219 | fBullets.push({ 220 | pos: vec(player.pos.x + offset, player.pos.y) 221 | }); 222 | // Reset the firing cooldown 223 | player.firingCooldown = G.PLAYER_FIRE_RATE; 224 | // Switch the side of the firing gun by flipping the boolean value 225 | player.isFiringLeft = !player.isFiringLeft; 226 | 227 | color("yellow"); 228 | // Generate particles 229 | particle( 230 | player.pos.x + offset, // x coordinate 231 | player.pos.y, // y coordinate 232 | 4, // The number of particles 233 | 1, // The speed of the particles 234 | -PI/2, // The emitting angle 235 | PI/4 // The emitting width 236 | ); 237 | } 238 | color ("black"); 239 | char("a", player.pos); 240 | 241 | // text(fBullets.length.toString(), 3, 10); 242 | 243 | // Updating and drawing bullets 244 | fBullets.forEach((fb) => { 245 | fb.pos.y -= G.FBULLET_SPEED; 246 | 247 | // Drawing fBullets for the first time, allowing interaction from enemies 248 | color("yellow"); 249 | box(fb.pos, 2); 250 | }); 251 | 252 | remove(enemies, (e) => { 253 | e.pos.y += currentEnemySpeed; 254 | e.firingCooldown--; 255 | if (e.firingCooldown <= 0) { 256 | eBullets.push({ 257 | pos: vec(e.pos.x, e.pos.y), 258 | angle: e.pos.angleTo(player.pos), 259 | rotation: rnd() 260 | }); 261 | e.firingCooldown = G.ENEMY_FIRE_RATE; 262 | play("select"); 263 | } 264 | 265 | color("black"); 266 | // Interaction from enemies to fBullets 267 | // Shorthand to check for collision against another specific type 268 | // Also draw the sprits 269 | const isCollidingWithFBullets = char("b", e.pos).isColliding.rect.yellow; 270 | const isCollidingWithPlayer = char("b", e.pos).isColliding.char.a; 271 | if (isCollidingWithPlayer) { 272 | end(); 273 | play("powerUp"); 274 | } 275 | 276 | if (isCollidingWithFBullets) { 277 | color("yellow"); 278 | particle(e.pos); 279 | play("explosion"); 280 | addScore(10 * waveCount, e.pos); 281 | } 282 | 283 | // Also another condition to remove the object 284 | return (isCollidingWithFBullets || e.pos.y > G.HEIGHT); 285 | }); 286 | 287 | remove(fBullets, (fb) => { 288 | // Interaction from fBullets to enemies, after enemies have been drawn 289 | color("yellow"); 290 | const isCollidingWithEnemies = box(fb.pos, 2).isColliding.char.b; 291 | return (isCollidingWithEnemies || fb.pos.y < 0); 292 | }); 293 | 294 | remove(eBullets, (eb) => { 295 | // Old-fashioned trigonometry to find out the velocity on each axis 296 | eb.pos.x += G.EBULLET_SPEED * Math.cos(eb.angle); 297 | eb.pos.y += G.EBULLET_SPEED * Math.sin(eb.angle); 298 | // The bullet also rotates around itself 299 | eb.rotation += G.EBULLET_ROTATION_SPD; 300 | 301 | color("red"); 302 | const isCollidingWithPlayer 303 | = char("c", eb.pos, {rotation: eb.rotation}).isColliding.char.a; 304 | 305 | if (isCollidingWithPlayer) { 306 | // End the game 307 | end(); 308 | play("powerUp"); 309 | } 310 | 311 | const isCollidingWithFBullets 312 | = char("c", eb.pos, {rotation: eb.rotation}).isColliding.rect.yellow; 313 | if (isCollidingWithFBullets) addScore(1, eb.pos); 314 | 315 | // If eBullet is not onscreen, remove it 316 | return (!eb.pos.isInRect(0, 0, G.WIDTH, G.HEIGHT)); 317 | }); 318 | } -------------------------------------------------------------------------------- /docs/step_08/main.js: -------------------------------------------------------------------------------- 1 | // The title of the game to be displayed on the title screen 2 | title = "CHARGE RUSH"; 3 | 4 | // The description, which is also displayed on the title screen 5 | description = ` 6 | Destroy enemies. 7 | `; 8 | 9 | // The array of custom sprites 10 | characters = [ 11 | ` 12 | ll 13 | ll 14 | ccllcc 15 | ccllcc 16 | ccllcc 17 | cc cc 18 | `,` 19 | rr rr 20 | rrrrrr 21 | rrpprr 22 | rrrrrr 23 | rr 24 | rr 25 | `,` 26 | y y 27 | yyyyyy 28 | y y 29 | yyyyyy 30 | y y 31 | ` 32 | ]; 33 | 34 | // Game design variable container 35 | const G = { 36 | WIDTH: 100, 37 | HEIGHT: 150, 38 | 39 | STAR_SPEED_MIN: 0.5, 40 | STAR_SPEED_MAX: 1.0, 41 | 42 | PLAYER_FIRE_RATE: 4, 43 | PLAYER_GUN_OFFSET: 3, 44 | 45 | FBULLET_SPEED: 5, 46 | 47 | ENEMY_MIN_BASE_SPEED: 1.0, 48 | ENEMY_MAX_BASE_SPEED: 2.0, 49 | ENEMY_FIRE_RATE: 45, 50 | 51 | EBULLET_SPEED: 2.0, 52 | EBULLET_ROTATION_SPD: 0.1 53 | }; 54 | 55 | // Game runtime options 56 | // Refer to the official documentation for all available options 57 | options = { 58 | viewSize: {x: G.WIDTH, y: G.HEIGHT}, 59 | isCapturing: true, 60 | isCapturingGameCanvasOnly: true, 61 | captureCanvasScale: 2, 62 | seed: 1, 63 | isPlayingBgm: true, 64 | isReplayEnabled: true, 65 | theme: "dark" 66 | }; 67 | 68 | // JSDoc comments for typing 69 | /** 70 | * @typedef {{ 71 | * pos: Vector, 72 | * speed: number 73 | * }} Star 74 | */ 75 | 76 | /** 77 | * @type { Star [] } 78 | */ 79 | let stars; 80 | 81 | /** 82 | * @typedef {{ 83 | * pos: Vector, 84 | * firingCooldown: number, 85 | * isFiringLeft: boolean 86 | * }} Player 87 | */ 88 | 89 | /** 90 | * @type { Player } 91 | */ 92 | let player; 93 | 94 | /** 95 | * @typedef {{ 96 | * pos: Vector 97 | * }} FBullet 98 | */ 99 | 100 | /** 101 | * @type { FBullet [] } 102 | */ 103 | let fBullets; 104 | 105 | /** 106 | * @typedef {{ 107 | * pos: Vector, 108 | * firingCooldown: number 109 | * }} Enemy 110 | */ 111 | 112 | /** 113 | * @type { Enemy [] } 114 | */ 115 | let enemies; 116 | 117 | /** 118 | * @typedef {{ 119 | * pos: Vector, 120 | * angle: number, 121 | * rotation: number 122 | * }} EBullet 123 | */ 124 | 125 | /** 126 | * @type { EBullet [] } 127 | */ 128 | let eBullets; 129 | 130 | /** 131 | * @type { number } 132 | */ 133 | let currentEnemySpeed; 134 | 135 | /** 136 | * @type { number } 137 | */ 138 | let waveCount; 139 | 140 | /** 141 | * 142 | */ 143 | 144 | // The game loop function 145 | function update() { 146 | // The init function running at startup 147 | if (!ticks) { 148 | // A CrispGameLib function 149 | // First argument (number): number of times to run the second argument 150 | // Second argument (function): a function that returns an object. This 151 | // object is then added to an array. This array will eventually be 152 | // returned as output of the times() function. 153 | stars = times(20, () => { 154 | // Random number generator function 155 | // rnd( min, max ) 156 | const posX = rnd(0, G.WIDTH); 157 | const posY = rnd(0, G.HEIGHT); 158 | // An object of type Star with appropriate properties 159 | return { 160 | // Creates a Vector 161 | pos: vec(posX, posY), 162 | // More RNG 163 | speed: rnd(G.STAR_SPEED_MIN, G.STAR_SPEED_MAX) 164 | }; 165 | }); 166 | 167 | player = { 168 | pos: vec(G.WIDTH * 0.5, G.HEIGHT * 0.5), 169 | firingCooldown: G.PLAYER_FIRE_RATE, 170 | isFiringLeft: true 171 | }; 172 | 173 | fBullets = []; 174 | enemies = []; 175 | eBullets = []; 176 | 177 | waveCount = 0; 178 | } 179 | 180 | // Spawning enemies 181 | if (enemies.length === 0) { 182 | currentEnemySpeed = 183 | rnd(G.ENEMY_MIN_BASE_SPEED, G.ENEMY_MAX_BASE_SPEED) * difficulty; 184 | for (let i = 0; i < 9; i++) { 185 | const posX = rnd(0, G.WIDTH); 186 | const posY = -rnd(i * G.HEIGHT * 0.1); 187 | enemies.push({ 188 | pos: vec(posX, posY), 189 | firingCooldown: G.ENEMY_FIRE_RATE 190 | }); 191 | } 192 | 193 | waveCount++; // Increase the tracking variable by one 194 | } 195 | 196 | // Update for Star 197 | stars.forEach((s) => { 198 | // Move the star downwards 199 | s.pos.y += s.speed; 200 | // Bring the star back to top once it's past the bottom of the screen 201 | if (s.pos.y > G.HEIGHT) s.pos.y = 0; 202 | 203 | // Choose a color to draw 204 | color("light_black"); 205 | // Draw the star as a square of size 1 206 | box(s.pos, 1); 207 | }); 208 | 209 | // Updating and drawing the player 210 | player.pos = vec(input.pos.x, input.pos.y); 211 | player.pos.clamp(0, G.WIDTH, 0, G.HEIGHT); 212 | // Cooling down for the next shot 213 | player.firingCooldown--; 214 | // Time to fire the next shot 215 | if (player.firingCooldown <= 0) { 216 | // Get the side from which the bullet is fired 217 | const offset = (player.isFiringLeft) 218 | ? -G.PLAYER_GUN_OFFSET 219 | : G.PLAYER_GUN_OFFSET; 220 | // Create the bullet 221 | fBullets.push({ 222 | pos: vec(player.pos.x + offset, player.pos.y) 223 | }); 224 | // Reset the firing cooldown 225 | player.firingCooldown = G.PLAYER_FIRE_RATE; 226 | // Switch the side of the firing gun by flipping the boolean value 227 | player.isFiringLeft = !player.isFiringLeft; 228 | 229 | color("yellow"); 230 | // Generate particles 231 | particle( 232 | player.pos.x + offset, // x coordinate 233 | player.pos.y, // y coordinate 234 | 4, // The number of particles 235 | 1, // The speed of the particles 236 | -PI/2, // The emitting angle 237 | PI/4 // The emitting width 238 | ); 239 | } 240 | color ("black"); 241 | char("a", player.pos); 242 | 243 | // text(fBullets.length.toString(), 3, 10); 244 | 245 | // Updating and drawing bullets 246 | fBullets.forEach((fb) => { 247 | fb.pos.y -= G.FBULLET_SPEED; 248 | 249 | // Drawing fBullets for the first time, allowing interaction from enemies 250 | color("yellow"); 251 | box(fb.pos, 2); 252 | }); 253 | 254 | remove(enemies, (e) => { 255 | e.pos.y += currentEnemySpeed; 256 | e.firingCooldown--; 257 | if (e.firingCooldown <= 0) { 258 | eBullets.push({ 259 | pos: vec(e.pos.x, e.pos.y), 260 | angle: e.pos.angleTo(player.pos), 261 | rotation: rnd() 262 | }); 263 | e.firingCooldown = G.ENEMY_FIRE_RATE; 264 | play("select"); 265 | } 266 | 267 | color("black"); 268 | // Interaction from enemies to fBullets 269 | // Shorthand to check for collision against another specific type 270 | // Also draw the sprits 271 | const isCollidingWithFBullets = char("b", e.pos).isColliding.rect.yellow; 272 | const isCollidingWithPlayer = char("b", e.pos).isColliding.char.a; 273 | if (isCollidingWithPlayer) { 274 | end(); 275 | play("powerUp"); 276 | } 277 | 278 | if (isCollidingWithFBullets) { 279 | color("yellow"); 280 | particle(e.pos); 281 | play("explosion"); 282 | addScore(10 * waveCount, e.pos); 283 | } 284 | 285 | // Also another condition to remove the object 286 | return (isCollidingWithFBullets || e.pos.y > G.HEIGHT); 287 | }); 288 | 289 | remove(fBullets, (fb) => { 290 | // Interaction from fBullets to enemies, after enemies have been drawn 291 | color("yellow"); 292 | const isCollidingWithEnemies = box(fb.pos, 2).isColliding.char.b; 293 | return (isCollidingWithEnemies || fb.pos.y < 0); 294 | }); 295 | 296 | remove(eBullets, (eb) => { 297 | // Old-fashioned trigonometry to find out the velocity on each axis 298 | eb.pos.x += G.EBULLET_SPEED * Math.cos(eb.angle); 299 | eb.pos.y += G.EBULLET_SPEED * Math.sin(eb.angle); 300 | // The bullet also rotates around itself 301 | eb.rotation += G.EBULLET_ROTATION_SPD; 302 | 303 | color("red"); 304 | const isCollidingWithPlayer 305 | = char("c", eb.pos, {rotation: eb.rotation}).isColliding.char.a; 306 | 307 | if (isCollidingWithPlayer) { 308 | // End the game 309 | end(); 310 | play("powerUp"); 311 | } 312 | 313 | const isCollidingWithFBullets 314 | = char("c", eb.pos, {rotation: eb.rotation}).isColliding.rect.yellow; 315 | if (isCollidingWithFBullets) addScore(1, eb.pos); 316 | 317 | // If eBullet is not onscreen, remove it 318 | return (!eb.pos.isInRect(0, 0, G.WIDTH, G.HEIGHT)); 319 | }); 320 | } -------------------------------------------------------------------------------- /docs/bundle.d.ts: -------------------------------------------------------------------------------- 1 | declare let title: string; 2 | declare let description: string; 3 | declare let characters: string[]; 4 | declare type ThemeName = 5 | | "simple" 6 | | "pixel" 7 | | "shape" 8 | | "shapeDark" 9 | | "crt" 10 | | "dark"; 11 | declare type Options = { 12 | isPlayingBgm?: boolean; 13 | isSpeedingUpSound?: boolean; 14 | isCapturing?: boolean; 15 | isCapturingGameCanvasOnly?: boolean; 16 | captureCanvasScale?: number; 17 | isShowingScore?: boolean; 18 | isShowingTime?: boolean; 19 | isReplayEnabled?: boolean; 20 | isRewindEnabled?: boolean; 21 | isDrawingParticleFront?: boolean; 22 | isDrawingScoreFront?: boolean; 23 | isMinifying?: boolean; 24 | viewSize?: { x: number; y: number }; 25 | seed?: number; 26 | theme?: ThemeName; 27 | }; 28 | declare let options: Options; 29 | declare function update(); 30 | 31 | declare let ticks: number; 32 | // difficulty (Starts from 1, increments by a minute) 33 | declare let difficulty: number; 34 | // score 35 | declare let score: number; 36 | 37 | // Add score 38 | declare function addScore(value: number); 39 | declare function addScore(value: number, x: number, y: number); 40 | declare function addScore(value: number, pos: VectorLike); 41 | 42 | // End game 43 | declare function end(gameOverText?: string); 44 | declare function complete(completeText?: string); 45 | 46 | // color 47 | declare type Color = 48 | | "transparent" 49 | | "white" 50 | | "red" 51 | | "green" 52 | | "yellow" 53 | | "blue" 54 | | "purple" 55 | | "cyan" 56 | | "black" 57 | | "light_red" 58 | | "light_green" 59 | | "light_yellow" 60 | | "light_blue" 61 | | "light_purple" 62 | | "light_cyan" 63 | | "light_black"; 64 | declare function color(colorName: Color); 65 | 66 | // Draw functions return a collision info. 67 | type Collision = { 68 | isColliding: { 69 | rect?: { 70 | transparent?: boolean; 71 | white?: boolean; 72 | red?: boolean; 73 | green?: boolean; 74 | yellow?: boolean; 75 | blue?: boolean; 76 | purple?: boolean; 77 | cyan?: boolean; 78 | black?: boolean; 79 | light_red?: boolean; 80 | light_green?: boolean; 81 | light_yellow?: boolean; 82 | light_blue?: boolean; 83 | light_purple?: boolean; 84 | light_cyan?: boolean; 85 | light_black?: boolean; 86 | }; 87 | text?: { [k: string]: boolean }; 88 | char?: { [k: string]: boolean }; 89 | }; 90 | }; 91 | 92 | // Draw rectangle 93 | declare function rect( 94 | x: number, 95 | y: number, 96 | width: number, 97 | height?: number 98 | ): Collision; 99 | declare function rect(x: number, y: number, size: VectorLike): Collision; 100 | declare function rect( 101 | pos: VectorLike, 102 | width: number, 103 | height?: number 104 | ): Collision; 105 | declare function rect(pos: VectorLike, size: VectorLike): Collision; 106 | 107 | // Draw box (center-aligned rect) 108 | declare function box( 109 | x: number, 110 | y: number, 111 | width: number, 112 | height?: number 113 | ): Collision; 114 | declare function box(x: number, y: number, size: VectorLike): Collision; 115 | declare function box( 116 | pos: VectorLike, 117 | width: number, 118 | height?: number 119 | ): Collision; 120 | declare function box(pos: VectorLike, size: VectorLike): Collision; 121 | 122 | // Draw bar (angled rect) 123 | declare function bar( 124 | x: number, 125 | y: number, 126 | length: number, 127 | thickness: number, 128 | rotate: number, 129 | centerPosRatio?: number 130 | ): Collision; 131 | declare function bar( 132 | pos: VectorLike, 133 | length: number, 134 | thickness: number, 135 | rotate: number, 136 | centerPosRatio?: number 137 | ): Collision; 138 | 139 | // Draw line 140 | declare function line( 141 | x1: number, 142 | y1: number, 143 | x2: number, 144 | y2: number, 145 | thickness?: number 146 | ): Collision; 147 | declare function line( 148 | x1: number, 149 | y1: number, 150 | p2: VectorLike, 151 | thickness?: number 152 | ): Collision; 153 | declare function line( 154 | p1: VectorLike, 155 | x2: number, 156 | y2: number, 157 | thickness?: number 158 | ): Collision; 159 | declare function line( 160 | p1: VectorLike, 161 | p2: VectorLike, 162 | thickness?: number 163 | ): Collision; 164 | 165 | // Draw arc 166 | declare function arc( 167 | centerX: number, 168 | centerY: number, 169 | radius: number, 170 | thickness?: number, 171 | angleFrom?: number, 172 | angleTo?: number 173 | ): Collision; 174 | declare function arc( 175 | centerPos: VectorLike, 176 | radius: number, 177 | thickness?: number, 178 | angleFrom?: number, 179 | angleTo?: number 180 | ): Collision; 181 | 182 | // Draw letters 183 | declare type LetterOptions = { 184 | color?: Color; 185 | backgroundColor?: Color; 186 | rotation?: number; 187 | mirror?: { x?: 1 | -1; y?: 1 | -1 }; 188 | scale?: { x?: number; y?: number }; 189 | }; 190 | 191 | declare function text( 192 | str: string, 193 | x: number, 194 | y: number, 195 | options?: LetterOptions 196 | ): Collision; 197 | 198 | declare function text( 199 | str: string, 200 | pos: VectorLike, 201 | options?: LetterOptions 202 | ): Collision; 203 | 204 | declare function char( 205 | str: string, 206 | x: number, 207 | y: number, 208 | options?: LetterOptions 209 | ): Collision; 210 | 211 | declare function char( 212 | str: string, 213 | pos: VectorLike, 214 | options?: LetterOptions 215 | ): Collision; 216 | 217 | // Add particles 218 | declare function particle( 219 | x: number, 220 | y: number, 221 | count?: number, 222 | speed?: number, 223 | angle?: number, 224 | angleWidth?: number 225 | ); 226 | declare function particle( 227 | pos: VectorLike, 228 | count?: number, 229 | speed?: number, 230 | angle?: number, 231 | angleWidth?: number 232 | ); 233 | 234 | // Record/Restore a frame state for replaying and rewinding 235 | declare function frameState(state: any): any; 236 | 237 | // Rewind a game 238 | declare function rewind(); 239 | 240 | // Return Vector 241 | declare function vec(x?: number | VectorLike, y?: number): Vector; 242 | 243 | // Return random number 244 | declare function rnd(lowOrHigh?: number, high?: number); 245 | // Return random integer 246 | declare function rndi(lowOrHigh?: number, high?: number); 247 | // Return plus of minus random number 248 | declare function rnds(lowOrHigh?: number, high?: number); 249 | 250 | // Input (mouse, touch, keyboard) 251 | declare type Input = { 252 | pos: Vector; 253 | isPressed: boolean; 254 | isJustPressed: boolean; 255 | isJustReleased: boolean; 256 | }; 257 | declare let input: Input; 258 | 259 | declare type KeyboardCode = 260 | | "Escape" 261 | | "Digit0" 262 | | "Digit1" 263 | | "Digit2" 264 | | "Digit3" 265 | | "Digit4" 266 | | "Digit5" 267 | | "Digit6" 268 | | "Digit7" 269 | | "Digit8" 270 | | "Digit9" 271 | | "Minus" 272 | | "Equal" 273 | | "Backspace" 274 | | "Tab" 275 | | "KeyQ" 276 | | "KeyW" 277 | | "KeyE" 278 | | "KeyR" 279 | | "KeyT" 280 | | "KeyY" 281 | | "KeyU" 282 | | "KeyI" 283 | | "KeyO" 284 | | "KeyP" 285 | | "BracketLeft" 286 | | "BracketRight" 287 | | "Enter" 288 | | "ControlLeft" 289 | | "KeyA" 290 | | "KeyS" 291 | | "KeyD" 292 | | "KeyF" 293 | | "KeyG" 294 | | "KeyH" 295 | | "KeyJ" 296 | | "KeyK" 297 | | "KeyL" 298 | | "Semicolon" 299 | | "Quote" 300 | | "Backquote" 301 | | "ShiftLeft" 302 | | "Backslash" 303 | | "KeyZ" 304 | | "KeyX" 305 | | "KeyC" 306 | | "KeyV" 307 | | "KeyB" 308 | | "KeyN" 309 | | "KeyM" 310 | | "Comma" 311 | | "Period" 312 | | "Slash" 313 | | "ShiftRight" 314 | | "NumpadMultiply" 315 | | "AltLeft" 316 | | "Space" 317 | | "CapsLock" 318 | | "F1" 319 | | "F2" 320 | | "F3" 321 | | "F4" 322 | | "F5" 323 | | "F6" 324 | | "F7" 325 | | "F8" 326 | | "F9" 327 | | "F10" 328 | | "Pause" 329 | | "ScrollLock" 330 | | "Numpad7" 331 | | "Numpad8" 332 | | "Numpad9" 333 | | "NumpadSubtract" 334 | | "Numpad4" 335 | | "Numpad5" 336 | | "Numpad6" 337 | | "NumpadAdd" 338 | | "Numpad1" 339 | | "Numpad2" 340 | | "Numpad3" 341 | | "Numpad0" 342 | | "NumpadDecimal" 343 | | "IntlBackslash" 344 | | "F11" 345 | | "F12" 346 | | "F13" 347 | | "F14" 348 | | "F15" 349 | | "F16" 350 | | "F17" 351 | | "F18" 352 | | "F19" 353 | | "F20" 354 | | "F21" 355 | | "F22" 356 | | "F23" 357 | | "F24" 358 | | "IntlYen" 359 | | "Undo" 360 | | "Paste" 361 | | "MediaTrackPrevious" 362 | | "Cut" 363 | | "Copy" 364 | | "MediaTrackNext" 365 | | "NumpadEnter" 366 | | "ControlRight" 367 | | "LaunchMail" 368 | | "AudioVolumeMute" 369 | | "MediaPlayPause" 370 | | "MediaStop" 371 | | "Eject" 372 | | "AudioVolumeDown" 373 | | "AudioVolumeUp" 374 | | "BrowserHome" 375 | | "NumpadDivide" 376 | | "PrintScreen" 377 | | "AltRight" 378 | | "Help" 379 | | "NumLock" 380 | | "Pause" 381 | | "Home" 382 | | "ArrowUp" 383 | | "PageUp" 384 | | "ArrowLeft" 385 | | "ArrowRight" 386 | | "End" 387 | | "ArrowDown" 388 | | "PageDown" 389 | | "Insert" 390 | | "Delete" 391 | | "OSLeft" 392 | | "OSRight" 393 | | "ContextMenu" 394 | | "BrowserSearch" 395 | | "BrowserFavorites" 396 | | "BrowserRefresh" 397 | | "BrowserStop" 398 | | "BrowserForward" 399 | | "BrowserBack"; 400 | 401 | declare type KeyboardCodeState = { 402 | [key in KeyboardCode]: { 403 | isPressed: boolean; 404 | isJustPressed: boolean; 405 | isJustReleased: boolean; 406 | }; 407 | }; 408 | 409 | declare type Keyboard = { 410 | isPressed: boolean; 411 | isJustPressed: boolean; 412 | isJustReleased: boolean; 413 | code: KeyboardCodeState; 414 | }; 415 | 416 | declare let keyboard: Keyboard; 417 | 418 | declare type Pointer = { 419 | pos: Vector; 420 | isPressed: boolean; 421 | isJustPressed: boolean; 422 | isJustReleased: boolean; 423 | }; 424 | 425 | declare let pointer: Pointer; 426 | 427 | // Play sound 428 | declare type SoundEffectType = 429 | | "coin" 430 | | "laser" 431 | | "explosion" 432 | | "powerUp" 433 | | "hit" 434 | | "jump" 435 | | "select" 436 | | "lucky"; 437 | declare function play(type: SoundEffectType); 438 | 439 | declare const PI: number; 440 | declare function abs(v: number): number; 441 | declare function sin(v: number): number; 442 | declare function cos(v: number): number; 443 | declare function atan2(y: number, x: number): number; 444 | declare function pow(b: number, e: number): number; 445 | declare function sqrt(v: number): number; 446 | declare function floor(v: number): number; 447 | declare function round(v: number): number; 448 | declare function ceil(v: number): number; 449 | declare function clamp(v: number, low?: number, high?: number): number; 450 | declare function wrap(v: number, low: number, high: number): number; 451 | declare function range(v: number): number[]; 452 | declare function times(count: number, func: (index: number) => T): T[]; 453 | declare function remove( 454 | array: T[], 455 | func: (v: T, index?: number) => any 456 | ): T[]; 457 | declare function addWithCharCode(char: string, offset: number): string; 458 | 459 | declare interface Vector { 460 | x: number; 461 | y: number; 462 | constructor(x?: number | VectorLike, y?: number); 463 | set(x?: number | VectorLike, y?: number): this; 464 | add(x?: number | VectorLike, y?: number): this; 465 | sub(x?: number | VectorLike, y?: number): this; 466 | mul(v: number): this; 467 | div(v: number): this; 468 | clamp(xLow: number, xHigh: number, yLow: number, yHigh: number): this; 469 | wrap(xLow: number, xHigh: number, yLow: number, yHigh: number): this; 470 | addWithAngle(angle: number, length: number): this; 471 | swapXy(): this; 472 | normalize(): this; 473 | rotate(angle: number): this; 474 | angleTo(x?: number | VectorLike, y?: number): number; 475 | distanceTo(x?: number | VectorLike, y?: number): number; 476 | isInRect(x: number, y: number, width: number, height: number): boolean; 477 | equals(other: VectorLike): boolean; 478 | floor(): this; 479 | round(): this; 480 | ceil(): this; 481 | length: number; 482 | angle: number; 483 | } 484 | 485 | declare interface VectorLike { 486 | x: number; 487 | y: number; 488 | } 489 | 490 | // Button 491 | declare type Button = { 492 | pos: VectorLike; 493 | size: VectorLike; 494 | text: string; 495 | isToggle: boolean; 496 | onClick: () => void; 497 | isPressed: boolean; 498 | isSelected: boolean; 499 | isHovered: boolean; 500 | toggleGroup: Button[]; 501 | }; 502 | 503 | declare function getButton({ 504 | pos, 505 | size, 506 | text, 507 | isToggle, 508 | onClick, 509 | }: { 510 | pos: VectorLike; 511 | size: VectorLike; 512 | text: string; 513 | isToggle?: boolean; 514 | onClick?: () => void; 515 | }): Button; 516 | 517 | declare function updateButton(button: Button); 518 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guide to getting started with CrispGameLib 2 | 3 | [cgl-url]: https://github.com/abagames/crisp-game-lib 4 | [aba-url]: https://github.com/abagames 5 | [aba-asa]: http://www.asahi-net.or.jp/~cs8k-cyu/ 6 | [cgl-jun]: https://github.com/JunoNgx/crips-game-lib-collection 7 | [crr]: https://junongx.github.io/crips-game-lib-collection/?chargerushre 8 | [cro]: http://abagames.sakura.ne.jp/html5/cr/ 9 | 10 | Welcome to my tutorial for [CrispGameLib][cgl-url]. 11 | 12 | As someone who has absolutely been in love with the entirety of [ABAGames][aba-url]' (Kenta Cho) works, I eventually got around to use **CrispGameLib** in July 2021, and had probably one of my best developement experiences ever. It eventually struck me that despite the library's simplicity and low barrier of entry, its popularity has been low, and I and Kenta appeared to be the only creators who used this library. 13 | 14 | Here's my attempt to change that. If you are into making videogames and looking for something interesting, I hope I have found you one right here. 15 | 16 | # Table of content 17 | 18 | - [About CrispGameLib](#about-crispgamelib) 19 | - [The goal](#the-goal) 20 | - [What you need](#what-you-need) 21 | - [How to read this tutorial](#how-to-read-this-tutorial) 22 | - [The tutorial](#the-tutorial) 23 | - [Step 00: Setting up](#step-00-setting-up) 24 | - [Step 001: Getting the software](#step-001-getting-the-software) 25 | - [Step 002: Getting the library](#step-002-getting-the-library) 26 | - [Step 003: Setup the npm package](#step-003-setup-the-npm-package) 27 | - [Step 01: Basic drawing and update (stars)](#step-01-basic-drawing-and-update-stars) 28 | - [Step 011: Renaming title](#step-011-renaming-title) 29 | - [Step 012: Create the tuning data container and change the size](#step-012-create-the-tuning-data-container-and-change-the-size) 30 | - [Step 013: Container variable and JSDoc](#step-013-container-variable-and-jsdoc) 31 | - [Step 014: The initialising block](#step-014-the-initialising-block) 32 | - [Step 015: The update loop](#step-015-the-update-loop) 33 | - [Step 02: Input and control (player)](#step-02-input-and-control-player) 34 | - [Step 021: Another type](#step-021-another-type) 35 | - [Step 022: Input handling](#step-022-input-handling) 36 | - [Step 023: Custom sprite](#step-023-custom-sprite) 37 | - [Step 03: Object control, creation, and removal (fBullets)](#step-03-object-control-creation-and-removal-fbullets) 38 | - [Step 031: Firing bullets](#step-031-firing-bullets) 39 | - [Step 032: Object management and removal](#step-032-object-management-and-removal) 40 | - [Step 033: Dual barrels](#step-033-dual-barrels) 41 | - [Step 034: Muzzleflash and particles](#step-034-muzzleflash-and-particles) 42 | - [Step 04: Mechanic control (enemies)](#step-04-mechanic-control-enemies) 43 | - [Step 041: The formation](#step-041-the-formation) 44 | - [Step 042: Processing the Enemy](#step-042-processing-the-enemy) 45 | - [Step 043: Spawning](#step-043-spawning) 46 | - [Step 05: Collision detection](#step-05-collision-detection) 47 | - [Step 051: Destroying enemies](#step-051-destroying-enemies) 48 | - [Step 052: Two-way interaction](#step-052-two-way-interaction) 49 | - [Step 06: How audio works](#step-06-how-audio-works) 50 | - [Step 061: The basic way](#step-061-the-basic-way) 51 | - [Step 062: Infinite sound](#step-062-infinite-sound) 52 | - [Step 07: More complex movements (eBullets)](#step-07-more-complex-movements-ebullets) 53 | - [Step 071: Enemy bullets](#step-071-enemy-bullets) 54 | - [Step 072: Scoring](#step-072-scoring) 55 | - [Step 08: Extra goodies](#step-08-extra-goodies) 56 | - [Step 081: Replay](#step-081-replay) 57 | - [Step 082: Themes](#step-082-themes) 58 | - [Step 083: GIF capturing](#step-083-gif-capturing) 59 | - [Game Distribution](#game-distribution) 60 | - [Community](#community) 61 | - [Feedback and Critique](#feedback-and-critique) 62 | 63 | # About CrispGameLib 64 | 65 | [CrispGameLib][cgl-url] is a Javascript game library geared towards making arcade-like mini games for web browsers. I believe it's fair to say that it's the spritual successor to [Mini Game Programming Library](https://github.com/abagames/mgl) and [MGL-coffee](https://github.com/abagames/mgl.coffee), both of which were made by Kenta Cho. 66 | 67 | CrispGameLib priotizes simplicity and leanness of the game, taking care of many common elements, allowing the developer to focus on the creating gameplay, prototyping and getting the game to a playable state. Here are some notable facts: 68 | 69 | * Games are playable only on web browsers, presented as [HTML5 canvas](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas). 70 | * The gameloop, collision detection, and high scores, are automatically handled under the hood. 71 | * All drawn sprites are limited to squared shapes of a preset of 16 colours. 72 | * Custom sprites are limited to the size of 6x6, represented by characters defined in the array `characters []` (more on this later). 73 | * Audio and music are procedurally generated and limited to a set of 8 sound effects (also, more on this later). 74 | * Control is restricted to one pointer, controlled with mouse or single touch. 75 | 76 | Needless to say, like most game engines and libraries out there, CrispGameLib is great for a particular kind of games, and not so great for others. If you are making a massive open world RPG with a lot of fine tuning and complex systems, this is not going to cut it. On the other hand, if you are prototyping an idea for smartphones you have in mind, or just looking for something small you can spend on for less than an afternoon, here's a great choice of tool. 77 | 78 | A game speaks a million words, so do check out [Kenta's works][aba-asa] and [mine][cgl-jun] for a good idea of what this library can do. 79 | 80 | # The goal 81 | 82 | This is a project driven tutorial. We are going to learn gamedev by making a very particular game: [Charge Rush RE][crr], which is my own remake of the mgl.coffee-powered [CHARGE RUSH][cro] by Kenta himself. This is a game I have massively enjoyed for many years over, which also has a great balance of simplicity, complexity, and depth, in from both gameplay and gamedev perspectives. These are great properties for a learning project. The final game does have some small deviations, which you can take a look at [here](https://junongx.github.io/crisp-game-lib-tutorial/?chargerush). 83 | 84 | At the end of the tutorial, besides having your own version of Charge Rush running and working, hopefully you'll have a good idea of: 85 | * The code structure and thinking process to create a game with CrispGameLib. 86 | * Using GameCrispLib features, including drawing, resolving collisions, using audio, and managing scores. 87 | * How game data are structured, accessed, and iterated in container arrays, with or without using CrispGameLib built-in functions (`times()` and `remove()`). 88 | 89 | Bonus things that would be extremely great if you could get an understanding of: 90 | * How CrispGameLib works under the hood and its quirks. 91 | * How to optimize the collision detection processes. 92 | * The software development practices I discuss and your own opinionated preferences. 93 | * How to make your next game. 94 | * How to use *JSDoc* to benefit a Javascript project. 95 | * How to distribute and deploy a CrispGameLib game on the web via GitHub Page. 96 | 97 | # What you need 98 | 99 | As much as I would like to make this tutorial as accessible as possible, covering `hello world` is unfortunately out of the scope of this tutorial. You don't need to be a Javascript expert, but it is necessary that you have **an understanding of basic programming** (especially including: the concept of variables, performing operations, conditions, loops, and functions). Basically, if you're relatively fluent in any programming language, you're good to go. 100 | 101 | You'll also need a capable device that can operate the **NodeJS** ecosystem and **a web browser that runs HTML5** (probably good as long as it's not Internet Explorer). Technically, you can use any IDE or text editor, but I personally find **VSCode** so well-optimized to this that it's a no-brainer choice. 102 | 103 | You also don't need any previous gamedev experience (this is a great choice for your first), though I will occasional make comparisons to other popular game engines to explain GameCrispLib's quirks. 104 | 105 | **Git and version control** are not essential for you to benefit from this tutorial, but is highly recommended like any work involving software. 106 | 107 | Finally, this tutorial was written on Windows, so don't freak out if things look a bit different on your Mac or Linux devices. 108 | 109 | # How to read this tutorial 110 | 111 | Just read it like you should read any tutorial: take it slow, make sure you get the part right, and try not to skip. 112 | 113 | In the folder `docs` of this repository, you'll find folders named `step_xx`, representing the incomplete versions of the game, corresponding to the steps of this tutorial. You can either access them locally by visiting the corresponding URL from your web browser `http://localhost:4000/?step_xx` after running `light-server`, or visiting the deployment via GitHub Page, listed at the end of each step. This might sound confusing now, but you'll get the idea after setting up at step 0. Use these as references for your progress in case you run into any problem. 114 | 115 | 116 | Additionally, you'll also run into certain notations where I explain certain aspects of making the game: 117 | * **CrispGameLib quirk**: explanation of the inner workings of the library those are most likely unusual compared to other tools that you have heard of. 118 | * **Javascript feature**: self-explanatorily, this tutorial assumes that are you unfamiliar with Javascript and will briefly explain features or aspects of the language when it's due. 119 | * **Under the hood**: this is where I will explain shorthand commands and how things inner work behind the scene to give you a bit more knowledge. Hopefully, things will look a bit less like magic to you. 120 | * **Further reading**: self-explanatorily, there is only so much I can cover in one single tutorial and some matters are best researched in-depth separately. 121 | * **Alternative implementation**: many problems or outcomes have no one single definite solution. Occasionally, I will provide an alternative implementation that has some sort of merits you can consider which would hopefully reinforce your understanding of the matter. 122 | * **For your experimentation**: this is where I encourage you to mildly deviate from the model code and do something yourself. These are generally harmless or have very little effect on the game, but will reinforce your understanding of how the codebase works. 123 | * **SWE practice**: while it might be strange to see the term software engineering slung around in beginner-level and simplicity-focused tutorial, but as a software developer myself, I advocate for readable and maintainable codebases. While this is somewhat contradictory to the nature and purpose of this library (games made quick and fast), I believe a healthy balance can be achieved. I will suggest some basic rule you should follow to maintain a good codebase in these sections. 124 | 125 | Naturally, this tutorial is highly opinionated and based on my personal experiences and understanding. You are highly encouraged to develop your own preferences and stick to them. I also highly welcome feedback and critiques; feel free to contact me in anyway you can regarding those. 126 | 127 | # The tutorial 128 | 129 | This is where the fun begins and things start happening on your computer. 130 | 131 | ## Step 00: Setting up 132 | 133 | ### Step 001: Getting the software 134 | 135 | Like any development work, before we even get to do anything at all on the game, some software installation and build environment setup is due. This is done only once on each device system that you work on. These are very ubiquitous software for development devices. Go to each URL, follow the installation prompting, and proceed with default settings should get it done. 136 | 137 | * [Git](https://git-scm.com/downloads) (can be omitted, but I strongly recommend you not to) 138 | * [NodeJS](https://nodejs.org/en/download/) 139 | * A terminal of your choice. I personally use [Hyper](https://hyper.is/#installation). You'll also need to enable `bash` [if you're on Windows](https://gist.github.com/coco-napky/404220405435b3d0373e37ec43e54a23). 140 | * A text editor/IDE of your choice. This tutorial assumes you are using [VSCode](https://code.visualstudio.com/). 141 | 142 | ---- 143 | **Further reading**: At some point, you should also register a GitHub account if you have not had one and [setup an SSH authentication](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh), which will be a significant life improvement when you start pushing your code to remote repositories frequently. 144 | 145 | ---- 146 | 147 | ### Step 002: Getting the library 148 | 149 | Once done, get a distributable version of CrispGameLib, and the simplest way to do so is to clone this repository. 150 | 151 | Navigate to the directory where you'd like to work on with the terminal (alternatively, use your operating system's file explorer and opens the terminal there) and enter: 152 | 153 | ``` 154 | git clone git@github.com:JunoNgx/crisp-game-lib-tutorial.git 155 | cd crisp-game-lib-tutorial 156 | ``` 157 | 158 | The second command will navigate the terminal into the newly cloned repository folder. 159 | 160 | ---- 161 | **Alternatively**: You can just [download this repository directly](https://github.com/JunoNgx/crisp-game-lib-tutorial/archive/refs/heads/master.zip), unzip it, and work from there. Or you can even get it directly from the original repository, after which you should do some cleanup in `docs` because of existing games. 162 | 163 | ---- 164 | 165 | ### Step 003: Setup the npm package 166 | 167 | In case you're not aware, this is an npm package. 168 | 169 | ---- 170 | **Further reading**: [What is npm? A Node Package Manager Tutorial for Beginners](https://www.freecodecamp.org/news/what-is-npm-a-node-package-manager-tutorial-for-beginners/) 171 | 172 | ---- 173 | 174 | To get the package setup and working, run `npm install` from the terminal. 175 | 176 | In ways you feel comfortable with, go to the folder `docs`, make a copy of `docs/_template` in the same place and rename it to `chargerush`. 177 | 178 | Return to your terminal and enter `npm run watch_games`. You should now no longer be able to type into the console (hint: if you'd like to exit, press `CTRL + C`). Meanwhile, open your browser and access the URL `http://localhost:4000/?chargerush`. 179 | 180 | ---- 181 | **Under the hood**: if you look into `package.json`, you will notice that `npm run watch_games` is a shorthand for `"light-server -s docs -w \"docs/**/* # # reload\""`, which initialises `light-server`, which is an npm package that allows you to run a static http server with livereloading (meaning that every time you save, the server will restart and refresh, running your new code immediately. Pretty magic, huh?). You don't need to know everything about `light-server`, but it's useful to understand [what it is](https://www.npmjs.com/package/light-server). 182 | 183 | ---- 184 | 185 | If you see a square bright screen against a slightly darker background, with what appears to be score and high score on the top corners, then congratulations, you've done that right 🥂. 186 | 187 | ![Step 000 - Engine running](images/step_001.png) 188 | 189 | In case you are not getting there yet: 190 | * Check the terminal and make sure that `light-server` is running. 191 | * Check `docs` folder and make sure that the copied template is correctly named. 192 | * Check the browser and make sure that you are accessing the `localhost` URL (not `0.0.0.0`), pointing to the right name of the folder after the question mark. 193 | 194 | Once you've got the game running, open VSCode in the root folder of the repository, and open the file `docs/chargerush/main.js`. It's up to your personal preference, but my favourite setup involves halving the screen into VSCode and the browser running the game. 195 | 196 | ![My setup](images/step_002.png) 197 | 198 | If you ever pause this tutorial to return another time, **don't forget** to run `light-server` again with `npm run watch_games`. 199 | 200 | Things will get interesting from here. 201 | 202 | ---- 203 | **Hint**: VSCode also has a built-in terminal. You may either run the server or operate `git` commands from there, saving another terminal window. 204 | 205 | ---- 206 | 207 | Step 00 conclusion: [deployment](https://junongx.github.io/crisp-game-lib-tutorial/?step_00) / [code](https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/master/docs/step_00/main.js). 208 | 209 | ## Step 01: Basic drawing and update (stars) 210 | 211 | ### Step 011: Renaming title 212 | 213 | The content of the template `main.js` is relatively lean. Comments have been added in for your information: 214 | 215 | ```javascript 216 | // The title of the game to be displayed on the title screen 217 | title = ""; 218 | 219 | // The description, which is also displayed on the title screen 220 | description = ` 221 | `; 222 | 223 | // The array of custom sprites 224 | characters = []; 225 | 226 | // Game runtime options 227 | // Refer to the official documentation for all available options 228 | options = {}; 229 | 230 | // The game loop function 231 | function update() { 232 | // The init function 233 | if (!ticks) { 234 | 235 | } 236 | } 237 | ``` 238 | ---- 239 | **SWE practice**: Do be very mindful of indentations. Incorrect indentations make the codes hard to read, on top of causing complications in diffs in version control. This template and tutorial are set to indentation of 4 whitespaces. **Further reading**: [Indentation Style](https://en.wikipedia.org/wiki/Indentation_style). 240 | 241 | ---- 242 | 243 | Let's do the minimally important thing: changing the game name. Edit the first line: 244 | 245 | ```javascript 246 | title = "CHARGE RUSH"; 247 | ``` 248 | 249 | ![Changed name](images/step_011.png) 250 | 251 | As soon as you save the file, the server should automatically reload and the browser should now shows the game with its title `CHARGE RUSH`. Feeling excited yet? 252 | 253 | ### Step 012: Create the tuning data container and change the size 254 | 255 | Next, we will create a Javascript object which will hold a lot of the game's important data. Add this block just above the `options`. 256 | 257 | ```javascript 258 | const G = { 259 | WIDTH: 100, 260 | HEIGHT: 150 261 | }; 262 | ``` 263 | ---- 264 | **SWE practice**: this object is declared as a `const` (for constant), which means its value is read-only once the game is started. Constant values should be capitalised in `CAPITALISED_SNAKE_CASE`, as these are essential values we will refer to over and over again throughout the codebase (this is premature, but you will soon see this enough, and also in contrast to local temporary `const` variables which I will use later on). **Further reading**: [When to capitalize your JavaScript constants](https://www.freecodecamp.org/news/when-to-capitalize-your-javascript-constants-4fabc0a4a4c4/). 265 | 266 | ---- 267 | 268 | We now may use these values to change the size of the game: 269 | 270 | ```javascript 271 | options = { 272 | viewSize: {x: G.WIDTH, y: G.HEIGHT} 273 | }; 274 | ``` 275 | 276 | ![Changed size](images/step_012.png) 277 | 278 | While it is possible to simply just declare this as `options = {viewSize: {x: 100, y: 150}};`, putting this behind one single constant variables will simplify the game tuning process significantly. If you change your mind and want the game to be square again, `G.HEIGHT` is the only one place to edit, instead of running after every single instances of the value `150`. 279 | 280 | We will also explore other properties of `options` along the way. Don't be surprised if you occasionally see strange properties enabled in the step references. 281 | 282 | ### Step 013: Container variable and JSDoc 283 | 284 | Next, we will make something simple, but satisfying and motivating: the stars. Add the following block below `options`: 285 | 286 | ```javascript 287 | /** 288 | * @typedef {{ 289 | * pos: Vector, 290 | * speed: number 291 | * }} Star 292 | */ 293 | 294 | /** 295 | * @type { Star [] } 296 | */ 297 | let stars; 298 | ``` 299 | 300 | If you think those blocks are weird, you are correct that they are not very common sights. Also, the following section is going to be slightly heavy. 301 | 302 | You probably have heard of this very hot thing called **TypeScript** in web development. They fix a major problem in Javascript, which is in its name itself: **typing**. By pre-defining sets of object properties as types, it is much easier to debug and get a program to work as intended. We are definitely not writing TypeScript, but **JSDoc** provides us with a very similar advantage. 303 | 304 | While the two blocks of comments above do absolutely nothing while the game is running, they help you in getting the game to run correctly. Here the type `Star` is defined as object with two property: `pos` of type `Vector` (which is defined by CrispGameLib) and `speed` of type `number`. Let's say for some reason, you make a mistake and assigned a `string` value to `star.speed` like `star1.speed = "tsk tsk"`, VSCode will highlight this mistake and yell at you, preventing you from running that mistake and wasting your time and effort on needless debugging. 305 | 306 | Similarly, `stars` is declared as an array of objects of type `Star`. 307 | 308 | You can even write this in a more verbose and descriptive manner if you choose to: 309 | 310 | ```javascript 311 | /** 312 | * @typedef { object } Star - A decorative floating object in the background 313 | * @property { Vector } pos - The current position of the object 314 | * @property { number } speed - The downwards floating speed of this object 315 | */ 316 | ``` 317 | If these feel weird, simply think of them as **class declaration**, a very common concept in programming. You probably will find them a hassle at first, but as far as my experience go, this is probably the most life-changing and quality-of-life improving thing I have found while writing Javascript. 318 | 319 | If you personally find them unnecessary, it is understandable and the opinion has merit in the context of these small games. Feel free to omit them from your codes and proceed, though I personally don't recommend it unless you know very well what you are doing. 320 | 321 | **Further reading**: [JSDoc](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html). 322 | 323 | ### Step 014: The initialising block 324 | 325 | ---- 326 | **Under the hood**: Like most game engines, CrispGameLib has an `update()` loop, running 60 times per second. The framerate is fixed and not changeable. This also mean that games made with CrispGameLib are entirely frame-rate dependent, omitting the need to handle `deltaTime` and instead, working with number of frames directly (if you have ever used Pico-8, you'd get the idea). You also get access to `ticks`, which provides you with the number of frames the game has passed. 327 | 328 | ---- 329 | 330 | In `update()`, you will see a block of `if (!ticks) {}` already written. In a nutshell, this is the equivalent to `init()`, the function that will run at the start of the game. 331 | 332 | Here, we'd like to initialise the variable `stars` we declared: 333 | ```javascript 334 | // The game loop function 335 | function update() { 336 | // The init function running at startup 337 | if (!ticks) { 338 | // A CrispGameLib function 339 | // First argument (number): number of times to run the second argument 340 | // Second argument (function): a function that returns an object. This 341 | // object is then added to an array. This array will eventually be 342 | // returned as output of the times() function. 343 | stars = times(20, () => { 344 | // Random number generator function 345 | // rnd( min, max ) 346 | const posX = rnd(0, G.WIDTH); 347 | const posY = rnd(0, G.HEIGHT); 348 | // An object of type Star with appropriate properties 349 | return { 350 | // Creates a Vector 351 | pos: vec(posX, posY), 352 | // More RNG 353 | speed: rnd(0.5, 1.0) 354 | }; 355 | }); 356 | } 357 | } 358 | ``` 359 | 360 | There is quite a lot to be unpacked here, so take it slow. There are four things to take note of: 361 | * The function `vec(x, y)` to create a `Vector` object. This is defined by CrispGameLib. 362 | * The random number generator `rnd (min, max)` (you should also be aware of its variant that returns a rounded integer `rndi (min, max)`). Here it is used to generate a random position within the screen. 363 | * I declared the temporary variables `posX` and `posY` as `const`, but did not capitalise them, because they are [scoped local variables](https://www.w3schools.com/js/js_scope.asp), in constrast to the global constant variable `G`. 364 | * The CrispGameLib built-in function `times( number, func())`. This might sound a bit confusing, but it is actually just a short hand for a `for loop`. **Alternatively**, the block can practically be re-written as: 365 | ```javascript 366 | function update() { 367 | if (!ticks) { 368 | for (let i = 0; i < 20; i++) { 369 | stars.push({ 370 | pos: vec(rnd(0, G.WIDTH), rnd(0, G.HEIGHT)), 371 | speed: rnd(0.5, 1.0) 372 | }); 373 | } 374 | } 375 | } 376 | ``` 377 | 378 | Also, this is also a chance for a refactor and add more game design variables to `G`. We'd be doing this a lot from now on, so keep track of your object `G`: 379 | ```javascript 380 | const G = { 381 | STAR_SPEED_MIN: 0.5, 382 | STAR_SPEED_MAX: 1.0 383 | } 384 | ``` 385 | ```javascript 386 | return { 387 | pos: vec(posX, posY), 388 | speed: rnd(G.STAR_SPEED_MIN, G.STAR_SPEED_MAX) 389 | }; 390 | ``` 391 | 392 | However, this has no visible effect on the game yet. 393 | 394 | ### Step 015: The update loop 395 | 396 | We'll now be drawing the stars on screen. Add this block inside the `update()` block, just below the `if (!ticks) {}`: 397 | 398 | ```javascript 399 | // Update for Star 400 | stars.forEach((s) => { 401 | // Move the star downwards 402 | s.pos.y += s.speed; 403 | // Bring the star back to top once it's past the bottom of the screen 404 | s.pos.wrap(0, G.WIDTH, 0, G.HEIGHT); 405 | 406 | // Choose a color to draw 407 | color("light_black"); 408 | // Draw the star as a square of size 1 409 | box(s.pos, 1); 410 | }); 411 | ``` 412 | 413 | This block should look a lot less foreign, if you have ever seen videogame codes: 414 | * The method `Array.forEach()` iterates and execute on each element in the array. In this case, each `star` is updated 60 times a second. **Further reading**: [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach). 415 | * `s.pos.y += s.speed` adds the y coordinate of the star by `speed` (which we randomly generated when we created the stars), bringing the star perpetually downwards to the bottom of the screen (unlike conventional high school math, the y-axis points downwards). 416 | * `wrap(minX, maxX, minY, maxY)` is a method for `Vector`, which wrap the object back to the otherside, when the object is outside of the screen (which is specified by the screen coordinates as the four arguments). The handling of the x coordinate here is redundant as it never changes. **Alternatively,** this can be re-written more effectively as `if (s.pos.y > G.HEIGHT) s.pos.y = 0;` 417 | * The color is set before the star is drawn with `color()` (`light_black` sounds a bit wacky, but it does make sense when you look at the list of colors). Here, the `box()` is chosen to represent the star, taking the star's coordinate as an argument. **Further reading**: [the drawing example demo in CrispGameLib](https://abagames.github.io/crisp-game-lib-games/?ref_drawing). Take note of the alternative use of `x` and `y` arguments as coordinates in opposed to a `Vector`. 418 | 419 | ![Moving stars](images/step_015.gif) 420 | 421 | Pretty cool, yeah? 422 | 423 | ---- 424 | **For your experimentation**: Try changing: 425 | * The value of `G.STAR_SPEED_MIN` and `G.STAR_SPEED_MAX` and see how things change. Feel free to stay on a different set of values. 426 | * The color of the stars (the list of colors can again be found in the [documentation][cgl-url]). 427 | * The size of the star as the second argument of `box()`. 428 | 429 | ---- 430 | 431 | Step 01 conclusion: [deployment](https://junongx.github.io/crisp-game-lib-tutorial/?step_01) / [code](https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/master/docs/step_01/main.js). 432 | 433 | ## Step 02: Input and control (player) 434 | 435 | Here we will start handling the player entity. 436 | 437 | ### Step 021: Another type 438 | 439 | First, let's get started with more type and variable declaring. This is not unlike what we did previously: 440 | ```javascript 441 | /** 442 | * @typedef {{ 443 | * pos: Vector, 444 | * }} Player 445 | */ 446 | 447 | /** 448 | * @type { Player } 449 | */ 450 | let player; 451 | ``` 452 | Unlike `stars`, `player` is in singular form, holding a single object instance of type `Player`. If you are feeling confused, do check out step 013 again. 453 | 454 | We can also initialise the `player` object in the initialisation block (this is right below `stars`): 455 | ```javascript 456 | player = { 457 | pos: vec(G.WIDTH * 0.5, G.HEIGHT * 0.5) 458 | }; 459 | ``` 460 | Do take note of the use of game size variables `G.WIDTH` and `G.HEIGHT`, divided by half, to access the mid-position of the screen. We can now also start drawing the player: 461 | 462 | ```javascript 463 | color("cyan"); 464 | box(player.pos, 4); 465 | ``` 466 | 467 | ![Basic player](images/step_021.gif) 468 | 469 | ### Step 022: Input handling 470 | 471 | This is, however, still not interactive. We will fix this by handling `input`. By conventional standard, an entity's updates occur before drawing, so put this line before the drawing codes above. 472 | 473 | ```javascript 474 | player.pos = vec(input.pos.x, input.pos.y); 475 | ``` 476 | 477 | ![Moving player](images/step_022.gif) 478 | 479 | Nice. The player now follows your mouse pointer. 480 | 481 | ---- 482 | **Further reading**: [The input example from the documention](https://abagames.github.io/crisp-game-lib-games/?ref_input). Besides the coordinate of the pointer, you also get access to three booleans `isPressed`, `isJustPressed`, `isJustReleased`, representing the three states of the button. While these will not be used in this tutorial, they are important. You can also do interesting and complicated input techniques with this, such as double tap/click, long press, or swiping. 483 | 484 | ---- 485 | 486 | However, we have one problem: the player occasionally moves out of the game screen, which is not ideal. We need to keep the player strictly within the screen at all times: 487 | 488 | ```javascript 489 | player.pos.clamp(0, G.WIDTH, 0, G.HEIGHT); 490 | ``` 491 | 492 | ![Clamped player's position](images/step_022b.gif) 493 | 494 | You will notice that the interface and signature of `Vector.clamp(minX, maxX, minY, maxY)` is very similar to `wrap()`, though it does something else. 495 | 496 | ### Step 023: Custom sprite 497 | 498 | A square, however, is not very appealing or interesting. This is where I show you how to use custom sprite characters. 499 | 500 | Just below the `description` declaration, notice that there is an empty array `character = [];`. Time to use it. Try populate it with something. Do note the use of backticks for **template literal** and how there was no indentation. VSCode is going to automatically insert indentations among other things, so make sure you paste in correctly and manually fix any incorrect whitespaces: 501 | 502 | ```javascript 503 | characters = [ 504 | ` 505 | ll 506 | ll 507 | ccllcc 508 | ccllcc 509 | ccllcc 510 | cc cc 511 | ` 512 | ]; 513 | ``` 514 | 515 | Now, replace the drawing line with another function to use this character: 516 | ```javascript 517 | color("cyan"); 518 | // box(player.pos, 4); 519 | char("a", player.pos); 520 | ``` 521 | 522 | ![New sprite](images/step_023.gif) 523 | 524 | Notice that the shape has been changed to the new array element we have just populated with, though the color remains the same. Now try something else by changing the color to `black`: 525 | 526 | ```javascript 527 | // color("cyan"); 528 | color ("black"); 529 | // box(player.pos, 4); 530 | char("a", player.pos); 531 | ``` 532 | 533 | ![Original color](images/step_023b.gif) 534 | 535 | Interesting, eh? 536 | 537 | In order to explain this weird phenomenon you've just witnessed, I need to show you an excerpt of the documentation, regarding the color list: 538 | 539 | ```javascript 540 | // Define pixel arts of characters. 541 | // Each letter represents a pixel color. 542 | // (l: black, r: red, g: green, b: blue 543 | // y: yellow, p: purple, c: cyan 544 | // L: light_black, R: light_red, G: light_green, B: light_blue 545 | // Y: light_yellow, P: light_purple, C: light_cyan) 546 | // Characters are assigned from 'a'. 547 | // 'char("a", 0, 0);' draws the character 548 | // defined by the first element of the array. 549 | ``` 550 | And look at this again: 551 | 552 | ```javascript 553 | characters = [ 554 | ` 555 | ll 556 | ll 557 | ccllcc 558 | ccllcc 559 | ccllcc 560 | cc cc 561 | ` 562 | ]; 563 | ``` 564 | 565 | Notice that the `l` and `c` are actually short forms of the color `black` and `cyan`. By changing these characters to other valid characters that also represent colors, you would change the color of some pixels in this sprite. The excerpt also explains the function `char()`, in which `a` is represented by the first element in the array `characters`. Also, by setting the drawing color to `color("black")`, the engine will draw the sprite with the originally colors, instead of an overlay. 566 | 567 | ---- 568 | **For your experimentation**: Using the available colors, make your own sprite that represents the player's ship by modifying the first element in `characters`. Do notice that you are limited only to the size 6x6. 569 | 570 | 571 | **CrispGameLib quirk**: At this point, you should also notice that the sprite is drawn at the middle of your cursor position. This is a slightly deviation from the norm in other game engine, in which the drawing origin is usually at the top left corner. In CrispGameLib, the drawing origin is in the middle. 572 | 573 | ---- 574 | 575 | Step 02 conclusion: [deployment](https://junongx.github.io/crisp-game-lib-tutorial/?step_02) / [code](https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/master/docs/step_02/main.js). 576 | 577 | ## Step 03: Object control, creation, and removal (fBullets) 578 | 579 | We are finally going to fire our gun! 580 | ### Step 031: Firing bullets 581 | 582 | First thing first, more stuff to declare. You should be quite familiar with this by now: 583 | 584 | ```javascript 585 | const G = { 586 | WIDTH: 100, 587 | HEIGHT: 150, 588 | 589 | STAR_SPEED_MIN: 0.5, 590 | STAR_SPEED_MAX: 1.0, 591 | 592 | PLAYER_FIRE_RATE: 4, 593 | PLAYER_GUN_OFFSET: 3, 594 | 595 | FBULLET_SPEED: 5 596 | }; 597 | ``` 598 | 599 | ```javascript 600 | /** 601 | * @typedef {{ 602 | * pos: Vector, 603 | * firingCooldown: number, 604 | * isFiringLeft: boolean 605 | * }} Player 606 | */ 607 | 608 | /** 609 | * @type { Player } 610 | */ 611 | let player; 612 | 613 | /** 614 | * @typedef {{ 615 | * pos: Vector 616 | * }} FBullet 617 | */ 618 | 619 | /** 620 | * @type { FBullet [] } 621 | */ 622 | let fBullets; 623 | ``` 624 | 625 | Do take note of the new properties added type `Player`: `firingCooldown` and `isFiringLeft`. If you have done your JSDoc properly, you would also notice that VSCode will start yelling at you, telling you that the `player` instance you initialised is incorrect and missing some properties (which is exactly what we expected). Other than fixing this, you should also start initalise `fBullets`, which is a short form of *friendly bullets*, to differentiate against *enemy bullets* we'd have later on. 626 | 627 | ```javascript 628 | player = { 629 | pos: vec(G.WIDTH * 0.5, G.HEIGHT * 0.5), 630 | firingCooldown: G.PLAYER_FIRE_RATE, 631 | isFiringLeft: true 632 | }; 633 | 634 | fBullets = []; 635 | ``` 636 | 637 | Next up, we update them: 638 | ```javascript 639 | // Updating and drawing the player 640 | player.pos = vec(input.pos.x, input.pos.y); 641 | player.pos.clamp(0, G.WIDTH, 0, G.HEIGHT); 642 | // Cooling down for the next shot 643 | player.firingCooldown--; 644 | // Time to fire the next shot 645 | if (player.firingCooldown <= 0) { 646 | // Create the bullet 647 | fBullets.push({ 648 | pos: vec(player.pos.x, player.pos.y) 649 | }); 650 | // Reset the firing cooldown 651 | player.firingCooldown = G.PLAYER_FIRE_RATE; 652 | } 653 | color ("black"); 654 | char("a", player.pos); 655 | 656 | // Updating and drawing bullets 657 | fBullets.forEach((fb) => { 658 | // Move the bullets upwards 659 | fb.pos.y -= G.FBULLET_SPEED; 660 | 661 | // Drawing 662 | color("yellow"); 663 | box(fb.pos, 2); 664 | }); 665 | ``` 666 | 667 | 668 | If you have played videogames before, you probably have heard of the concept "cooldown", with which, you'd need to wait for an interval time before you can use a powerful ability again. Though a machine gun is much faster, concept is similar, with a much shorter cooldown time, giving the feeling of bullets being constantly fired. 669 | 670 | Here, the cooldown is set `firingCooldown: G.PLAYER_FIRE_RATE` in the initialisation; and in the update loop, it is perpetually reduced `player.firingCooldown--;` (this is a shorthard for `player.firingCooldown = player.firingCooldown - 1;` in case you are unfamiliar). By the time the cooldown is completed `(player.firingCooldown <= 0)`, a bullet is created, it is set back to the intial value of `G.PLAYER_FIRE_RATE`, and the process repeats. At the fire rate of `5` (frames), the ship is now firing 12 rounds per second. 671 | 672 | In the next block, `fBullets` iterates over its elements and perform the update and drawing on each of them not unlike `stars`. 673 | 674 | ![Firing gun](images/step_031.gif) 675 | 676 | ### Step 032: Object management and removal 677 | 678 | If you let the game run in the current state for a while, you will notice that it eventually slows down. This is because it is performing updates on hundreds, if not thousands of instances of bullets, which has gone out of screen and is heading towards infinity upwards. Try adding this, which will display the number of bullets existing in the game world: 679 | 680 | ```javascript 681 | text(fBullets.length.toString(), 3, 10); 682 | ``` 683 | 684 | ![Lots of bullets](images/step_032.gif) 685 | 686 | This is quite horrendous. Of course those bullets are no longer relevant and we have to do something about them. 687 | 688 | ```javascript 689 | remove(fBullets, (fb) => { 690 | return fb.pos.y < 0; 691 | }); 692 | ``` 693 | 694 | This is another weird looking function unique to CrispGameLib. Like `forEach()`, it iterates over elements in array, but then, it also checks for conditions to remove them from the container. Think of it as a more intuitive and reversed version of the Javascript's native `Array.filter()`. It directly works on the array in the first parameters, and the elements that yield a `return true` in the second parameter (a function) are removed. 695 | 696 | In this case, a bullet out of screen is an irrelevant bullet, hence `fb.pos.y < 0`. Since our bullets only move in one direction, there is only one landmark to check against (the top of the screen). You can also use this function to update a group of objects, which I will show you later. 697 | 698 | But for now, the important thing is, there is only a few bullets on screen at a time, and the game is now much more resource-efficient. 699 | 700 | ![Few bullets](images/step_032b.gif) 701 | 702 | ### Step 033: Dual barrels 703 | 704 | If you have played the original game, you'd notice that this is not quite accurate, the ship is supposed to have two barrels, and bullets come out of them alternatively. We have actually already taken care part of that with `G.PLAYER_GUN_OFFSET` and `Player.isFiringLeft`. Now let's change the firing process: 705 | 706 | ```javascript 707 | if (player.firingCooldown <= 0) { 708 | // Get the side from which the bullet is fired 709 | const offset = (player.isFiringLeft) 710 | ? -G.PLAYER_GUN_OFFSET 711 | : G.PLAYER_GUN_OFFSET; 712 | // Create the bullet 713 | fBullets.push({ 714 | pos: vec(player.pos.x + offset, player.pos.y) 715 | }); 716 | // Reset the firing cooldown 717 | player.firingCooldown = G.PLAYER_FIRE_RATE; 718 | // Switch the side of the firing gun by flipping the boolean value 719 | player.isFiringLeft = !player.isFiringLeft; 720 | } 721 | ``` 722 | 723 | ---- 724 | **Javascript feature** and **further reading**: [Conditional (ternary) operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator). The line `const offset = (player.isFiringLeft) ? -G.PLAYER_GUN_OFFSET : G.PLAYER_GUN_OFFSET;` is the short form of a conditional check: 725 | ```javascript 726 | let offset; 727 | if (player.isFiringLeft) { 728 | offset = -G.PLAYER_GUN_OFFSET; 729 | } else { 730 | offset = G.PLAYER_GUN_OFFSET; 731 | } 732 | ``` 733 | Do get yourself familiar with it, this is a very useful shorthand. 734 | 735 | ---- 736 | 737 | ![Dual barrel](images/step_033.gif) 738 | 739 | You may also now comment out the number of bullets lines. 740 | 741 | ### Step 034: Muzzleflash and particles 742 | 743 | There is, however, one more thing I'd like to go over before before we're done with firing guns: we're going to put in some exiciting muzzleflash, which we will use **particles** to represent. 744 | 745 | ```javascript 746 | if (player.firingCooldown <= 0) { 747 | // Get the side from which the bullet is fired 748 | const offset = (player.isFiringLeft) 749 | ? -G.PLAYER_GUN_OFFSET 750 | : G.PLAYER_GUN_OFFSET; 751 | // Create the bullet 752 | fBullets.push({ 753 | pos: vec(player.pos.x + offset, player.pos.y) 754 | }); 755 | // Reset the firing cooldown 756 | player.firingCooldown = G.PLAYER_FIRE_RATE; 757 | // Switch the side of the firing gun by flipping the boolean value 758 | player.isFiringLeft = !player.isFiringLeft; 759 | 760 | color("yellow"); 761 | // Generate particles 762 | particle( 763 | player.pos.x + offset, // x coordinate 764 | player.pos.y, // y coordinate 765 | 4, // The number of particles 766 | 1, // The speed of the particles 767 | -PI/2, // The emitting angle 768 | PI/4 // The emitting width 769 | ); 770 | } 771 | ``` 772 | 773 | ![Particles](images/step_034.gif) 774 | 775 | **Further reading**: In order to best understand, here's a relevant excerpt from GameCrispLib documentation: 776 | 777 | ```javascript 778 | function particle( 779 | x: number, 780 | y: number, 781 | count?: number, 782 | speed?: number, 783 | angle?: number, 784 | angleWidth?: number 785 | ); 786 | function particle( 787 | pos: VectorLike, 788 | count?: number, 789 | speed?: number, 790 | angle?: number, 791 | angleWidth?: number 792 | ); 793 | ``` 794 | Do take note of my use of `PI` to achieve a 90 degree angle, and the alternative use of `Vector` instead of separated `x` and `y` coordinates, a frequent recurring motif in GameCrispLib API. 795 | 796 | Step 03 conclusion: [deployment](https://junongx.github.io/crisp-game-lib-tutorial/?step_03) / [code](https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/master/docs/step_03/main.js). 797 | 798 | ## Step 04: Mechanic control (enemies) 799 | 800 | Let's get some enemies in. 801 | 802 | ### Step 041: The formation 803 | 804 | Before we start doing anything, we need to take a look at what we're going to do. [Here's the original game again][cro], if you need a refresher. 805 | 806 | While it's not exactly obvious, but the enemies are spawned in a very particular way: 807 | * Enemies are evenly spreaded vertically. 808 | * Enemies' horizontal positions are randomized. 809 | * All enemies in the same wave have the same speed. 810 | * Enemies' score value is increased by 10 per wave. 811 | 812 | (And one reason I am certain about all of that, is because I looked at the [source code](http://abagames.sakura.ne.jp/html5/cr/main.coffee) 😎). 813 | 814 | ### Step 042: Processing the Enemy 815 | 816 | To proceed, let's declare some types: 817 | 818 | ```javascript 819 | /** 820 | * @typedef {{ 821 | * pos: Vector 822 | * }} Enemy 823 | */ 824 | 825 | /** 826 | * @type { Enemy [] } 827 | */ 828 | let enemies; 829 | 830 | /** 831 | * @type { number } 832 | */ 833 | let currentEnemySpeed; 834 | 835 | /** 836 | * @type { number } 837 | */ 838 | let waveCount; 839 | ``` 840 | 841 | Type `Enemy` apparently should have their own independent position. However, you'd notice that I have a separate variable `currentEnemySpeed`, which is because enemies that appear onscreen at the same time all have the same speed, so it would be slightly unoptimal to store the same value multiple times. In the grand scheme of the processing resources available, the cost of these variables are tiny, but this is to give you an idea and a taste of optimisation. 842 | 843 | To proceed, let's get out the rest of what we need: 844 | 845 | ```javascript 846 | 847 | // New sprite 848 | characters = [ 849 | ` 850 | ll 851 | ll 852 | ccllcc 853 | ccllcc 854 | ccllcc 855 | cc cc 856 | `,` 857 | rr rr 858 | rrrrrr 859 | rrpprr 860 | rrrrrr 861 | rr 862 | rr 863 | `, 864 | ]; 865 | 866 | // New game design variables 867 | const G = { 868 | ENEMY_MIN_BASE_SPEED: 1.0, 869 | ENEMY_MAX_BASE_SPEED: 2.0 870 | }; 871 | 872 | // Initalise the values: 873 | enemies = []; 874 | 875 | waveCount = 0; 876 | currentEnemySpeed = 0; 877 | 878 | // Another update loop 879 | // This time, with remove() 880 | remove(enemies, (e) => { 881 | e.pos.y += currentEnemySpeed; 882 | color("black"); 883 | char("b", e.pos); 884 | 885 | return (e.pos.y > G.HEIGHT); 886 | }); 887 | ``` 888 | 889 | However, we are not seeing anything because we haven't spawned them. 890 | 891 | ### Step 043: Spawning 892 | 893 | We'd spawn them the simple way: as long as there is no enemy around. Add this block before processing the `stars` and right after the initialisation: 894 | 895 | ```javascript 896 | if (enemies.length === 0) { 897 | currentEnemySpeed = 898 | rnd(G.ENEMY_MIN_BASE_SPEED, G.ENEMY_MAX_BASE_SPEED) * difficulty; 899 | for (let i = 0; i < 9; i++) { 900 | const posX = rnd(0, G.WIDTH); 901 | const posY = -rnd(i * G.HEIGHT * 0.1); 902 | enemies.push({ pos: vec(posX, posY) }) 903 | } 904 | } 905 | ``` 906 | 907 | Things to note: 908 | * **CrispGameLib feature**: there is a built-in variable called `difficulty`, which starts from `1`, and is progressively increased by `1` for every minute passed, slowly and gradually. If you'd like to see this for yourself, try printing this either onscreen (`text(difficulty.toString(), 3, 10);`) or to the web browser console (`console.log(difficulty);`). This variable here is used to modify the enemy speed, which will make the game more difficulty as time passes and the value of `difficulty` increases. 909 | * I'm not using `times()` here because we need to access the looping variable `i`, hence this is an old-fashioned standard `for loop`. 910 | 911 | ![Spawning enemies](images/step_043.gif) 912 | 913 | The game now looks much more complete. 914 | 915 | Step 04 conclusion: [deployment](https://junongx.github.io/crisp-game-lib-tutorial/?step_04) / [code](https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/master/docs/step_04/main.js). 916 | 917 | ## Step 05: Collision detection 918 | 919 | In CrispGameLib, objects' graphic also serve as their hitbox. Everytime a sprite is drawn, regardless with `char()`, `box()`, or `text()`, each and everyone of them is keeping track of which other sprites it is colliding with in the property `isColliding`. **Further reading**: [Collision example demo](https://abagames.github.io/crisp-game-lib-games/?ref_collision). For this reason, strategic thinking about collision should always be planned, such as objects of different types, should have at least different types and different colors, if their collision is to have an effect on the game. 920 | 921 | ### Step 051: Destroying enemies 922 | 923 | Now, let us make enemies destroyable by friendly bullets: 924 | ```javascript 925 | remove(enemies, (e) => { 926 | e.pos.y += currentEnemySpeed; 927 | color("black"); 928 | // Shorthand to check for collision against another specific type 929 | // Also draw the sprite 930 | const isCollidingWithFBullets = char("b", e.pos).isColliding.rect.yellow; 931 | 932 | // Check whether to make a small particle explosin at the position 933 | if (isCollidingWithFBullets) { 934 | color("yellow"); 935 | particle(e.pos); 936 | } 937 | 938 | // Also another condition to remove the object 939 | return (isCollidingWithFBullets || e.pos.y > G.HEIGHT); 940 | }); 941 | ``` 942 | 943 | ![Destroying enemies](images/step_051.gif) 944 | 945 | Here, the `boolean` variable `isCollidingWithFBullets` is used as a shorthand referral to check whether a sprite character of type `b` is colliding with any of the yellow rectangles (which are representing a friendly bullet). This initialisation also causes the `char` `b` to be drawn onscreen, even when it's not explicitly used for such purpose. `isCollidingWithFBullets` is then used to check whether there should be a small *particle* explosion at the location of the `Enemy` object, and whether this `Enemy` object should be removed from the container. 946 | 947 | ### Step 052: Two-way interaction 948 | 949 | While we have implemented a simple of form collision detection, having a two-way collision, in which both the bullet and the target are destroyed, is a slightly more complicated matter. 950 | 951 | Let us try: 952 | 953 | ```javascript 954 | remove(fBullets, (fb) => { 955 | const isCollidingWithEnemies = box(fb.pos, 2).isColliding.char.b; 956 | return (isCollidingWithEnemies || fb.pos.y < 0); 957 | }); 958 | ``` 959 | 960 | While this syntatically and logically correct, you will notice this does not work. The enemies are destroyed, but not friendly bullets. The question is why? 961 | 962 | Consider this: everything we have written happened in one single frame, which occurs 60 times in a second. By examining the location of `remove(fBullets, (fb) => {});` in the chronological sequence of an `update()`, you will noticed that when a `fBullet` attempts to detect another `char.b`, no `char.b` has yet been drawn in that frame. 963 | 964 | The solution: let `fBullets` react to a collision only after `Enemies` have been drawn: 965 | 966 | ```javascript 967 | // Updating and drawing bullets 968 | fBullets.forEach((fb) => { 969 | fb.pos.y -= G.FBULLET_SPEED; 970 | 971 | // Drawing fBullets for the first time, allowing interaction from enemies 972 | color("yellow"); 973 | box(fb.pos, 2); 974 | }); 975 | 976 | remove(enemies, (e) => { 977 | e.pos.y += currentEnemySpeed; 978 | color("black"); 979 | // Interaction from enemies to fBullets 980 | // Shorthand to check for collision against another specific type 981 | // Also draw the sprits 982 | const isCollidingWithFBullets = char("b", e.pos).isColliding.rect.yellow; 983 | 984 | if (isCollidingWithFBullets) { 985 | color("yellow"); 986 | particle(e.pos); 987 | } 988 | 989 | // Also another condition to remove the object 990 | return (isCollidingWithFBullets || e.pos.y > G.HEIGHT); 991 | }); 992 | 993 | remove(fBullets, (fb) => { 994 | // Interaction from fBullets to enemies, after enemies have been drawn 995 | color("yellow"); 996 | const isCollidingWithEnemies = box(fb.pos, 2).isColliding.char.b; 997 | return (isCollidingWithEnemies || fb.pos.y < 0); 998 | }); 999 | 1000 | ``` 1001 | 1002 | ![Two-way collision](images/step_052.gif) 1003 | 1004 | You will also notice that `fBullets` are drawn twice: the first time to allow themselves to be interacted with from `enemies`, and the second time, to interact with `enemies` from themselves. 1005 | 1006 | This is a **CrispGameLib quirk**, while this does sound mind-boggling at first, it is not as complicated as it looks. The takeaway is: always make sure that the two colliding sprites are already drawn, which means occasionally drawing some of them more than once. 1007 | 1008 | Step 05 conclusion: [deployment](https://junongx.github.io/crisp-game-lib-tutorial/?step_05) / [code](https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/master/docs/step_05/main.js). 1009 | ## Step 06: How audio works 1010 | 1011 | ### Step 061: The basic way 1012 | 1013 | And here's my favourite part of CrispGameLib. Let me just straight away show you an excerpt of the documentation ([not forgetting an example demo](https://abagames.github.io/crisp-game-lib-games/?ref_sound)): 1014 | ```javascript 1015 | function update() { 1016 | // Plays a sound effect. 1017 | // play(type: "coin" | "laser" | "explosion" | "powerUp" | 1018 | // "hit" | "jump" | "select" | "lucky"); 1019 | play("coin"); 1020 | } 1021 | ``` 1022 | It certainly doesn't look difficult. Let's add our own explosion sound: 1023 | 1024 | ```javascript 1025 | remove(enemies, (e) => { 1026 | e.pos.y += currentEnemySpeed; 1027 | color("black"); 1028 | const isCollidingWithFBullets = char("b", e.pos).isColliding.rect.yellow; 1029 | 1030 | if (isCollidingWithFBullets) { 1031 | color("yellow"); 1032 | particle(e.pos); 1033 | play("explosion"); // Here! 1034 | } 1035 | 1036 | return (isCollidingWithFBullets || e.pos.y > G.HEIGHT); 1037 | }); 1038 | ``` 1039 | 1040 | This is not something I can show you in gif, but if you did it right, you're having an explosion for every destroyed enemy. 1041 | ### Step 062: Infinite sound 1042 | 1043 | It gets even better. Let's add this to `options`. 1044 | 1045 | ```javascript 1046 | options = { 1047 | seed: 2 1048 | } 1049 | ``` 1050 | 1051 | It might not be obvious. But you are listening to a different explosion sound. 1052 | 1053 | To make it even more obvious, add `isPlayingBgm` to enable music: 1054 | 1055 | ```javascript 1056 | options = { 1057 | seed: 2, 1058 | isPlayingBgm: true 1059 | } 1060 | ``` 1061 | 1062 | It gets even crazier; let's add a game description: 1063 | ```javascript 1064 | description = ` 1065 | Destroy enemies. 1066 | `; 1067 | ``` 1068 | 1069 | The bottom line is, CrispGameLib uses a combination of your assigned random `seed` and the content of your `description` to generate a particular sets of audio for your game. This means you are putting in minimum work while still achieving a relatively unique audio experience for each of your games. 1070 | 1071 | Of course, without saying, it comes with a major downside. It means that you have pretty much almost no control at all over audio, and if you are looking to fine tune every single piece of audio, CrispGameLib can't give you that without some major modification to the engine. 1072 | 1073 | Step 06 conclusion: [deployment](https://junongx.github.io/crisp-game-lib-tutorial/?step_06) / [code](https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/master/docs/step_06/main.js). 1074 | 1075 | ## Step 07: More complex movements (eBullets) 1076 | 1077 | The game is finally taking shape. We are just a few steps away from completing this. 1078 | 1079 | ### Step 071: Enemy bullets 1080 | 1081 | We are now adding the final object type: enemy bullets, which means more type declaration and adding more properties to existing types. 1082 | 1083 | ```javascript 1084 | // New property: firingCooldown 1085 | /** 1086 | * @typedef {{ 1087 | * pos: Vector, 1088 | * firingCooldown: 1089 | * }} Enemy 1090 | */ 1091 | 1092 | // New type 1093 | /** 1094 | * @typedef {{ 1095 | * pos: Vector, 1096 | * angle: number, 1097 | * rotation: number 1098 | * }} EBullet 1099 | */ 1100 | 1101 | /** 1102 | * @type { EBullet [] } 1103 | */ 1104 | let eBullets; 1105 | 1106 | ``` 1107 | 1108 | More sprites: 1109 | ```javascript 1110 | characters = [ 1111 | ` 1112 | ll 1113 | ll 1114 | ccllcc 1115 | ccllcc 1116 | ccllcc 1117 | cc cc 1118 | `,` 1119 | rr rr 1120 | rrrrrr 1121 | rrpprr 1122 | rrrrrr 1123 | rr 1124 | rr 1125 | `,` 1126 | y y 1127 | yyyyyy 1128 | y y 1129 | yyyyyy 1130 | y y 1131 | ` 1132 | ]; 1133 | ``` 1134 | 1135 | More gameplay variables: 1136 | ```javascript 1137 | const G = { 1138 | WIDTH: 100, 1139 | HEIGHT: 150, 1140 | 1141 | STAR_SPEED_MIN: 0.5, 1142 | STAR_SPEED_MAX: 1.0, 1143 | 1144 | PLAYER_FIRE_RATE: 4, 1145 | PLAYER_GUN_OFFSET: 3, 1146 | 1147 | FBULLET_SPEED: 5, 1148 | 1149 | ENEMY_MIN_BASE_SPEED: 1.0, 1150 | ENEMY_MAX_BASE_SPEED: 2.0, 1151 | ENEMY_FIRE_RATE: 45, 1152 | 1153 | EBULLET_SPEED: 2.0, 1154 | EBULLET_ROTATION_SPD: 0.1 1155 | }; 1156 | ``` 1157 | 1158 | Don't forget the initialise and fix whatever VSCode is yelling at you, too. 1159 | 1160 | The attacking mechanism of `enemies` isn't unlike `player`'s, as `firingCooldown` decreases towards zero, fires a bullet, and resets again: 1161 | 1162 | ```javascript 1163 | remove(enemies, (e) => { 1164 | e.pos.y += currentEnemySpeed; 1165 | e.firingCooldown--; 1166 | if (e.firingCooldown <= 0) { 1167 | eBullets.push({ 1168 | pos: vec(e.pos.x, e.pos.y), 1169 | angle: e.pos.angleTo(player.pos), 1170 | rotation: rnd() 1171 | }); 1172 | e.firingCooldown = G.ENEMY_FIRE_RATE; 1173 | play("select"); // Be creative, you don't always have to follow the label 1174 | } 1175 | 1176 | color("black"); 1177 | const isCollidingWithFBullets = char("b", e.pos).isColliding.rect.yellow; 1178 | if (isCollidingWithFBullets) { 1179 | color("yellow"); 1180 | particle(e.pos); 1181 | play("explosion"); 1182 | } 1183 | 1184 | return (isCollidingWithFBullets || e.pos.y > G.HEIGHT); 1185 | }); 1186 | ``` 1187 | 1188 | Take note of the utility method `Vector.angleTo(destinationVector)`. **Alternatively**, you can do it the old-fashioned way with trigonometry: `const angle = Math.atan2(player.pos.y - e.pos.y, player.pos.x - e.pos.x);` 1189 | 1190 | Also, update `eBullets` and handle the collision with `player`. 1191 | 1192 | ```javascript 1193 | remove(eBullets, (eb) => { 1194 | // Old-fashioned trigonometry to find out the velocity on each axis 1195 | eb.pos.x += G.EBULLET_SPEED * Math.cos(eb.angle); 1196 | eb.pos.y += G.EBULLET_SPEED * Math.sin(eb.angle); 1197 | // The bullet also rotates around itself 1198 | eb.rotation += G.EBULLET_ROTATION_SPD; 1199 | 1200 | color("red"); 1201 | const isCollidingWithPlayer 1202 | = char("c", eb.pos, {rotation: eb.rotation}).isColliding.char.a; 1203 | 1204 | if (isCollidingWithPlayer) { 1205 | // End the game 1206 | end(); 1207 | // Sarcasm; also, unintedned audio that sounds good in actual gameplay 1208 | play("powerUp"); 1209 | } 1210 | 1211 | // If eBullet is not onscreen, remove it 1212 | return (!eb.pos.isInRect(0, 0, G.WIDTH, G.HEIGHT)); 1213 | }); 1214 | ``` 1215 | 1216 | While this looks like quite a bit to comprehend, most of these are no longer new at this point: 1217 | * Do take note of the third argument for `char()`, which takes in an object. The property `rotation` here isn't the same as `angle` and works slightly different (a 90 degree rotation is represented by `1`). This property allows the bullet to rotate around itself. 1218 | * The function `end()`, which self-describingly ends the game, automatically puts the game into an ending state and returns to the title screen subsequently. 1219 | * `Vector.isInRect(topLeftX, topLeftY, length, width)`, self-exlanatorily, checks whether the coordinate is within a particular rectangle. Here it is used to detect whether the bullet is within the game screen. 1220 | 1221 | ![Firing enemy bullets](images/step_071.gif) 1222 | 1223 | ---- 1224 | **Alternative implementation**: There is a less nerdy way implement the angled movement for eBullet with built-in utility methods: 1225 | 1226 | ```javascript 1227 | remove(eBullets, (eb) => { 1228 | const velocityVector = vec(G.EBULLET_SPEED, 0).rorateTo(eb.angle); 1229 | eb.pos.add(velocityVector); 1230 | }); 1231 | ``` 1232 | 1233 | The variable `G.EBULLET_SPEED` represent the bullet's speed as a *scalar* (only magnitude, no direction). To represent this as a *vector*, it can be initialised as `vec(G.EBULLET_SPEED, 0)`. This vector has the indicated magnitude, pointing towards the 0 degree direction (visually on-screen, this is towards the right, hence the 0 value for `y`). Next up, `rotateTo(angle)` is a built-in method for the class `Vector`, which breaks down this magnitude appropriately to x and y component of a vector. Finally, we use the method `add()` to calculate the sum of two vectors, as `eb.pos.add(velocityVector)` will become the position of the bullet in the next frame, taking the velocity of this object into account. 1234 | 1235 | You will also find it useful to have a `vel` property in object types that might have varied movement speeds. 1236 | 1237 | This is not a "superior" or "better" way to do it, just a different implementation. Visually and mechanically, there is no difference. In any case, using `math.sin` and `math.cos` is a universal way to implement angled movement in all game engines and contexts. Which method you should use is a mere matter of personal preference. 1238 | 1239 | ---- 1240 | 1241 | At this point, it's fair that `enemies` are also able to destroy the `player`, too. 1242 | 1243 | ```javascript 1244 | const isCollidingWithPlayer = char("b", e.pos).isColliding.char.a; 1245 | if (isCollidingWithPlayer) { 1246 | end(); 1247 | play("powerUp"); 1248 | } 1249 | ``` 1250 | 1251 | ### Step 072: Scoring 1252 | 1253 | Here's the part that makes the player keeps playing and coming back. It is, however, surprisingly simple. 1254 | 1255 | Each destroyed enemy should provide the player with a score of multiplication of 10, based on the `waveCount`. Any contact between `eBullet` and `fBullet` will yield a small amount of scores, too. 1256 | 1257 | First thing first, we need to keep a good track of `waveCount`. 1258 | 1259 | ```javascript 1260 | if (!ticks) { 1261 | waveCount = 0; 1262 | } 1263 | ``` 1264 | ```javascript 1265 | if (enemies.length === 0) { 1266 | currentEnemySpeed = 1267 | rnd(G.ENEMY_MIN_BASE_SPEED, G.ENEMY_MAX_BASE_SPEED) * difficulty; 1268 | for (let i = 0; i < 9; i++) { 1269 | const posX = rnd(0, G.WIDTH); 1270 | const posY = -rnd(i * G.HEIGHT * 0.1); 1271 | enemies.push({ 1272 | pos: vec(posX, posY), 1273 | firingCooldown: G.ENEMY_FIRE_RATE 1274 | }); 1275 | } 1276 | 1277 | waveCount++; // Increase the tracking variable by one 1278 | } 1279 | ``` 1280 | 1281 | Upon collisions: 1282 | ```javascript 1283 | remove(enemies, (e) => { 1284 | const isCollidingWithFBullets = char("b", e.pos).isColliding.rect.yellow; 1285 | if (isCollidingWithFBullets) { 1286 | color("yellow"); 1287 | particle(e.pos); 1288 | play("explosion"); 1289 | addScore(10 * waveCount, e.pos); 1290 | } 1291 | }); 1292 | ``` 1293 | ```javascript 1294 | remove(eBullets, (eb) => { 1295 | const isCollidingWithFBullets 1296 | = char("c", eb.pos, {rotation: eb.rotation}).isColliding.rect.yellow; 1297 | if (isCollidingWithFBullets) addScore(1, eb.pos); 1298 | }); 1299 | ``` 1300 | 1301 | And congratulations, the game is now in a very playable state 🎉. 1302 | 1303 | ![Scoring](images/step_072.gif) 1304 | 1305 | Step 07 conclusion: [deployment](https://junongx.github.io/crisp-game-lib-tutorial/?step_07) / [code](https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/master/docs/step_07/main.js). 1306 | 1307 | ## Step 08: Extra goodies 1308 | 1309 | While this is all great and nice, I'd like to give you an even cooler version of the game. 1310 | 1311 | ### Step 081: Replay 1312 | 1313 | ```javascript 1314 | options = { 1315 | isReplayEnabled: true 1316 | } 1317 | ``` 1318 | 1319 | By enabling an option with this one single line, the title screen will now automatically replay your last session. Your game is now 10x cooler without you having to do anything. 1320 | 1321 | ![Replay](images/step_081.gif) 1322 | 1323 | ### Step 082: Themes 1324 | 1325 | Excerpt from documentation: 1326 | ```javascript 1327 | // theme?: "simple" | "pixel" | "shape" | "shapeDark" | "crt" | "dark"; 1328 | // // Select the appearance theme. 1329 | ``` 1330 | 1331 | By adding another option `theme`, you'll get access to a set of filters that pretty much transforms your game visually. 1332 | 1333 | ```javascript 1334 | options = { 1335 | theme: "dark" 1336 | } 1337 | ``` 1338 | ![Replay](images/step_082.gif) 1339 | ![Replay](images/step_082b.gif) 1340 | 1341 | However, it should be strongly emphasized that this is **not an option to be used recklessly**. `simple` and `dark` are the only two guaranteed safe options. Everything else is extremely resource hungry, and not all games are suitable for these themes (like this, for example). You are not going to have a very good performance or experience otherwise. 1342 | 1343 | This goes doubly so, if you have any intention of using the next feature. 1344 | 1345 | ### Step 083: GIF capturing 1346 | 1347 | While LiceCap is always there, it is pretty cool to have a built-in tool to natively record gameplay gifs. 1348 | 1349 | ```javascript 1350 | options = { 1351 | isCapturing: true, 1352 | isCapturingGameCanvasOnly: true, 1353 | captureCanvasScale: 2 1354 | } 1355 | ``` 1356 | With at least the first option enabled, pressing the key `C` on your keyboard while running the game will record the last 5 seconds of footage, which will then be inserted into the HTML page the game is running on. You can then retrieve the gif file from there. 1357 | 1358 | By enabling the first option only, you'll get a relatively small GIF with horizontal margins which is optimized for sharing on Twitter. 1359 | 1360 | ![Replay](images/step_083a.gif) 1361 | 1362 | Enabling `isCapturingGameCanvasOnly` will allow you to capture only the game canvas, in which case, you can use the third option `captureCanvasScale` to adjust the output size. This is also how I have been recording gifs for this tutorial. 1363 | 1364 | Needless to say, the smaller the output, the faster it works. It should also be noted that any theme that isn't `simple` and `dark` is not going to play very well with these two options, on top of their potential performance issue. 1365 | 1366 | So there you are, congratulations. Hopefully you have now acquired a good amount of knowledge of CrispGameLib and ready take on your own ideas. 1367 | 1368 | Step 08 conclusion: [deployment](https://junongx.github.io/crisp-game-lib-tutorial/?step_08) / [code](https://raw.githubusercontent.com/JunoNgx/crisp-game-lib-tutorial/master/docs/step_08/main.js). 1369 | 1370 | # Game Distribution 1371 | 1372 | The most simple way to distribute your games made with CrispGameLib is using GitHub Page. 1373 | 1374 | If you already have a forked repository of CrispGameLib: 1375 | * Access the Settings/Pages for the forked repository. 1376 | * Choose the appropriate branch (most likely `master`) and change the source folder to `/docs` from the dropdown menu. 1377 | * Access the game at `https://.github.io//?`. 1378 | 1379 | At this point, you may simply make a copy of `_template`, rename it, and start working on your own games. Your new commits and changes, once pushed to remote, will be instantly reflected on your GitHub Page. Do create branches if you have need to. 1380 | 1381 | Distributing the direct URLs is also a convenient way to let your audiences access your game. 1382 | 1383 | # Community 1384 | 1385 | Feel free to post your work to reddit in our community on Reddit at [r/CrispGameLib](https://www.reddit.com/r/CrispGameLib/) or hashtag your Twitter post with #CrispGameLib. 1386 | 1387 | # Feedback and Critique 1388 | 1389 | Feedback, questions, suggestions, and contribution are highly welcomed. Feel free to reach me in anyway you can, though the most direct way would be opening an issue for this repository. 1390 | --------------------------------------------------------------------------------