This repository was archived by the owner on Aug 27, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathindex.html
More file actions
1211 lines (931 loc) · 49.8 KB
/
Copy pathindex.html
File metadata and controls
1211 lines (931 loc) · 49.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE HTML>
<html>
<head>
<title>melonJS - Space invaders tutorial</title>
<meta name="description" content="melonJS space invaders tutorial">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="vendor/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="vendor/prism.css">
<link rel="stylesheet" type="text/css" href="style/style.css">
<link rel="icon" type="image/png" href="style/favicon.png">
<script type="text/javascript">
//<![CDATA[
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-13050059-3']);
_gaq.push(['_trackPageview']);
(function () {
var ga = document.createElement('script');
ga.type = 'text/javascript';
ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ga, s);
})();
//]]>
</script>
</head>
<body class="language-javascript">
<a href="https://github.com/melonjs/tutorial-space-invaders"><img
style="position: absolute; top: 0; right: 0; border: 0; z-index: 1000;"
src="https://camo.githubusercontent.com/e7bbb0521b397edbd5fe43e7f760759336b5e05f/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677265656e5f3030373230302e706e67"
alt="Fork me on GitHub"
data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_green_007200.png"></a>
<div id="main">
<div class="row">
<div id="sidebar" class="col-md-2 col-md-offset-1 sidebar">
<a class="logo" href="http://www.melonjs.org/"><img src="style/melon.png" alt="melonJS"></a>
<ul class="nav nav-sidebar">
<li><h3>Content</h3></li>
<li><a href="#intro">Introduction</a></li>
<li><a href="#part1">Setting up our ships</a></li>
<li><a href="#part2">Applying movement</a></li>
<li><a href="#part3">Adding Lasers, pew pew!</a></li>
<li><a href="#part4">Collisions</a></li>
<li><a href="#part5">Win & Loss Conditions</a></li>
<li><a href="#challenges">Challenges</a></li>
</ul>
</div>
<div id="content" class="col-md-8 col-md-offset-3 main">
<h1>Space Invaders Tutorial</h1>
<p>In this tutorial, we will create a space invaders clone. This tutorial will primarily be focused on
creating more game elements through code, and using other APIs that MelonJS provides, that the <a
href="http://melonjs.github.io/tutorial-platformer/">platformer tutorial</a> does not cover.</p>
<!-- INTRO ================================================================== -->
<div>
<a id="intro" class="offset-anchor"></a>
<div class="subcontent">
<h2>Introduction</h2>
<p>To work through this tutorial, you need the following:</p>
<ul>
<li>The melonJS <a
href="https://github.com/melonjs/es6-boilerplate/archive/refs/heads/main.zip">boilerplate</a>, that
we will use as default template project for our tutorial (please make sure to install the required dependencies as instructed in the <a
href="https://github.com/melonjs/es6-boilerplate#readme">README</a>)
</li>
<li>The tutorial <a href="tutorial_data.zip">image assets</a>, to be uncompressed into the
boilerplate src/data directory. So when you unzip, you should have:
<pre>
data/img/player.png
data/img/ships.png
</pre>
</li>
<li>The melonJS <a href="http://melonjs.github.io/melonJS/docs/">documentation</a> for more
details
</li>
</ul>
<p>
<b>Testing/debugging :</b><br/>
If you just want to use the filesystem, the problem is you'll run into "cross-origin request"
security errors. With Chrome, you need to use the "--disable-web-security" parameter or better
"--allow-file-access-from-files" when launching the browser. This must be done in order to test
any local content, else the browser will complain when trying to load assets through XHR.
<strong>Though this method is not recommended, </strong> since as long as you have the option
enabled, you're adding security vulnerabilities to your environmnet.
</p>
<p> A second and easier option is to use a local web server, as for example detailed in the melonJS
boilerplate <a href="https://github.com/melonjs/es6-boilerplate#readme">README</a>, by using the <b>npm run dev</b>
tool, and that will allow you to test your game in your browser.
</p>
</div>
</div>
<!-- Part 1 -->
<a id="part1" class="offset-anchor"></a>
<div class="subcontent">
<h2>Setting up our ships</h2>
<p>Your directory structure from the boilerplate should look something like this:</p>
<pre>
data/
img/
player.png
ships.png
js/
plugin/
debug/
renderables/
player.js
stage/
play.js
title.js
index.css
index.html
index.js
manifest.js
</pre>
<p>The boilerplate provides a bunch of default code. For this tutorial there are some files that we will
not need. You can delete the file stage/title.js. Then
update the index.html file to no longer include those, and remove the references of TitleScreen from
the game.js file. Lastly, set the scaleMethod to "flex-width"</p>
<pre><code>me.device.onReady(function () {
// initialize the display canvas once the device/browser is ready
if (!me.video.init(1218, 562, {parent : "screen", scale : "auto", scaleMethod: "flex-width"})) {
alert("Your browser does not support HTML5 canvas.");
return;
}
// Initialize the audio.
me.audio.init("mp3,ogg");
// allow cross-origin for image/texture loading
me.loader.crossOrigin = "anonymous";
// set and load all resources.
me.loader.preload(DataManifest, function() {
// set the user defined game stages
me.state.set(me.state.PLAY, new PlayScreen());
// Start the game.
me.state.change(me.state.PLAY);
});
});
</code></pre>
<p>index.js is where the game is bootstrapped. index.html loads index.js as a module, which sets up the
window ready event. The <strong>me.video.init</strong> bit creates the
canvas tag and gets the video setup.</p>
<p>Then we initialize the audio engine, telling it what formats we are supporting for this game.</p>
<p>We also tell, using <strong>me.loader</strong>, what assets needs to be loaded via an array, and set
a callback to our loaded function.</p>
<p>The final step of this process is setting the state of the game to loading.</p>
<aside>
<h5>Note</h5>
<p>Game states can be used to for things like a menu screen, play screen, pause screen, etc. When
changing state, it's important to note that everything is removed from the game world, unless
you have it set to be <a
href="http://melonjs.github.io/melonJS/docs/Renderable.html#isPersistent"
target="_blank">persistent</a> across states.</p>
</aside>
<pre><code>me.loader.preload(DataManifest, function() {
// set the user defined game stages
me.state.set(me.state.PLAY, new PlayScreen());
// Start the game.
me.state.change(me.state.PLAY);
});
</code></pre>
<p>The loaded function then sets up the playscreen and tells the game to use that screen object for the
play state.</p>
<p>Then the game state is set to PLAY.</p>
<h4>Back to space invaders</h4>
<p>The first thing to add is images to the resources.js file.</p>
<pre><code>const DataManifest = [
{ name: "player", type: "image", src: "data/img/player.png" },
{ name: "ships", type: "image", src: "data/img/ships.png" }
];
export default DataManifest;
</code></pre>
<p>This variable is the one passed to me.loader.preload in index.js</p>
<p>The structure for an asset is :</p>
<table>
<tr>
<td>name</td>
<td>The name of the asset you wish to use in your game. A string key.</td>
</tr>
<tr>
<td>type</td>
<td>The type of the asset. Valid types are: <strong>audio, binary, image, json, tmx,
tsx</strong>. Binary is a good solution for loading raw text, or any other format not
listed. TMX & TSX are for tiled file formats. Whether it be the xml or json format.
</td>
</tr>
<tr>
<td>src</td>
<td>The path to the asset, relative from index.html. For audio you need specify the folder
instead of direct path.
</td>
</tr>
</table>
<p>Open <strong>js/stage/play.js</strong> and empty the code from the two methods: <strong>onResetEvent</strong>
and <strong>onDestroyEvent</strong>. Then save, and then open the game in your web browser.</p>
<p>There is not much to see yet. Let's change that.</p>
<p>First thing is to create a player entity.</p>
<p>Add a new file under the js folder, and call it <strong>player.js</strong>. Be sure to add it in the
index.html file.</p>
<pre><code>class PlayerEntity extends me.Sprite {
/**
* constructor
*/
constructor() {
let image = me.loader.getImage("player");
super(
me.game.viewport.width / 2 - image.width / 2,
me.game.viewport.height - image.height - 20,
{ image : image, width: 32, height: 32 }
);
}
/**
* update the sprite
*/
update(dt) {
// change body force based on inputs
//....
// call the parent method
return super.update(dt);
}
/**
* collision handler
* (called when colliding with other objects)
*/
onCollision(response, other) {
// Make all other objects solid
return true;
}
};
</code></pre>
<p>
So what we're doing is exporting a class that extends
<strong>me.Sprite</strong>. The constructor method grabs the player image from the loader.</p>
<p>For the x coordinate, we simply grab the dead center, and subtract half the ship, so it can be
positioned in the center. And then set its y property to be 20 pixels above the bottom. Then finally
pass the image instance to it.</p>
<p>
Now open up <strong>js/stage/play.js</strong>, and edit the onResetEvent method so it looks like
this:
</p>
<pre><code>
import * as me from 'https://esm.run/melonjs@10';
import PlayerEntity from "../renderables/player.js";
class PlayScreen extends me.Stage {
/**
* action to perform on state change
*/
onResetEvent() {
this.player = new PlayerEntity();
me.game.world.addChild(this.player, 1);
}
};
export default PlayScreen;
</code></pre>
<p>The <strong>onResetEvent</strong> is called when this state is loaded. So when invoking</p>
<pre><code>me.state.change(me.state.PLAY);</code></pre>
<p>In the index.js file, <strong>onResetEvent</strong> is then called.</p>
<img src="screens/screenone.png"/>
<p>Yay, the ship is on the bottom of the screen!
</p>
<p>But we can still see the loading bar, that's not cool. The reason for this is that MelonJS does not
want to do any operations that it doesn't have to. Sometimes you'll have a background image that
gets redrawn, so it covers the original loading bar. However, we don't have a background image for
this game, so what we will do is add a color layer in play.js.</p>
<pre><code>me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);</code></pre>
<p>
Add that in the play screen, above the line where we added the player. The first parameter is simply
the name for the layer, so it's easy to fetch from the game world later if you need to.</p>
<p>The second parameter is the color to draw in hex.</p>
<p>The second parameter passed to the addChild function is the z index. We want it to draw first, so we
set it at zero.
</p>
<p>Now the pesky loading bar should be gone. Time to add in an enemy. Create a new file under the js
folder called enemy.js, and add it to the index.html file.</p>
<p>Since enemies will have to collide with things like the player's laser, we also need to add a me.Body object to our EnemyEntity, so lets get that going:</p>
<pre><code>
import * as me from 'https://esm.run/melonjs@10';
class EnemyEntity extends me.Sprite {
constructor(x, y) {
super(x, y, {
image: "ships",
framewidth: 32,
frameheight: 32,
});
// give the sprite a physics body so it can collide and stuff
this.body = new me.Body(this);
this.body.addShape(new me.Rect(0, 0, this.width, this.height));
// ignore gravity so the ship doesn't fall through the bottom of the screen
this.body.ignoreGravity = true;
}
}
export default EnemyEntity;
</code></pre>
<p>With the enemy, we will need to place them in different spots, so x & y will be added to its
constructor, and then passed along to the me.Sprite's constructor. The third parameter in the array
is a hash of settings. The settings specifies the image as "ships", referencing our DataManifest
array, with the frame width and height set to 32x32.</p>
<p>Back in play.js, add an enemy to the game world. Your play.js should now look like:</p>
<pre><code>
import * as me from 'https://esm.run/melonjs@10';
import PlayerEntity from "../renderables/player.js";
import EnemyEntity from './../renderables/enemy.js';
class PlayScreen extends me.Stage {
/**
* action to perform on state change
*/
onResetEvent() {
me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
this.player = new PlayerEntity();
this.enemy = new EnemyEntity(50, 50);
me.game.world.addChild(this.player, 1);
me.game.world.addChild(this.enemy, 2);
}
};
export default PlayScreen;
</code></pre>
<p>You can put the enemy at any x & y to try it out. Save & refresh the page in your
browser.</p>
<p>You'll likely notice that the ship is constantly changing how it looks. If you open the ships.png
file under data/img, you can see that it is a sprite sheet containing 4 different ships. Since we didn't add and set
any default animation yet, it is just looping through each & every frame. Let's
fix that.</p>
<p>Add a new method to our enemy:</p>
<pre><code>chooseShipImage() {
let frame = me.Math.random(0, 4);
this.renderable.addAnimation("idle", [frame], 1);
this.renderable.setCurrentAnimation("idle");
}
</code></pre>
<p>The first line simply randomizes which frame we want. The ship is 32x32, the image is 64x64, so we
have 4 possible frames.</p>
<p>The second line is accessing the animation sheet instance (this.renderable), and uses the
addAnimation function to add a new idle frame. So we simply specify the index that was generated at
random.</p>
<p>With the final line, we set the current animation to idle.</p>
<p>Now call the function at the bottom of the constructor, like so:</p>
<pre><code>class EnemyEntity extends me.Sprite {
constructor(x, y) {
super(x, y, {
image: "ships",
framewidth: 32,
frameheight: 32,
});
// give the sprite a physics body so it can collide and stuff
this.body = new me.Body(this);
this.body.addShape(new me.Rect(0, 0, this.width, this.height));
this.body.ignoreGravity = true;
//
this.chooseShipImage();
}
/**
*
*/
chooseShipImage() {
let frame = me.Math.random(0, 4);
this.addAnimation("idle", [frame], 1);
this.setCurrentAnimation("idle");
}
}
</code></pre>
<p>Now refresh the page, and our ship should only pop up as one of them. Try refreshing it multiple
times to see it change.</p>
<p><a href="tutorial_step1/index.html"><img src="media/tutorial_step1.png" alt="Step One"></a></p>
</div>
<!-- Part 2 -->
<a id="part2" class="offset-anchor"></a>
<div class="subcontent">
<h2>Applying Movement</h2>
<p>Now that we have ships on screen, let's actually get some interaction going.</p>
<p>Back in play.js, lets add some keybindings:</p>
<pre><code>class PlayScreen extends me.Stage {
/**
* action to perform on state change
*/
onResetEvent() {
me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
this.player = new PlayerEntity();
this.enemy = new EnemyEntity(50, 50);
me.game.world.addChild(this.player, 1);
me.game.world.addChild(this.enemy, 2);
me.input.bindKey(me.input.KEY.LEFT, "left");
me.input.bindKey(me.input.KEY.RIGHT, "right");
me.input.bindKey(me.input.KEY.A, "left");
me.input.bindKey(me.input.KEY.D, "right");
}
/**
*
*/
onDestroyEvent() {
me.input.unbindKey(me.input.KEY.LEFT);
me.input.unbindKey(me.input.KEY.RIGHT);
me.input.unbindKey(me.input.KEY.A);
me.input.unbindKey(me.input.KEY.D);
}
};
</code></pre>
<p>The method calls here are pretty straight forward. We bind a keypress to an action name. Multiple
keys can be assigned to a single action name.</p>
<p>It's typically a good game design practice to offer multiple key bindings. Even a better practice
make it configurable. You always need to keep in mind people who are left handed or who have
different layouts.</p>
<p>You might also noticed I added the <strong>z index</strong> option to the addChild calls. It's a
pretty good practice, because that way you ensure your draw order.</p>
<p>The <strong>onDestroyEvent</strong> removes the events when changing state. Not something we actually
need, because we only have the play state after loading. But a good practice to keep in mind.</p>
<p>Now that we have bindings, let's implement player movement. Add the following update function to the
player class:</p>
<pre><code>update(dt) {
return super.update(dt);
}</code></pre>
<p>Then add a velx property to the player in its constructor method, as well as the furthest x position it can
go on screen (maxX):</p>
<pre><code>constructor() {
let image = me.loader.getImage("player");
super(
me.game.viewport.width / 2 - image.width / 2,
me.game.viewport.height - image.height - 20,
{ image : image, width: 32, height: 32 }
);
this.velx = 450;
this.maxX = me.game.viewport.width - this.width;
}
</code></pre>
<p>Then modify the update method to check for the key events, and move the player accordingly.</p>
<pre><code>update(dt) {
super.update(dt);
if (me.input.isKeyPressed("left")) {
this.pos.x -= this.velx * time / 1000;
}
if (me.input.isKeyPressed("right")) {
this.pos.x += this.velx * time / 1000;
}
this.pos.x = me.Math.clamp(this.pos.x, 32, this.maxX);
return true;
}
</code></pre>
<p>Update functions of our game objects will always receive a delta time (in milliseconds). It's
important to pass it along to our parent's class update.</p>
<pre><code>super.update(dt);</code></pre>
<p>After that, it's a matter of checking if the <strong>left</strong> action is currently pressed. Using
the velocity value set earlier, we simply subtract the velocity value, multiplied by the delta in
seconds.</p>
<pre><code>if (me.input.isKeyPressed("left")) {
this.pos.x -= this.velx * dt / 1000;
}
</code></pre>
<p>To move right, we check for the <strong>right</strong> action, and add the velocity value to our x
position.</p>
<pre><code>if (me.input.isKeyPressed("right")) {
this.pos.x += this.velx * dt / 1000;
}
</code></pre>
<aside>
<h5>Note</h5>
<p>The reason for use of delta time, is to have consistent transformations across various frame
rates. If you have pos.x += 5; in your update, and run at 1 frame per second, the player will
move 5 pixels once per second. If you have 30 fps, the player will move at 150 pixels per
second. The delta time is used to level the playing field. The less fps you have, the more jumpy
it will look.</p>
</aside>
<p>We then use <strong>clamp</strong> to ensure the x value does not go outside the screen.</p>
<pre><code>this.pos.x = me.Math.clamp(this.pos.x, 32, this.maxX);</code></pre>
<p>The return value tells melon whether a re-draw is required. This can be useful to dictate for when an
animation sheet needs to animate on a given frame. However, this is a single sprite, so we can just
tell it to redraw.</p>
<pre><code>return true;</code></pre>
<p>Save the file & refresh your browser. Try using A/D or the Left & Right arrow keys to
move.</p>
<h4>Enemy movement</h4>
<p>A defining characteristic of space invaders is that all the ships move in one direction, shift down
and then go in the other direction. They all move together. We could take the velocity logic that we
used for the player, and apply it to the enemy class. But we can better leverage MelonJS to do this
for us. Time to use our own subclass of <strong><a
href="http://melonjs.github.io/melonJS/docs/Container.html">Container</a></strong></p>
<p>Objects inside a container are relative to its parent. So when we move the container, all objects
inside shift with it. This applies to rotation & scale operations as well. So let's create
one.</p>
<p>Create a new file: js/managers/enemy-manager.js, and add it to the index.html.</p>
<pre><code>
import * as me from 'https://esm.run/melonjs@10';
import EnemyEntity from './../renderables/enemy.js';
class EnemyManager extends me.Container {
static COLS = 9;
static ROWS = 4;
constructor() {
super(0, 32, EnemyManager.COLS * 64 - 32, EnemyManager.ROWS * 64 - 32);
this.enableChildBoundsUpdate = true;
this.vel = 16;
}
}
export default EnemyManager;
</code></pre>
<p>Essentially what we're setting up here is the start position and the base width. Starting the
container 32 pixels down, and at 0 left (or x).</p>
<p>Notice as well the <a href="http://melonjs.github.io/melonJS/docs/Container.html#enableChildBoundsUpdate">"enableChildBoundsUpdate"</a>
property we are setting to true, to ensure that our object container is resized properly
to take in account all added and removing childs.</p>
<p>We're allotting 64 pixels per ship width & height wise. Then subtracting 32 pixels because the
last row & column does not require the side padding.</p>
<p>For adding enemies to our container, we need another method:</p>
<pre><code>createEnemies() {
for (let i = 0; i < EnemyManager.COLS; i++) {
for (let j = 0; j < EnemyManager.ROWS; j++) {
var enemy = new EnemyEntity(i * 64, j * 64);
this.addChild(enemy);
}
}
}
</code></pre>
<p>Generating 9 columns, and 4 rows: 36 ships. </p>
<p>Now in play.js, import EnemyManager, remove the addChild for the enemy, and set a property to an enemy manager. Below that
invoke createEnemies, and add it to the game world.</p>
<pre><code>import EnemyManager from "../managers/enemy-manager.js";</code></pre>
<pre><code>onResetEvent() {
me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
this.player = new PlayerEntity();
me.game.world.addChild(this.player, 1);
this.enemyManager = new EnemyManager();
this.enemyManager.createEnemies();
me.game.world.addChild(this.enemyManager, 2);
me.input.bindKey(me.input.KEY.LEFT, "left");
me.input.bindKey(me.input.KEY.RIGHT, "right");
me.input.bindKey(me.input.KEY.A, "left");
me.input.bindKey(me.input.KEY.D, "right");
}
</code></pre>
<p>Once you save and refresh, you should see a bunch of random ships.</p>
<img src="screens/screentwo.png" alt=""/>
<p>For movement, let's keep it simple and have the container move once per second. For this, we can use
a melonjs timer.</p>
<p>Add these two methods to the enemy-manager.js</p>
<pre><code>onActivateEvent() {
this.timer = me.timer.setInterval(() => {
this.pos.x += this.vel;
}, 1000);
}
onDeactivateEvent() {
me.timer.clearInterval(this.timer);
}
</code></pre>
<p>And then set the vel property in the init method to 16:</p>
<pre><code>this.vel = 16;</code></pre>
<p><strong>onActivateEvent</strong> is called (if it's defined) when the object is added to the game
world. This goes for any object you pass to addChild on a container. Likewise, <strong>onDeactivateEvent</strong>
is called when the object is removed from the game world.</p>
<p>Using the MelonJS version of <strong>setInterval</strong> (which is built into the game loop, it does
not use window.setInterval), we can then increment the x position.</p>
<p>Save and refresh the browser. The enemy ships now all move together</p>
<p>Then add the <strong>removeChildNow</strong> counterpart:</p>
<pre><code>onActivateEvent() {
this.timer = me.timer.setInterval(() => {
let bounds = this.getBounds();
if ((this.vel > 0 && (bounds.right + this.vel) >= me.game.viewport.width) ||
(this.vel < 0 && (bounds.left + this.vel) <= 0)) {
this.vel *= -1;
this.pos.y += 16;
if (this.vel > 0) {
this.vel += 5;
}
else {
this.vel -= 5;
}
}
else {
this.pos.x += this.vel;
}
}, 1000);
}
</code></pre>
<p>That's a fair bit of code, so let's break it down.</p>
<p>Using the child bounds, we can retrieve the left & right values to world coordinates.</p>
<pre><code>var bounds = this.getBounds();</code></pre>
<p>The first part of the if checks if the container is moving right, and the right edge + velocity is
outside the viewport.</p>
<pre><code>(this.vel > 0 && (bounds.right + this.vel) >= me.game.viewport.width)</code></pre>
<p>The second part checks if the container is moving left, and its left bounds is less than zero.</p>
<pre><code>(this.vel < 0 && (bounds.left + this.vel) <= 0)</code></pre>
<p>In the block, we reverse the velocity, move down by 16 pixels and then increase the velocity.</p>
<pre><code>this.vel *= -1;
this.pos.y += 16;
if (this.vel > 0) {
this.vel += 5;
}
else {
this.vel -= 5;
}
</code></pre>
<p>Then the last bit, we increment the velocity if the container hasn't moved left or right</p>
<pre><code>else {
this.pos.x += this.vel;
}
</code></pre>
<p>Save and refresh; this time, it should now move back and forth across the screen, closer to our
player. </p>
<p><a href="tutorial_step2/index.html"><img src="media/tutorial_step2.png" alt="Live Example"></a></p>
</div>
<!-- Part 3 -->
<a id="part3" class="offset-anchor"></a>
<div class="subcontent">
<h2>Adding Lasers, pew pew!</h2>
<p>Time to get some actual “game” in this game.</p>
<p>First thing to do is open up your play.js file, and add a new keybind & unbind:</p>
<pre><code>me.input.bindKey(me.input.KEY.SPACE, "shoot", true);</code></pre>
<pre><code>me.input.unbindKey(me.input.KEY.SPACE);</code></pre>
<p>The reason for the boolean in the <strong>bindKey</strong> call is to only allow one register per key
press. So in order to shoot twice, the player must press the space bar, release it, and then press
it again.</p>
<p>Before we wire up the player to shoot, we need a laser. Create a laser.js file, and add the following
code to it. As always, be sure to import the laser.js script tag in the index.js file.</p>
<pre><code>
import * as me from 'https://esm.run/melonjs@10';
import PlayScreen from "../stage/play.js";
import CONSTANTS from '../constants.js';
export class Laser extends me.Renderable {
/**
* constructor
*/
constructor(x, y) {
super(x, y, CONSTANTS.LASER.WIDTH, CONSTANTS.LASER.HEIGHT);
// add a physic body and configure it
this.body = new me.Body(this);
// add a default collision shape
this.body.addShape(new me.Rect(0, 0, this.width, this.height));
// this body has a velocity of 0 units horizontal and 16 units vertically
this.body.vel.set(0, 16);
// the force to be applied at each update is -8 units vertically (in html, this means towards top of window)
this.body.force.set(0, -8);
// cap the velocity of the laser beam to the initial velocity
this.body.setMaxVelocity(3, 16);
// this object is officially a projectile
this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;
// don't let gravity affect the object
this.body.ignoreGravity = true;
// always update, so that we can track it when outside the screen
this.alwaysUpdate = true;
}
/**
* call when the object instance is being recycled
*/
onResetEvent(x, y) {
this.pos.set(x, y);
}
/**
*
* @param dt
* @returns {boolean}
*/
update(dt) {
// if the laser is above the screen, remove it from the game world
if (this.pos.y + this.height <= 0) {
me.game.world.removeChild(this);
}
return super.update(dt);
}
/**
* draw the laser
*/
draw(renderer) {
let color = renderer.getColor();
renderer.setColor('#5EFF7E');
renderer.fillRect(this.pos.x, this.pos.y, this.width, this.height);
renderer.setColor(color);
}
}
export default Laser;
</code></pre>
<p>Oof! That's a lot of code! Let's step through it, line by line.</p>
<p>First, we declare our imports, MelonJS itself, as well as a new file called constants.js</p>
<pre><code>
import * as me from 'https://esm.run/melonjs@10';
import CONSTANTS from '../constants.js';</code></pre>
<p>constants.js is a new file we created to store values that should be shared across classes. Here's the contents:</p>
<pre><code>const defines = {
LASER: {
WIDTH: 5,
HEIGHT: 28
}
};
export default defines;</code></pre>
<p>Traditional stuff here. Setup the x & y position from its parameters, and a width+height
properties. A bit different from our other objects, we have set the z index on the object manually.
This is an alternative to passing the z index in the addChild call.</p>
<pre><code>super(x, y, { width: CONSTANTS.LASER.WIDTH, height: CONSTANTS.LASER.HEIGHT });</code></pre>
<p>Next is to add a physics body. Which we will use to move the laser across the
screen and enable collisio detection with the eneny ships.</p>
<pre><code>
// add a physic body and configure it
this.body = new me.Body(this);
// add a default collision shape
this.body.addShape(new me.Rect(0, 0, this.width, this.height));
this.body.vel.set(0, 16);
this.body.force.set(0, -8);
this.body.setMaxVelocity(3, 16);
this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;
</code></pre>
<p>By default, me.Body will not setup shapes for you, so here we pass a shape based on the
laser width, and height.</p>
<p>Then we set a velocity. Velocity is a vector, and we want the laser to move up.
Now, note that velocity should never be negative to dictate direction. So how do we get the beam
to move up the screen?</p>
<p>The answer is by using a force field. #facepalm</p>
<p>Seriously, though, we need to tell the laser's body that the force being applied to it each update is
negative. That's why we tell the body that the force is negative 8 units.</p>
<p>Lastly, we need to cap the velocity so it doesn't accidentally move faster than we want. This simply tells
MelonJS that the body cannot move faster than 3 units horizontally and 16 units vertically.</p>
<pre><code>this.body.vel.set(0, 16);
this.body.force.set(0, -8);
this.body.setMaxVelocity(3, 16);
</code></pre>
<p>Then we set a collision type. This is useful in collision callbacks.</p>
<pre><code>this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;</code></pre>
<p>For the rest we basically extend the draw method to actually draw our laser using melonJS basic primitive drawing.</p>
<p>The final step for our Laser's constructor:</p>
<pre><code>constructor(x, y) {
// ...
this.alwaysUpdate = true;
}
</code></pre>
<p>The <strong>alwaysUpdate</strong> property is to be avoided as much as possible. It will update an
object when it is outside the viewport. The reason to use it in this game is because we don't want
to remove the laser until it is offscreen. If we wait until it's offscreen, and alwaysUpdate is
false, it will never get removed.</p>
<p>Speaking of the update method.</p>
<pre><code>update(dt) {
// because we're using melonjs' physics system, all we need to do is update the object.
// this call will move the object
super.update(dt);
// if the laser is above the screen, remove it from the game world
if (this.pos.y + this.height <= 0) {
me.game.world.removeChild(this);
}
return true;
}
</code></pre>
<p>If the position of the laser plus the height (so the bottom of the laser) is less than zero, we can
remove the laser from the game world. Again, this will function now work because alwaysUpdate is set
to true.</p>
<pre><code>if (this.pos.y + this.height <= 0) {
me.game.world.removeChild(this);
}
</code></pre>
<p>One of the new features in MelonJS 10 is that you no longer need to manually tell the entity to
check collisions or update the body!</p>
<p>The next step, we are going to use the object pooling feature,
for this we need to register the laser to the objet pool,
and importantly to pass true as the third parameter to actually reuse any available instance (if false, which is the default, the object pooling system will return only new intances).
You will notice as well that we have a <b>onResetEvent()</b> method, this one is called by the object pooling when reusing an object instance (since the constructor is only called one time when created)
Add the following code to index.js, same as the Player & Enemy objects.</p>
<pre><code>import Laser from './js/renderables/laser.js';</code></pre>
<pre><code>me.pool.register("laser", Laser, true);</code></pre>
<p>Then back in the player.js file, import the constants and add the laser shooting in the update method:</p>
<pre><code>import CONSTANTS from '../constants.js';</code></pre>
<pre><code>if (me.input.isKeyPressed("shoot")) {
me.game.world.addChild(me.pool.pull("laser", this.getBounds().centerX - CONSTANTS.LASER.WIDTH / 2, this.getBounds().top));
}
</code></pre>
<p>Reload the game, and try shooting. You should see the lasers fire. However they don't collide with
anything.</p>
<p><a href="tutorial_step3/index.html"><img src="screens/screenthree.png" alt="PEW PEW PEW"/></a></p>
</div>
<a id="part4" class="offset-anchor"></a>
<div class="subcontent">
<h2>Collisions</h2>
<p>First lets give our Enemy a physics body. Append this to the constructor method in enemy.js</p>
<pre><code>this.body.vel.set(0, 0);
this.body.collisionType = me.collision.types.ENEMY_OBJECT;
</code></pre>
<p>Now lets add a collision handler to the laser.js file.</p>
<pre><code>onCollision(response, other) {
if (other.body.collisionType === me.collision.types.ENEMY_OBJECT) {
me.game.world.removeChild(this);