├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
1339 | 
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 | 
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 |
--------------------------------------------------------------------------------