By Fernando Doglio
Translation: Crazy geek
Original text: blog. Bitsrc. IO/writing – a – t…
Strictly prohibited without permission
Game development doesn’t have to be limited to people using Unity or Unreal Engine4. JavaScript game development has been around for a while. In fact, the latest versions of the most popular browsers (such as Chrome, Firefox and Edge) offer support for advanced graphics rendering (such as WebGL), which opens up very interesting game development opportunities.
However, there is no way to cover all of it in one article (there are entire books written on it), and for personal reasons, I prefer to rely on frameworks before delving into specific technologies.
That’s why, after some research, I decided to write this quick tutorial in MelonJS.
What is MelonJS?
As you might have guessed, MelonJS is a JavaScript game engine that is fully compatible with all major browsers (from Chrome to Opera, all the way to Chrome mobile and iOS Safari).
It has a set of features that stood out during my research:
- For starters, it is completely self-contained and requires no external dependencies to make it work.
- However, it can be integrated with a number of third-party tools to make your job easier, such as Tiled (which helps you create maps and game levels), TexturePacker (which helps you create the texture atlas you need and simplifies and optimise Sprite management).
- Integrated 2D physics engine. This means you can use realistic 2D motion and collision detection right out of the box. This is critical, because it takes a lot of work (not to mention math, which is not my cup of tea) to solve all these problems.
- Support for sound apis that allow you to add sound effects and background music with great ease.
There are other awesome features that you can check out on the engine’s website, but those are the ones we’ll focus on in this article.
** Use Bit (Github) to easily share and reuse JS modules, UI components in projects, suggest updating.
Bit components: Easily shared across projects across teams
Design our games
The purpose of a typing game is to provide the player with the ability to move or perform certain actions by typing (or hitting random keys).
I remember learning how to type as a kid (yes, a long time ago) in a game called “Mario Teaches Typing” where you had to type a single letter to progress, either jumping on a turtle or hitting a square from below. The image below gives you an idea of what the game looks like and how to interact with it.
While it’s a fun little game, it’s not really a platformer, as the actions Mario performs always correspond to a button and never fail.
For this article, though, I wanted to make things more interesting, rather than creating a simple typing game like the one above:
Instead of a single letter deciding what to do next, the game offers five choices, each of which must be written in one full word:
- forward
- Leap forward
- Jump up
- Jump backward
- Move back
In other words, instead of the classic arrow-based control, you can move the character by the words you type.
Beyond that, the game will be a classic platformer, where players can collect coins by walking around. For the sake of brevity, we’ll exclude enemies and other types of entities from this tutorial (although you should be able to deduce the code used and create your own entities based on that code).
To keep this article at a reasonable length, I’ll focus on just one stage, a full range of actions (in other words, you’ll be able to perform all five), a few enemies, a collection of collectibles, and a decent number of steps to jump on.
The tools you need
Although melonJS is completely independent, there are some tools that can help you along the way, and I recommend you use them:
- Texture Packer: With this, you will be able to automatically generate a Texture atlas, which is another way of expressing JSON files that package all the images so the engine can retrieve them later and use them as needed. If you don’t have this tool, manually maintaining the atlas can be too time-consuming.
- Tiled: This will be our level editor. Although you can download it for free (you’ll need to find a link that says “No thanks, just take me to the Downloads”), you can donate as little as $1 to the author of this magical tool. This is recommended if you have a PayPal account or debit card available, as it takes time and effort to maintain.
Using these tools, you’ll be able to continue learning and complete this tutorial, so let’s start coding.
Basic platformer
To get started with this project, we can use some sample code. When you download the engine, it will come with a default set of sample projects that you can check out (they are in the Example folder).
This sample code is the code we used to get the project started quickly. Among them, you will find:
-
The data folder, which contains everything unrelated to the code. Here you can find sounds, music, images, map definitions and even fonts.
-
Js folder, where you will save all game-related code.
-
Index.html and index.css files. These are the points of contact your app needs to interact with the outside world.
Understand existing code
Leaving the resources in the Data folder for now, we need to see what this example provides for us.
Execute the game
To implement the game, you need to do a few things:
- A melonJS. If you have, make sure you get it
dist
Contents of the folder. Copy it to any folder and be sure to add it to like any other JS fileindex.html
File. - Install (if not already installed) the HTTP-server module provided in NPM, which can quickly provide HTTP services to related folders. If it is not installed, simply do the following:
$ npm install -g http-server
Copy the code
Once installed, run from the project folder:
$ http-server
Copy the code
You can test the game by visiting http://localhost:8080.
Look at the code
It’s a platformer with basic (and very awkward) action, several different enemies, and a collectible. This is basically the same goal, but the control scheme is slightly different.
The key files to check here are:
- Game.js: This file contains all the initialization code, but the interesting thing is how to instantiate the game graphics and master.
- Screens/Play.js: Contains all the code needed to set up your levels. You’ll notice there’s not much in it. Since the level definition is done using another tool (Tiled), this code simply enables this functionality.
- Entities/Player.js: Obviously this is your main goal. This file contains your character’s movement code, collision response and control key bindings. It’s not a huge scale, but it’s where you want to spend the most time.
- Entities/medimes.js: Next to the Player code, this is important because you’ll see how to set up automatic behavior based on predefined coordinates.
The rest of the files are useful, but not that important, and we’ll use them when we need them.
Know where it all came from
If you’ve done your homework, you’ve probably noticed that there’s not a single line of code that instantiates either the player or the enemy. Their coordinates are nowhere to be found. So, how to understand games?
This is where the level editor comes in. If you downloaded Tiled, you can open a file named map1.tmx in the data/map folder and see something like the following:
The center of the screen shows you the level you’re designing. If you look closely, you’ll see images and rectangular shapes, some of which have different colors and names. These objects represent things in the game, depending on their name and the layer they belong to.
On the right side of the screen, you will see a list of layers in it (top right). There are different types of layers:
- Image layer: Used for background or foreground images
- Object layer: For colliding objects, entities, and any objects you want to instantiate in the map.
- Tile layer: Where you will place tiles to create the actual level.
The lower right corner contains the map block. Tileet can also be created by Tiled and can be found in the same folder with the TSX extension.
Finally, on the left side of the screen, you’ll see the Properties section, where you’ll see details about the selected object or the layer that you clicked. You will be able to change generic properties (such as the color of a layer to better understand the location of its objects) and add custom properties that will later be passed as parameters to the constructors of entities in the game.
Change your exercise routine
Now that we’re ready to code, let’s focus on the main purpose of this article, which is to take a working version of the example and try to modify it to work as a typing game.
This means that the first thing that needs to change is the motion scheme, or in other words: change control.
Go to entities/player.js and check the init method. You’ll notice a lot of bindKey and bindGamepad calls. This code essentially ties a specific key to a logical action. In short, it ensures that whether you press the right arrow key, the D key, or move the analog stick to the right, the same “right” action will be triggered in the code.
All of this needs to be removed, which doesn’t help us. Also create a new file, call it wordservices.js, and create an object in this file that will return the word for each turn, which will help us understand which action the player chose.
/** * Shuffles array in place. * @param {Array} a items An array containing the items. */
function shuffle(a) {
var j, x, i;
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = a[i];
a[i] = a[j];
a[j] = x;
}
return a;
}
ActionWordsService = {
init: function(totalActions) {
//load words...
this.words = [
"test"."hello"."auto"."bye"."mother"."son"."yellow"."perfect"."game"
]
this.totalActions = totalActions
this.currentWordSet = []
},
reshuffle: function() {
this.words = shuffle(this.words)
},
getRegionPostfix: function(word) {
let ws = this.currentWordSet.find( ws= > {
return ws.word == word
})
if(ws) return ws.regionPostfix
return false
},
getAction: function(word) {
let match = this.getWords().find( am= > {
return am.word == word
})
if(match) return match.action
return false
},
getWords: function() {
let actions = [ { action: "right".coords: [1.0].regionPostfix: "right"},
{ action: "left".coords: [- 1.0].regionPostfix: "left"},
{ action: "jump-ahead".coords: [1.0.5].regionPostfix: "upper-right"},
{ action: "jump-back".coords: [- 1.0.5].regionPostfix: "upper-left"},
{ action: "up".coords: [0.- 1].regionPostfix: "up"}]this.currentWordSet = this.words.slice(0.this.totalActions).map( w= > {
let obj = actions.shift()
obj.word = w
return obj
})
return this.currentWordSet
}
}
Copy the code
Essentially, the service contains a list of words, which it then randomly arranges, and each time the list is requested (using the getWords method), a random set of words is picked up and assigned to one of the operations mentioned above. There are additional attributes associated with each operation:
- Based on the action HUD, the Coords property is used to place text in the correct coordinates (more on that later)
- The regionPostfix property is used to select the correct frame for the HUD operation.
Now, let’s look at how to request user input during the game.
Note: Before moving on, remember that in order for the new service to be available to the rest of the code, you must include it in the index.html file, just like any other JS library:
<script type="text/javascript" src="js/wordServices.js"></script>
Copy the code
How do I capture user input
You can potentially use combinations of key bindings to mimic the behavior of input fields using game elements, but consider all the possible combinations and behaviors provided by input fields by default (for example, paste text, select, move without removing characters, etc.), and all programs must be programmed to make them available.
Instead, we can simply add a text field to the MAIN HTML page and style it using CSS above the Canvas element, and it will become part of the game.
All you need is this code inside :
<input type="text" id="current-word" />
Copy the code
Although this is entirely up to you, I recommend that you use jQuery to simplify the code required to attach callbacks to KeyPress events. You can certainly do this using native JS, but I prefer the syntactic sugar provided by this library.
The following code, in the load method of the game.js file, is responsible for capturing user input:
me.$input = $("#current-word")
let lastWord = ' '
me.$input.keydown( (evnt) = > {
if(evnt.which == 13) {
console.log("Last word: ", lastWord)
StateManager.set("lastWord", lastWord)
lastWord = ' '
me.$input.val("")}else {
if(evnt.which > 20) {
let validChars = /[a-z0-9]+/gi
if(!String.fromCharCode(evnt.which).match(validChars)) return false
}
setTimeout(_= > {
lastWord = me.$input.val() //String.fromCharCode(evnt.which)
console.log("Partial: ", lastWord)
}, 1)
}
setTimeout((a)= > {
StateManager.set("partialWord", me.$input.val())
}, 1);
})
Copy the code
Essentially, we capture the input element and store it in the global object ME. This global variable contains everything the game needs.
This way, we can set up event handlers for any key press. As you can see, I’m checking key code 13 (which represents the ENTER key) to see when the player has finished typing, otherwise I’ll make sure they’re typing valid characters (I just avoid using special characters so that the default fonts provided by melonJS don’t work).
Finally I set up two different states on the StateManager object, with lastWord knowing the lastWord the player typed and partialWord decoding what is being typed. These two states are important.
Components share data
How to share data between components is a common problem in many frameworks. We capture the input as part of the game component, so how do we share this input with others?
My solution is to create a global component that acts as event Emitter:
const StateManager = {
on: function(k, cb) {
console.log("Adding observer for: ", k)
if(!this.observers) {
this.observers = {}
}
if(!this.observers[k]) {
this.observers[k] = []
}
this.observers[k].push(cb)
},
clearObserver: function(k) {
console.log("Removing observers for: ", k)
this.observers[k] = []
},
trigger: function(k) {
this.observers[k].forEach( cb= > {
cb(this.get(k))
})
},
set: function(k, v) {
this[k] = v
this.trigger(k)
},
get: function(k) {
return this[k]
}
}
Copy the code
The code is very simple, you can set multiple “observers” (which are callback functions) for a particular state, and once that state is set (that is, changed), all of those callbacks will be called with the new value.
Add the UI
The last step before creating the level is to display some basic UI. Because we need to show where the player can move and what words to type.
To do this, two different UI elements are used:
- One for graphics, which will have several different frames, essentially one for normal images, and then one that displays each orientation as “selected” (with
ActionWordsService
On theregionPostfix
Attribute association) - One is used to output text around the image. By the way, this is also related to
ActionWordsService
On thecoords
Attribute is associated.
We can overlay the existing hud.js file in the js folder. Add two new components to it.
The first is the ActionControl component, which looks like this:
game.HUD.ActionControl = me.GUI_Object.extend({
init: function(x, y, settings) {
game.HUD.actionControlCoords.x = x //me.game.viewport.width - (me.game.viewport.width / 2)
game.HUD.actionControlCoords.y = me.game.viewport.height - (me.game.viewport.height / 2) + y
settings.image = game.texture;
this._super(me.GUI_Object, "init", [
game.HUD.actionControlCoords.x,
game.HUD.actionControlCoords.y,
settings
])
//update the selected word as we type
StateManager.on('partialWord', w => {
let postfix = ActionWordsService.getRegionPostfix(w)
if(postfix) {
this.setRegion(game.texture.getRegion("action-wheel-" + postfix))
} else {
this.setRegion(game.texture.getRegion("action-wheel")}this.anchorPoint.set(0.5.1)})//react to the final word
StateManager.on('lastWord', w => {
let act = ActionWordsService.getAction(w)
if(! act) { me.audio.play("error".false);
me.game.viewport.shake(100.200, me.game.viewport.AXIS.X)
me.game.viewport.fadeOut("#f00".150.function(){})}else {
game.data.score += Constants.SCORES.CORRECT_WORD
}
})
}
})
Copy the code
It seems like a lot, but it just does a little bit:
- It is from
settings
Property, we will check it after setting the map on the Tiled. - Add code that responds to partially typed words. We will be
postfix
Property for the word currently written. - And added code to react to the full word. If an action is associated with the word (that is, the correct word), it gives the player points. Otherwise, the screen will shake and the wrong sound will play.
The second graphic section, the words to be entered, looks like this:
game.HUD.ActionWords = me.Renderable.extend({
init: function(x, y) {
this.relative = new me.Vector2d(x, y);
this._super(me.Renderable, "init", [
me.game.viewport.width + x,
me.game.viewport.height + y,
10.//x & y coordinates
10
]);
// Use screen coordinates
this.floating = true;
// make sure our object is always draw first
this.z = Infinity;
// create a font
this.font = new me.BitmapText(0.0, {
font : "PressStart2P".size: 0.5.textAlign : "right".textBaseline : "bottom"
});
// recalculate the object position if the canvas is resize
me.event.subscribe(me.event.CANVAS_ONRESIZE, (function(w, h){
this.pos.set(w, h, 0).add(this.relative);
}).bind(this));
this.actionMapping = ActionWordsService.getWords()
},
update: function() {
this.actionMapping = ActionWordsService.getWords()
return true
},
draw: function(renderer) {
this.actionMapping.forEach( am= > {
if(am.coords[0] = =0 && am.coords[1] = =1) return
let x = game.HUD.actionControlCoords.x + (am.coords[0] *80) + 30
let y = game.HUD.actionControlCoords.y + (am.coords[1] *80) - 30
this.font.draw(renderer, am.word, x, y)
})
}
})
Copy the code
The heavy lifting of this component is done through the DRAW method. The init method simply initializes the variable. During the call to DRAW, we will iterate over the selected word and position the word around the coordinates of the ActionControl component using the coordinates associated with it and a set of fixed numbers.
Here’s what the proposed motion control design looks like (and how the coordinates relate to it) :
Of course, it should have a transparent background.
Just make sure to save these images in the /data/img/ Assets /UI folder so that when you open TexturePacker it will recognize the new image and add it to the texture atlas.
The image above shows how to add a new image for the Action Wheel. You can then click “Publish Sprite Sheet” and accept all the default options. It overwrites the existing atlas, so you don’t need to do anything for your code. This step is critical because the texture atlas will be loaded as a resource (more on that in a minute) and multiple entities will use it for things like animations. Remember to do this whenever you add or update graphics to your game.
Put everything together with Tiled
Ok, now that we’ve covered the basics, let’s play the game. The first thing to notice: maps.
By using tiled and the default tileet included with melonJS, I created this map (25×16 tiles map, where tiles are 32 x 32px) :
These are the layers I’m using:
- HUD: It contains only one element named hud. ActionControl (it’s important to keep the name the same, you’ll see why in a moment). The following figure shows the attributes of this element (note the custom attributes)
- Collision: By default, melonJS will put the
collision
All of the initial layers are assumed to be collision layers, which means that any shapes in them are not traversable. Here you will define all the shapes of the floor and platform. - Player: This layer contains only the mainPlayer element (a shape that will let melonJS know where to place the player at the start of the game).
- Entities: In this layer I have added coins again, their names are important, please be consistent as they need to match the names you have registered in your code.
- The last three layers can be filled with maps and background images.
Once we’re ready, we can go to the game.js file and add the following lines to the loaded method:
// register our objects entity in the object pool
me.pool.register("mainPlayer", game.PlayerEntity);
me.pool.register("CoinEntity", game.CoinEntity);
me.pool.register("HUD.ActionControl", game.HUD.ActionControl);
Copy the code
This code is used to register your entities (you use Tiled to place entities directly on the map). The first parameter provides the name you need to match with Tiled.
Also, in this file, the onLoad method should look like this:
onload: function() {
// init the video
if(! me.video.init(965.512, {wrapper : "screen".scale : "auto".scaleMethod : "fit".renderer : me.video.AUTO, subPixel : false })) {
alert("Your browser does not support HTML5 canvas.");
return;
}
// initialize the "sound engine"
me.audio.init("mp3,ogg");
// set all ressources to be loaded
me.loader.preload(game.resources, this.loaded.bind(this));
ActionWordsService.init(5)},Copy the code
Our base requirement was 965×512 resolution (I found it worked well when the screen was at the same height as the map. After 16*32 = 512 in our example), the ActionWordsService will be initialized with five words (these are the five directions you can go in).
Another interesting piece of code in the onLoad method is:
me.loader.preload(game.resources, this.loaded.bind(this));
Copy the code
Resource file
All types of resources required by the game (i.e. images, sounds, background music, JSON configuration files, etc.) need to be added to the resources.js file.
This is the contents of your resource file:
game.resources = [
{ name: "tileset".type:"image".src: "data/img/tileset.png" },
{ name: "background".type:"image".src: "data/img/background.png" },
{ name: "clouds".type:"image".src: "data/img/clouds.png" },
{ name: "screen01".type: "tmx".src: "data/map/screen01.tmx" },
{ name: "tileset".type: "tsx".src: "data/map/tileset.json" },
{ name: "action-wheel".type:"image".src: "data/img/assets/UI/action-wheel.png" },
{ name: "action-wheel-right".type:"image".src: "data/img/assets/UI/action-wheel-right.png" },
{ name: "action-wheel-upper-right".type:"image".src: "data/img/assets/UI/action-wheel-upper-right.png" },
{ name: "action-wheel-up".type:"image".src: "data/img/assets/UI/action-wheel-up.png" },
{ name: "action-wheel-upper-left".type:"image".src: "data/img/assets/UI/action-wheel-upper-left.png" },
{ name: "action-wheel-left".type:"image".src: "data/img/assets/UI/action-wheel-left.png" },
{ name: "dst-gameforest".type: "audio".src: "data/bgm/" },
{ name: "cling".type: "audio".src: "data/sfx/" },
{ name: "die".type: "audio".src: "data/sfx/" },
{ name: "enemykill".type: "audio".src: "data/sfx/" },
{ name: "jump".type: "audio".src: "data/sfx/" },
{ name: "texture".type: "json".src: "data/img/texture.json" },
{ name: "texture".type: "image".src: "data/img/texture.png" },
{ name: "PressStart2P".type:"image".src: "data/fnt/PressStart2P.png" },
{ name: "PressStart2P".type:"binary".src: "data/fnt/PressStart2P.fnt"}];Copy the code
You can use things like block sets, screen maps, etc. (Note that the name is always a filename without an extension, this is mandatory, otherwise the resource will not be found).
COINS
Coins in the game are pretty simple, but something needs to happen when you collide with them, and their code looks like this:
game.CoinEntity = me.CollectableEntity.extend({
/** * constructor */
init: function (x, y, settings) {
// call the super constructor
this._super(me.CollectableEntity, "init", [
x, y ,
Object.assign({
image: game.texture,
region : "coin.png"
}, settings)
]);
},
/** * collision handling */
onCollision : function (/*response*/) {
// do something when collide
me.audio.play("cling".false);
// give some score
game.data.score += Constants.SCORES.COIN
//avoid further collision and delete it
this.body.setCollisionMask(me.collision.types.NO_OBJECT);
me.game.world.removeChild(this);
return false; }});Copy the code
Note that the coin entity actually extends CollectibleEntity (this gives it a special collision type to the entity, so melonJS knows to call a collision handler when the player moves over it). All you have to do is call its parent constructor, Then when you pick it up, the onCollision method plays the sound, adds one to the global score, and finally removes the object from the world.
The finished product
Put it all together and you have a working game that lets you move in five different directions based on the words you type.
It should look something like this:
And since this tutorial is already too long, you can check out the full code for the game on Github.