Space Invaders Tutorial
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 platformer tutorial does not cover.
Introduction
To work through this tutorial, you need the following:
- The melonJS boilerplate, that we will use as default template project for our tutorial (please make sure to install the required dependencies as instructed in the README)
- The tutorial image assets, to be uncompressed into the
boilerplate src/data directory. So when you unzip, you should have:
data/img/player.png data/img/ships.png
- The melonJS documentation for more details
Testing/debugging :
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.
Though this method is not recommended, since as long as you have the option
enabled, you're adding security vulnerabilities to your environmnet.
A second and easier option is to use a local web server, as for example detailed in the melonJS boilerplate README, by using the npm run dev tool, and that will allow you to test your game in your browser.
Setting up our ships
Your directory structure from the boilerplate should look something like this:
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
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"
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);
});
});
index.js is where the game is bootstrapped. index.html loads index.js as a module, which sets up the window ready event. The me.video.init bit creates the canvas tag and gets the video setup.
Then we initialize the audio engine, telling it what formats we are supporting for this game.
We also tell, using me.loader, what assets needs to be loaded via an array, and set a callback to our loaded function.
The final step of this process is setting the state of the game to loading.
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);
});
The loaded function then sets up the playscreen and tells the game to use that screen object for the play state.
Then the game state is set to PLAY.
Back to space invaders
The first thing to add is images to the resources.js file.
const DataManifest = [
{ name: "player", type: "image", src: "data/img/player.png" },
{ name: "ships", type: "image", src: "data/img/ships.png" }
];
export default DataManifest;
This variable is the one passed to me.loader.preload in index.js
The structure for an asset is :
name | The name of the asset you wish to use in your game. A string key. |
type | The type of the asset. Valid types are: audio, binary, image, json, tmx, tsx. 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. |
src | The path to the asset, relative from index.html. For audio you need specify the folder instead of direct path. |
Open js/stage/play.js and empty the code from the two methods: onResetEvent and onDestroyEvent. Then save, and then open the game in your web browser.
There is not much to see yet. Let's change that.
First thing is to create a player entity.
Add a new file under the js folder, and call it player.js. Be sure to add it in the index.html file.
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;
}
};
So what we're doing is exporting a class that extends me.Sprite. The constructor method grabs the player image from the loader.
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.
Now open up js/stage/play.js, and edit the onResetEvent method so it looks like this:
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;
The onResetEvent is called when this state is loaded. So when invoking
me.state.change(me.state.PLAY);
In the index.js file, onResetEvent is then called.
Yay, the ship is on the bottom of the screen!
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.
me.game.world.addChild(new me.ColorLayer("background", "#000000"), 0);
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.
The second parameter is the color to draw in hex.
The second parameter passed to the addChild function is the z index. We want it to draw first, so we set it at zero.
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.
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:
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;
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.
Back in play.js, add an enemy to the game world. Your play.js should now look like:
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;
You can put the enemy at any x & y to try it out. Save & refresh the page in your browser.
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.
Add a new method to our enemy:
chooseShipImage() {
let frame = me.Math.random(0, 4);
this.renderable.addAnimation("idle", [frame], 1);
this.renderable.setCurrentAnimation("idle");
}
The first line simply randomizes which frame we want. The ship is 32x32, the image is 64x64, so we have 4 possible frames.
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.
With the final line, we set the current animation to idle.
Now call the function at the bottom of the constructor, like so:
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");
}
}
Now refresh the page, and our ship should only pop up as one of them. Try refreshing it multiple times to see it change.
Applying Movement
Now that we have ships on screen, let's actually get some interaction going.
Back in play.js, lets add some keybindings:
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);
}
};
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.
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.
You might also noticed I added the z index option to the addChild calls. It's a pretty good practice, because that way you ensure your draw order.
The onDestroyEvent 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.
Now that we have bindings, let's implement player movement. Add the following update function to the player class:
update(dt) {
return super.update(dt);
}
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):
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;
}
Then modify the update method to check for the key events, and move the player accordingly.
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;
}
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.
super.update(dt);
After that, it's a matter of checking if the left action is currently pressed. Using the velocity value set earlier, we simply subtract the velocity value, multiplied by the delta in seconds.
if (me.input.isKeyPressed("left")) {
this.pos.x -= this.velx * dt / 1000;
}
To move right, we check for the right action, and add the velocity value to our x position.
if (me.input.isKeyPressed("right")) {
this.pos.x += this.velx * dt / 1000;
}
We then use clamp to ensure the x value does not go outside the screen.
this.pos.x = me.Math.clamp(this.pos.x, 32, this.maxX);
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.
return true;
Save the file & refresh your browser. Try using A/D or the Left & Right arrow keys to move.
Enemy movement
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 Container
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.
Create a new file: js/managers/enemy-manager.js, and add it to the index.html.
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;
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).
Notice as well the "enableChildBoundsUpdate" property we are setting to true, to ensure that our object container is resized properly to take in account all added and removing childs.
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.
For adding enemies to our container, we need another method:
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);
}
}
}
Generating 9 columns, and 4 rows: 36 ships.
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.
import EnemyManager from "../managers/enemy-manager.js";
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");
}
Once you save and refresh, you should see a bunch of random ships.
For movement, let's keep it simple and have the container move once per second. For this, we can use a melonjs timer.
Add these two methods to the enemy-manager.js
onActivateEvent() {
this.timer = me.timer.setInterval(() => {
this.pos.x += this.vel;
}, 1000);
}
onDeactivateEvent() {
me.timer.clearInterval(this.timer);
}
And then set the vel property in the init method to 16:
this.vel = 16;
onActivateEvent 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, onDeactivateEvent is called when the object is removed from the game world.
Using the MelonJS version of setInterval (which is built into the game loop, it does not use window.setInterval), we can then increment the x position.
Save and refresh the browser. The enemy ships now all move together
Then add the removeChildNow counterpart:
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);
}
That's a fair bit of code, so let's break it down.
Using the child bounds, we can retrieve the left & right values to world coordinates.
var bounds = this.getBounds();
The first part of the if checks if the container is moving right, and the right edge + velocity is outside the viewport.
(this.vel > 0 && (bounds.right + this.vel) >= me.game.viewport.width)
The second part checks if the container is moving left, and its left bounds is less than zero.
(this.vel < 0 && (bounds.left + this.vel) <= 0)
In the block, we reverse the velocity, move down by 16 pixels and then increase the velocity.
this.vel *= -1;
this.pos.y += 16;
if (this.vel > 0) {
this.vel += 5;
}
else {
this.vel -= 5;
}
Then the last bit, we increment the velocity if the container hasn't moved left or right
else {
this.pos.x += this.vel;
}
Save and refresh; this time, it should now move back and forth across the screen, closer to our player.
Adding Lasers, pew pew!
Time to get some actual “game” in this game.
First thing to do is open up your play.js file, and add a new keybind & unbind:
me.input.bindKey(me.input.KEY.SPACE, "shoot", true);
me.input.unbindKey(me.input.KEY.SPACE);
The reason for the boolean in the bindKey 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.
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.
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;
Oof! That's a lot of code! Let's step through it, line by line.
First, we declare our imports, MelonJS itself, as well as a new file called constants.js
import * as me from 'https://esm.run/melonjs@10';
import CONSTANTS from '../constants.js';
constants.js is a new file we created to store values that should be shared across classes. Here's the contents:
const defines = {
LASER: {
WIDTH: 5,
HEIGHT: 28
}
};
export default defines;
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.
super(x, y, { width: CONSTANTS.LASER.WIDTH, height: CONSTANTS.LASER.HEIGHT });
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.
// 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;
By default, me.Body will not setup shapes for you, so here we pass a shape based on the laser width, and height.
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?
The answer is by using a force field. #facepalm
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.
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.
this.body.vel.set(0, 16);
this.body.force.set(0, -8);
this.body.setMaxVelocity(3, 16);
Then we set a collision type. This is useful in collision callbacks.
this.body.collisionType = me.collision.types.PROJECTILE_OBJECT;
For the rest we basically extend the draw method to actually draw our laser using melonJS basic primitive drawing.
The final step for our Laser's constructor:
constructor(x, y) {
// ...
this.alwaysUpdate = true;
}
The alwaysUpdate 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.
Speaking of the update method.
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;
}
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.
if (this.pos.y + this.height <= 0) {
me.game.world.removeChild(this);
}
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!
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 onResetEvent() 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.
import Laser from './js/renderables/laser.js';
me.pool.register("laser", Laser, true);
Then back in the player.js file, import the constants and add the laser shooting in the update method:
import CONSTANTS from '../constants.js';
if (me.input.isKeyPressed("shoot")) {
me.game.world.addChild(me.pool.pull("laser", this.getBounds().centerX - CONSTANTS.LASER.WIDTH / 2, this.getBounds().top));
}
Reload the game, and try shooting. You should see the lasers fire. However they don't collide with anything.
Collisions
First lets give our Enemy a physics body. Append this to the constructor method in enemy.js
this.body.vel.set(0, 0);
this.body.collisionType = me.collision.types.ENEMY_OBJECT;
Now lets add a collision handler to the laser.js file.
onCollision(response, other) {
if (other.body.collisionType === me.collision.types.ENEMY_OBJECT) {
me.game.world.removeChild(this);
me.state.current().enemyManager.removeChild(other);
return false;
}
}
The res parameter that we are not using, is simply the collision result. So it contains details on how much overlap there was, where the collision was, etc.
Since we set the collision type on the Enemy's body to be an ENEMY_OBJECT, we can check for that type on the object the laser collided with.
if (other.body.collisionType === me.collision.types.ENEMY_OBJECT) {
Then we remove the enemy from the laser, along with the enemy from the enemyManager container.
me.game.world.removeChild(this);
me.state.current().enemyManager.removeChild(other);
The return false in this case isn't strictly necessary, but it's important to point out. When you return false from a collision handler in MelonJS, the object will pass through. If you return true, it will do a hard stop.
Save the changes, and reload your browser. You should now be able to take out the enemy ships.
Next step, is adding the win & loss conditions.
Win & Loss Conditions
The final step to this game is to actually add conditions for winning & losing. The conditions themselves will be pretty straight forward. When the ships get within range of the player, the player loses. When the player destroys all the enemy ships, they win.
So what happens when the game ends? A lot of the time you want to display a screen of some sort that the player lost or won. To keep this simple and show you another little trick, we'll just reset the game. So it starts over.
First, we'll do the loss condition
The pseudo code for this will be:
if enemy manager overlaps player then end game else continue end
The PlayScreen is our current game state. It holds the reference to the player, and it has the ability to reset the state. So let's add the logic for checking a lose condition there. First, we need to store the player object on the state, then we need to check it against another object.
onResetEvent() {
// ... omitted for brevity
this.player = new PlayerEntity();
me.game.world.addChild(this.player, 1);
// ... omitted for brevity
}
checkIfLoss : function (y) {
if (y >= this.player.pos.y) {
this.reset();
}
},
Add that above the onResetEvent method. It accepts a Y value, and checks if it has surpassed the player. Then calls its reset method. The reset will wipe out every object from the game world, and reload the state. So it re-invokes onResetEvent, re-populating the enemies and player.
Now to call this condition check, simply add the method call to our interval in the enemy manager:
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;
}
me.state.current().checkIfLoss(bounds.bottom); // <<<
}
else {
this.pos.x += this.vel;
}
}, 1000);
Since we're checking in the checkIfLoss method if the passed number is greater than the Y position of the player, we need to pass the bottom edge of the container, which is just bounds.bottom.
Save and refresh the browser. this.player will now be set properly, so calling our new method will now work. Let the enemies move around for a minute, and watch the game reset.
The Win Condition
Likewise, we'll just have the game reset once the player wins. Since we want to cause the win once all the ships are gone, we can check the length of the children on enemy manager.
First add this boolean to the bottom of the createEnemies method:
this.createdEnemies = true;
Add the following change to the onChildChange callback previously defined in the enemy manager:
this.onChildChange = () => {
if(this.children.length === 0) {
me.state.current().reset();
}
}
This is pretty simple. Children is an array, so we check its length to be zero, and then reset the game if the condition is met
Save and refresh the browser. Try to take out all the ships in time, and see the game reset.
Challenges
We left some parts out of this tutorial, so you could explore them yourself. This is an important part of programming and game development.
If you get stuck on any of the challenges or parts of the tutorial, please search for the problem, or ask us the question on our forum @html5gamedevs
Challenge #1
Add a proper win & loss screen
- These screens can be made by adding additional ScreenObjects to the game, register them in index.js, and then changing state. For what states to use for the win & screen, look at the states available: http://melonjs.github.io/melonJS/docs/state.html
- The win and loss screen can contain a sprite, or text, or both. Whatever you wish really. Be sure to look at Text and Sprite. To display a me.Text object, use an instance of me.Renderable that contains an instance of me.Text, and implement the draw function to invoke me.Text#draw.
- Adjust the checkIfLoss method to show your new loss screen instead.
- Adjust the if block in the update method on EnemyManager, to change state to your win screneobject.
- Even more bonus, add a menu screen that tells the player how to play.
Challenge #2
Add a UI
- Add an enemy counter, and enemy velocity to the top right/left corner of the screen. These properties can be retrieved via: me.state.current().enemyManager.children.length me.state.current().enemyManager.vel
- Again look at Text, and implement a renderable for drawing text. Try to only use one class that extends renderable that can be used for both UI pieces.
- Add a score element. Keep track of the score on the play screen. Update it each time an enemy is killed. Remember that enemies are removed from the collision handler on the laser.
Challenge #3
Add the concept of levels
- After you defeat a wave, instead of refreshing the same wave, do a new wave the starts faster. The main logic here will be keeping wave count on the index.js, and increase it after each win. Then use that count in the enemy manager to configure the velocity.
- Have each wave progress faster too (+ 8 each Y increment over + 5 for example). Play with the numbers a bit until it feels right.