This tutorial will show you how with the help of the popular HTML5 game framework Phaser and MightyEditor you can create your own game prototype. This tutorial will take about an hour and ~140 lines of code.
In this tutorial we will learn more about:
- tiled maps
- switching rooms
- groups
Requirements
The latest Google Chrome browser. Others are not tested and optimized.
This tutorial also assumes, that you have a basic knowledge about MightyEditor and Phaser, thus the most basic terms won’t be explained in too much of a detail.
Previous tutorials: Digger and SockMan.
Creating the project
First, go to MightyEditor, click on “Create New Project” and fill in a name for the project. “Dungeon”, in this case.
Next, locate the Settings panel in the bottom-right corner of the screen, and change the world and viewport dimensions to 512px – the dimensions of one room:
Creating the map
Download and extract the game assets and then upload them to the editor.
Then it’s time to create the tilemap. We will begin with the starting room. First, set the frame dimensions of ‘dungeon_tiles.png’ spritesheet to 32px:
The map of this tutorial will consist of 9 rooms. For the first room, create a new tile layer naming it “map00” (tilemap tutorial) with the following parameters (in the Settings panel):
- widthInTiles – 16;
- heightInTiles – 16;
- tileWidth – 32;
- tileHeight – 32.
The “00” in the name stands for the location of this room in the room array (there will be more rooms).
Use the tile tool to create a map like this:
As for the other rooms, you have 2 choices: either hide this one (the ‘eye’ icon to the left of object name) or create all rooms next to each other and then change their location coordinates to 0. Either way – remember to name the room tilemaps according to their location in the map array (created later) like “mapYX”, where the Y is index on the Y axis (0 is on top) and X is index on the X axis, so that “map21” is accessed in the array as map[2][1]. This tutorial creates a 3×3 level, but you can make it as big as you want (just change the array size accordingly) or even leave some of the rooms empty if you want. Here it looks like this:
Remember to change the coordinates of every room to X=0 and Y=0.
Adding player character
First, change the frame dimensions of “character.png” spritesheet to 50×50 pixels, anchorX to 0.5 and anchorY to 0.7.
Place the character object somewhere on “map00” (because it’s the starting room), name it “character” and in its physics settings set these values:
- enabled: 1
- immovable: 0
- size – width & height: 30
- bounce – x & y: 0
- gravity allow: 0
The anchorY of 0.7 and body size of 30×30 pixels means that any collision or overlapping will occur only with the feet of the character.
Opening the game now would give you the starting room and character.
The basic gameplay
Switch to the source editor and go to the demo state (js/state/demo.js). Everything will be done here.
create
Delete everything in the create method and instead add this:
create: function() { this.level = []; for (var i = 0; i < 3; i++) { this.level[i] = []; for (var j = 0; j < 3; j++) { this.level[i][j] = { map: null, visited: false }; } } this.location = { x: 0, y: 0 }; this.createRoom(); this.character = mt.create("character"); this.character.goingUp = false; this.character.goingDown = false; this.character.goingLeft = false; this.character.goingRight = false; this.cursors = this.game.input.keyboard.createCursorKeys(); },
Here, we first create a 2D array to store the rooms of the level, where each element is an object consisting of two members:
- map: the tilemap will be stored here, when created;
- visited: a boolean flag for checking, whether this room has already been visited.
Then create a location object with two members representing the current location of the character in 2D array, call the createRoom() function (more on that in the next step) and create the character and 4 members who help the game decide, which room to draw next based on the direction in which the player is going.
And finally, we create character controls (arrow keys), although the actual controlling will be done in the update method.
createRoom
Add the createRoom function below the create method like this:
createRoom: function() { if (this.level[this.location.y][this.location.x].visited === false) { this.level[this.location.y][this.location.x].map = this.game.add.group(); this.level[this.location.y][this.location.x].map.add(mt.create("map" + this.location.y + this.location.x)); this.level[this.location.y][this.location.x].map.children[0].map.setCollisionByExclusion([10, 12]); this.level[this.location.y][this.location.x].visited = true; } else { this.level[this.location.y][this.location.x].map.visible = true; } },
In this function we check on whether this room has already been visited:
- if not visited: creates a Phaser group and, based on the character location in the array, creates the needed tilemap. After that, sets collision on all tiles EXCEPT for tiles with indexes of 10 and 12 and finally – flags this room as visited;
- if has been visited: does no create this room from scratch, instead simply makes it visible again (upon leaving this room, it will be made invisible).
update
Now, add an update method below create. This method is constantly looping itself at around 60 frames per second and is the place where the magic happens – physics checks, movement, changing rooms etc.
In here, add this code:
update: function() { this.game.physics.arcade.collide(this.character, this.level[this.location.y][this.location.x].map.children[0]); if (this.cursors.up.isDown) { this.character.body.velocity.y = -200; } else if (this.cursors.down.isDown) { this.character.body.velocity.y = 200; } else { this.character.body.velocity.y = 0; } if (this.cursors.right.isDown) { this.character.body.velocity.x = 200; } else if (this.cursors.left.isDown) { this.character.body.velocity.x = -200; } else { this.character.body.velocity.x = 0; } if (this.character.y > 492) { this.character.goingDown = true; this.changeRoom(); } else this.character.goingDown = false; if (this.character.y < 20) { this.character.goingUp = true; this.changeRoom(); } else this.character.goingUp = false; if (this.character.x > 492) { this.character.goingRight = true; this.changeRoom(); } else this.character.goingRight = false; if (this.character.x < 20) { this.character.goingLeft = true; this.changeRoom(); } else this.character.goingLeft = false; },
In this method, the game first checks for collision with those tilemap tiles, which are flagged as having collision.
Then there are the movement controls – changing the velocity of character depending on key pressed.
And finally we check, whether the character is nearing the edges of this room and, if it is, calls the changeRoom() function.
changeRoom
Finally, we add the changeRoom() function below createRoom:
changeRoom: function() { this.level[this.location.y][this.location.x].map.visible = false; if (this.character.goingDown) { this.location.y += 1; this.character.y = 20; } if (this.character.goingUp) { this.location.y -= 1; this.character.y = 492; } if (this.character.goingRight) { this.location.x += 1; this.character.x = 20; } if (this.character.goingLeft) { this.location.x -= 1; this.character.x = 492; } this.createRoom(); this.character.bringToTop(); }
This function, when called upon moving the character towards a door, first makes the current room invisible, so its visuals can’t interfere with the room we are moving to (remember, that all rooms are located at the same coordinates on the map). Then, depending on the direction our character is moving, changes its coordinates in the array by 1 on either the Y or X axis. Finally, a new room is created and the character sprite is brought to top, so it is above any other game objects.
The whole basic code
"use strict"; window.Dungeon.state.demo = { create: function() { this.level = []; for (var i = 0; i < 3; i++) { this.level[i] = []; for (var j = 0; j < 3; j++) { this.level[i][j] = { map: null, visited: false }; } } this.location = { x: 0, y: 0 }; this.createRoom(); this.character = mt.create("character"); this.character.goingUp = false; this.character.goingDown = false; this.character.goingLeft = false; this.character.goingRight = false; this.cursors = this.game.input.keyboard.createCursorKeys(); }, update: function() { this.game.physics.arcade.collide(this.character, this.level[this.location.y][this.location.x].map.children[0]); if (this.cursors.up.isDown) { this.character.body.velocity.y = -200; } else if (this.cursors.down.isDown) { this.character.body.velocity.y = 200; } else { this.character.body.velocity.y = 0; } if (this.cursors.right.isDown) { this.character.body.velocity.x = 200; } else if (this.cursors.left.isDown) { this.character.body.velocity.x = -200; } else { this.character.body.velocity.x = 0; } if (this.character.y > 492) { this.character.goingDown = true; this.changeRoom(); } else this.character.goingDown = false; if (this.character.y < 20) { this.character.goingUp = true; this.changeRoom(); } else this.character.goingUp = false; if (this.character.x > 492) { this.character.goingRight = true; this.changeRoom(); } else this.character.goingRight = false; if (this.character.x < 20) { this.character.goingLeft = true; this.changeRoom(); } else this.character.goingLeft = false; }, createRoom: function() { if (this.level[this.location.y][this.location.x].visited === false) { this.level[this.location.y][this.location.x].map = this.game.add.group(); this.level[this.location.y][this.location.x].map.add(mt.create("map" + this.location.y + this.location.x)); this.level[this.location.y][this.location.x].map.children[0].map.setCollisionByExclusion([10, 12]); this.level[this.location.y][this.location.x].visited = true; } else { this.level[this.location.y][this.location.x].map.visible = true; } }, changeRoom: function() { this.level[this.location.y][this.location.x].map.visible = false; if (this.character.goingDown) { this.location.y += 1; this.character.y = 20; } if (this.character.goingUp) { this.location.y -= 1; this.character.y = 492; } if (this.character.goingRight) { this.location.x += 1; this.character.x = 20; } if (this.character.goingLeft) { this.location.x -= 1; this.character.x = 492; } this.createRoom(); this.character.bringToTop(); } };
Now you can open the game and explore your mini-dungeon.
But that’s not all – a game needs a goal!
Adding coins and a goal
Return to the map editor and select the ‘coin_small.png’ asset. Change both of its anchors to 0.5, set its frame dimensions to 25×25 pixels and enable physics for this sprite.
Now place the coins wherever you want in your rooms, similarly to how the rooms were created. Remember to group the coins between rooms, so, for “map11”, the coin group would be called “coins11”.
Note – at the time of writing, if a tilemap object is included in a group and later created with mt.create(), the tilemap, even though it is rendered, it isn’t listed as a children of that group, thus collision checking becomes impossible. That is why coin groups and room tilemaps should be separate objects and grouped together manually in source editor – then tilemaps are properly listed as children of that group.
Also place the “stairs.png” sprite in of the rooms (with enabled physics) – those will serve as the end point of the level.
After that has been done, your level should resemble this to some degree:
Remember to the coordinates of all groups and tilemaps to 0, so they overlap each other. The coordinates of stairs should be x=224, y=224.
Finally add a text object with ‘0’ as its text, name it “coinText” add place it in the top-right corner of the room.
Adding the final parts of code
Character animation
In the create method add these lines after creating the character:
this.character.animations.add('idle', [4, 5, 6, 7], 10, true); this.character.animations.add('run', [0, 1, 2, 3], 10, true); this.character.animations.play('idle');
This defines the frames used for each animation and starts the default animation.
Then, in the update method, change the block of code responsible for movement to this:
if (this.cursors.up.isDown) { this.character.body.velocity.y = -200; this.character.animations.play('run'); } else if (this.cursors.down.isDown) { this.character.body.velocity.y = 200; this.character.animations.play('run'); } else { this.character.body.velocity.y = 0; } if (this.cursors.right.isDown) { this.character.body.velocity.x = 200; this.character.animations.play('run'); this.character.scale.x = 1; } else if (this.cursors.left.isDown) { this.character.body.velocity.x = -200; this.character.animations.play('run'); this.character.scale.x = -1; } else { this.character.body.velocity.x = 0; } if (this.character.body.velocity.x === 0 && this.character.body.velocity.y === 0) { this.character.animations.play('idle'); }
This plays an animation depending on the conditions of movement.
You can also read learn about animations here.
Coins and stairs
The coin group for each room is created upon entering a new room just like tilemaps, thus add these lines in the createRoom function just after the group is created:
this.level[this.location.y][this.location.x].map.add(mt.create("coins" + this.location.y + this.location.x)); this.level[this.location.y][this.location.x].map.children[1].callAll('animations.add', 'animations', 'idle', [0, 1, 2, 3], 10, true); this.level[this.location.y][this.location.x].map.children[1].callAll('animations.play', 'animations', 'idle'); if (this.location.y === 0 && this.location.x === 2) { this.level[this.location.y][this.location.x].map.add(mt.create("stairs")); }
This adds the coin group to the same group which contains the tilemap for the room you have just entered. The animations are also created and and begin to play for each coin in this group.
It is important to keep a single sequence of objects when adding them to a group, because when they have been added, the objects become children of this group so, in this tutorial, wherever something like
this.level[this.location.y][this.location.x].map.children[1]
appears, the index next to ‘children’ means: 0=tilemap, 1=coin group, 2=stairs.
Finally, we create the stair object if your character has entered the room which is supposed to be the end room (map02 in this case).
Before we add the final collision functions in the update method, we must create our text object so we can keep track of how many coins have been collected, hence, in the create method add this line:
this.coinText = mt.create("coinText");
And, to keep the number always visible, we must bring it on top of any other objects whenever we are changing rooms. So, in the changeRoom function add:
this.world.bringToTop(this.coinText);
Finally, in the update method add these two functions:
this.game.physics.arcade.overlap(this.character, this.level[this.location.y][this.location.x].map.children[1], function(character, coin) { coin.destroy(); var newCoins = parseInt(this.coinText._text) + 1; this.coinText.setText(newCoins); }, null, this); if (this.level[this.location.y][this.location.x].map.children[2]) { this.game.physics.arcade.collide(this.character, this.level[this.location.y][this.location.x].map.children[2], function() { if (this.coinText._text >= 30) { this.game.state.start("demo"); } }, null, this); }
The overlap function checks if the character sprite is overlapping a coin in the ‘coinsXY’ group and if it is, destroys the coin and adds +1 to the score (text objects are of char type, so parseInt is needed to convert the inner text to an integer and apply math to the variable).
And the collision function checks for collision between character and stairs. If you have collected at least 30 coins, you win and get transported to the beginning of this game state (everything is reset).
Final code
Congratulations! The game should now be fully playable!
Your code should now resemble this:
"use strict"; window.Dungeon.state.demo = { create: function() { //creates 2D array for keeping the rooms this.level = []; for (var i = 0; i < 3; i++) { this.level[i] = []; for (var j = 0; j < 3; j++) { this.level[i][j] = { map: null, visited: false }; } } //character location relative to array coordinates this.location = { x: 0, y: 0 }; this.createRoom(); //creates first room this.character = mt.create("character"); this.character.goingUp = false; this.character.goingDown = false; this.character.goingLeft = false; this.character.goingRight = false; //adds animations (key, array of frame IDs, frames per second, loop) this.character.animations.add('idle', [4, 5, 6, 7], 10, true); this.character.animations.add('run', [0, 1, 2, 3], 10, true); this.character.animations.play('idle'); this.coinText = mt.create("coinText"); //enables arrow keys as input this.cursors = this.game.input.keyboard.createCursorKeys(); }, update: function() { //adds collision between character and wall tiles this.game.physics.arcade.collide(this.character, this.level[this.location.y][this.location.x].map.children[0]); //ensures coin collection and their subsequent destruction this.game.physics.arcade.overlap(this.character, this.level[this.location.y][this.location.x].map.children[1], function(character, coin) { coin.destroy(); var newCoins = parseInt(this.coinText._text) + 1; //increases score for each coin collected this.coinText.setText(newCoins); }, null, this); //if this room contains stairs, checks for collision with them if (this.level[this.location.y][this.location.x].map.children[2]) { this.game.physics.arcade.collide(this.character, this.level[this.location.y][this.location.x].map.children[2], function() { //if at least 30 coins have been collected, resets this game state if (this.coinText._text >= 30) { this.game.state.start("demo"); } }, null, this); } //movement controls if (this.cursors.up.isDown) { this.character.body.velocity.y = -200; this.character.animations.play('run'); } else if (this.cursors.down.isDown) { this.character.body.velocity.y = 200; this.character.animations.play('run'); } else { this.character.body.velocity.y = 0; } if (this.cursors.right.isDown) { this.character.body.velocity.x = 200; this.character.animations.play('run'); this.character.scale.x = 1; } else if (this.cursors.left.isDown) { this.character.body.velocity.x = -200; this.character.animations.play('run'); this.character.scale.x = -1; } else { this.character.body.velocity.x = 0; } if (this.character.body.velocity.x === 0 && this.character.body.velocity.y === 0) { this.character.animations.play('idle'); } //checks whether character is nearing the edge of a room //if is, flags the direction in which to change rooms and changes the room if (this.character.y > 492) { this.character.goingDown = true; this.changeRoom(); } else this.character.goingDown = false; if (this.character.y < 20) { this.character.goingUp = true; this.changeRoom(); } else this.character.goingUp = false; if (this.character.x > 492) { this.character.goingRight = true; this.changeRoom(); } else this.character.goingRight = false; if (this.character.x < 20) { this.character.goingLeft = true; this.changeRoom(); } else this.character.goingLeft = false; }, createRoom: function() { //if the new room hasn't been visited before, creates it from scratch //based on the current location (relative to the array) of your character if (this.level[this.location.y][this.location.x].visited === false) { this.level[this.location.y][this.location.x].map = this.game.add.group(); this.level[this.location.y][this.location.x].map.add(mt.create("map" + this.location.y + this.location.x)); this.level[this.location.y][this.location.x].map.add(mt.create("coins" + this.location.y + this.location.x)); //adds animations for this coin group this.level[this.location.y][this.location.x].map.children[1].callAll('animations.add', 'animations', 'idle', [0, 1, 2, 3], 10, true); this.level[this.location.y][this.location.x].map.children[1].callAll('animations.play', 'animations', 'idle'); //if the end room is reached, adds stairs as well if (this.location.y === 0 && this.location.x === 2) { this.level[this.location.y][this.location.x].map.add(mt.create("stairs")); } //flags for collision all tiles EXCEPT for those with IDs of 10 and 12 this.level[this.location.y][this.location.x].map.children[0].map.setCollisionByExclusion([10, 12]); //once room is created, flags it as visited this.level[this.location.y][this.location.x].visited = true; } else { //if this room has been visited before, flags it as such and simply makes it visible again //so that all changes made in a previous visit are still there this.level[this.location.y][this.location.x].map.visible = true; } }, changeRoom: function() { //sets the visibility of the room you are currently leaving to invisible this.level[this.location.y][this.location.x].map.visible = false; //depending of the direction you are going, updates te location of character relative to the array //sets the character location so it appears as if he is walking in the newly created room if (this.character.goingDown) { this.location.y += 1; this.character.y = 20; } if (this.character.goingUp) { this.location.y -= 1; this.character.y = 492; } if (this.character.goingRight) { this.location.x += 1; this.character.x = 20; } if (this.character.goingLeft) { this.location.x -= 1; this.character.x = 492; } this.createRoom(); //creates the new room //brings these objects to top this.character.bringToTop(); this.world.bringToTop(this.coinText); } };
What is the “ID”?
how do you know what the ID is?