npm init && npm i phaser
And then the build tools...
npm i -D parcel parcel-reporter-static-files-copy
make a static
folder for assets and then create a .parcelrc
file in the root of your project folder and add the following into it.
{
"extends": ["@parcel/config-default"],
"reporters": ["...", "parcel-reporter-static-files-copy"]
}
Create a dist
folder a src
folder an src/index.html
and a src/main.js
and load the javascript file into the html file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Game Demo</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
</style>
</head>
<body>
<main id="root"></main>
<script type="module" src="https://github.com/MultiverseLearningProducts/community-gaming-workshop-part-1/raw/master/main.js"></script>
</body>
</html>
In your package.json
add a "source" key that contains the path to your index.html
then add a script to run parcel i.e. npm start
{
"...": "...other package.json stuff",
"source": "src/index.html",
"scripts": {
"start": "parcel",
},
"...": "...other package.json stuff",
}
// src/main.js
import Phaser from 'phaser'
function preload () {
}
function create () {
}
function update () {
}
new Phaser.Game({
type: Phaser.AUTO,
width: 16 * 64,
height: 12 * 64,
backgroundColor: '#888',
physics: {
default: 'matter',
matter: {
gravity: { y: 0 },
debug: false
}
},
scene: { preload, create, update }
})
I'm using this free pack of the Fallen Angel. When you download this you get an asset pack, all the frames are stored as individual .png. We will want to compile all the frames we want to work with into a single sprite sheet, and then create a mapping of that sheet. I am using a simple online service to facilitate this Sprite Sheet Packer. I have uploaded 2 different folders of images to make my idle animation sheet.
In the preload
function load your sprites, the arguments below are
idle
/idle.png
idle.json
file that acts like a map or atlas for the spritesheetthis.load.atlas('idle', '/idle.png', '/idle.json')
Access the sprites in the create
function and map the animation frames.
console.log(this.textures.get('idle').getFrameNames())
Anoying they don't have a consistent naming pattern. I might rewrite the frame names with a script like this:
const frames = require('./static/idle.json')
const path = require('path')
const fs = require('fs')
const renamed = frames.frames.map((frame, index) => Object.assign(frame, {filename: `angel_${index < 10 ? "0" : ""}${index}.png`}))
fs.writeFileSync(path.join(__dirname, "static", "idle_renamed.json"), JSON.stringify({...frames, frames: renamed}))
Now back in the create
function we only need to do 3 things:
this.anims.create({
key: 'idle',
frames: this.anims.generateFrameNames('idle', {
start: 0,
end: 35,
prefix: "angel_",
zeroPad: 2,
suffix: ".png"
}),
frameRate: 12,
repeat: -1
})
this.angel = this.add.sprite(100, 200, 'angel')
this.angel.play('idle')
Wow, she is enormous. We can down scale her. Update the line where we add the sprite.
this.angel = this.add.sprite(100, 200, 'angel').setScale(0.25)
Now you see her idling don't you want to be able to move her? The programming tasks we are going to build out now are similar to what we have done above. The next thing now is starting to use the update function. Our game has an tick. Like a clock ticks every second, our game will run the update function 60 times a second. This is where the idea of a 'game engine' comes from this ticking, a programming pattern that will repeatedly call a function like the ticking of an engine. Action will be driven by these update functions. We are going to be checking for keyboard inputs, and then updating our character to have momentum that relates to a key press direction and an animation that does the same.
Can you figure out how to do this from the steps you have already seen?
You should be able to add the other animations, i.e. 'right-walk'. Below is the code to initialise the keyboard listeners. To apply velocity the sprite needs to be attached to the physics engine. Instead of this.add.sprite(100, 200, 'angel').setScale(0.25)
. You will need to use this.physics.add.sprite(100, 200, 'angel').setScale(0.25)
see there we are adding the sprite to the this
context of the game via the physics
engine. Now we'll be able to apply forces to our sprite.
this.cursors = this.input.keyboard.createCursorKeys()
this.angel = this.matter.add.sprite(100, 200, 'angel').setScale(0.25)
if (this.cursors.right.isDown) {
this.angel.flipX = false
this.angel.setVelocityX(300)
this.angel.play('right_walk', true)
} else if (this.cursors.left.isDown) {
this.angel.setVelocityX(-300)
this.angel.play('right_walk', true)
this.angel.flipX = true
} else {
this.angel.setVelocityX(0)
this.angel.play('idle', true)
}
Do you think you can make her jump? then slash, throw etc
Finally before leaving the character lets think a little more about the container of the world in which she lives. We can apply forces to our character that simulate gravity and this can help the feeling of being in a more realistic environment. In the game config can you see the debug
flag. At the moment it is set to be false, can you switch it to be true?
Now you can see the physics acting on the sprite. We are going to take a moment to appreciate a few things now about the bounding box around a sprite and the way that physics acts on bodies in the world. We can start by applying gravity to the scene. In the game config, give the y
value something like 5.
new Phaser.Game({
type: Phaser.AUTO,
width: 16 * 64,
height: 12 * 64,
backgroundColor: '#888',
physics: {
default: 'arcade',
arcade: {
gravity: { y: 5 },
debug: true
}
},
scene: { preload, create, update }
})
Now our playable character just falls out of the world. For now we can bound her in by adding this.matter.world.setBounds(0, 0, 16 * 64, 12 * 64)
to the create function. In the table below I'm trying to communicate to you the ways setOrigin and setSize effect the physical positioning of the character and the way it will effect the collide and interaction with other physical bodies in the world.
Here is the initial config
this.angel = this.physics.add.sprite(100, 200, 'idle')
.setScale(0.25)
.setRectangle(120,150)
this.matter.world.setBounds(0, 0, 16 * 64, 12 * 64)
To create a world that our player can start to explore I have been using a free program called Tiled. This is were we can design our level, the world for our character to explore. Download yourself a copy and find a 2D tileset to start working with. This is what I'm using.
Look at the tile size for the artwork you have downloaded. These packs are 32 x 32. When you open Tiled create a new map and set the tile size to match your artpack, and decide the size of map you want to create. I'm creating a long level so 72 tiles wide and 12 tiles high.
Now we are ready to start building our world in the form of a tiled map. Your tileset will give you pieces of artwork that you can piece together like a jigsaw puzzle. Use layers to seperate elements in your design, you will end up importing the map layer by layer in your code.
Now, pop on some tunes and get creative!
When you are done or done enough to start testing your map goto File -> Export As -> filename.json and export as a json file, you need to serve the file to your frontend code so save it into your public folder or in this project it will go in the static
folder. Any tileset artwork also needs to be served to your frontend and also needs to be in the static
folder.
We need both the json mapping and the artwork. We load these in the preload function, then we'll create the map
instance and then add all the layers one by one. Below is an example of the preload function I needed. Can you spot the Tiled json file being loaded?
function preload () {
this.load.atlas('idle', '/idle.png', '/idle.json')
this.load.atlas('move', '/move.png', '/move.json')
this.load.tilemapTiledJSON('tilemap', '/moon-level.json')
this.load.image('bg0', '/tileset_artwork/Background_0.png')
this.load.image('bg1', '/tileset_artwork/Background_1.png')
this.load.image('bg_brush', '/tileset_artwork/brush.png')
this.load.image('bg_grass_1', '/tileset_artwork/Grass_background_1.png')
this.load.image('bg_grass_2', '/tileset_artwork/Grass_background_2.png')
this.load.image('bg_tiles', '/tileset_artwork/Tiles.png')
}
In the create function we will make our tilemap it's quite fun to have a look at the generated json, you should be able to guess whats going on behind the scenes in the phaser code to join the artwork with our json map.
const map = this.make.tilemap({key: 'tilemap'})
// 'Background_0' comes from the moon-level.json - 'bg0' is my label set in the preload function
const bg1 = map.addTilesetImage('Background_0', 'bg0')
const bgFence = map.addTilesetImage('Background_1', 'bg1')
const bush = map.addTilesetImage('brush', 'bg_brush')
const grass_1 = map.addTilesetImage('Grass_background_1', 'bg_grass_1')
const grass_2 = map.addTilesetImage('Grass_background_2', 'bg_grass_2')
const tiles = map.addTilesetImage('Tiles', 'bg_tiles')
// The arrays are all the art sheets a layer needs
map.createLayer('Moon-sky', [bg1], 0, 0)
map.createLayer('Bg-fence', [bg1, bgFence], 0, 0)
map.createLayer('bush', [bush, grass_1, grass_2, tiles], 0, 0)
map.createLayer('path', [tiles], 0, 0)
map.createLayer('foreground-dressing', [bgFence, bush, grass_1, grass_2, tiles], 0, 0)
The code above is in three stages.
createLayer
The 0, 0
in createLayer
is the x and y coordinate to position the layer. Now you should see your Angel character rendered along with the tilemap. The feeling of our world is coming together. The final step for this workshop is to start connecting these 2 things together, the world and the character.
To stop the character falling through the world we need to create obstructive contact or cause the character to collide and stop on the path. For this I'm going back to the Tiled map and going to create not a tiled layer by an object layer. On this object layer we are going to create an invisible polygon (multi sided shape) that will define areas our character will be obstructed by.
Use the polygon tool to click and create a shape. Click on the original point to complete the polygon. In the properties section (top left hand corner) give the shape a name to reference in your code later.
Save the updated tilemap, re export as json. Now you can bring this polygon shape into your game code. First we'll find the object we just name (using it's name) then reformat the line data from this:
[
{
"x":0,
"y":0
},
{
"x":556.655665566557,
"y":4.40044004400443
},
{
"x":556.655665566557,
"y":-125.412541254125
}
]
To data that looks like this:
"0 0 556.655665566557 4.40044004400443 556.655665566557 -125.412541254125"
That what coords
ends up being a string of x, y values separated by spaces.
const platformOcclusion = map.findObject('occlusions', obj => obj.name === 'platform')
const coords = platformOcclusion.polygon.map(({x, y}) => ([x, y])).flat().join(" ")
const platformPolygon = this.add.polygon(platformOcclusion.x, platformOcclusion.y, coords, 0xff66ff, 0.2).setOrigin(0, 0)
this.matter.add.gameObject(platformPolygon, {
isStatic: true,
shape: {
type: 'fromVerts',
verts: coords,
flagInternal: true
}
}).setVisible(false).setPosition(
platformPolygon.x + platformPolygon.body.centerOffset.x,
platformPolygon.y + platformPolygon.body.centerOffset.y
)
Reading the code we get our data from the tilemap, reformat it, create a polygon shape, then we attach matter physics to that shape by using it to create a gameObject
. The chaining functions render the shape invisible, and adjust it's position because the center of the gameObject is calculated by center of mass not geometrically. So we overide that and position the shape as if the origin was it's top left-hand corner.
Can you update the character to jump? Now we have obstacles we can climb onto! You can be more fancy and add the jumping animation that is in the artwork if you want to. Just for functionality I'm going to just listen for the up arrow key and add some vertical velocity in the update function
if(this.cursors.up.isDown) {
this.angel.setVelocityY(-12)
}
The level is longer that the viewport, so we can travel into the map to manage that we just need to get the camera to follow the character as we move, and offset it so the map does not appear to moved when when the camera focuses on the character. Add these 2 lines to the create function. You might need to adjust the bounds of the world depending on your map's width and height.
this.cameras.main.setBounds(0, 0, 64 * 72, 64 * 12)
this.cameras.main.startFollow(this.angel)
We have built a game from scratch. A playable character and a world for them to explore. Finally we have introduced collision and cameras. These tricks enable us to establish a world. In the next workshop we will need to think more about game state and starting to manage more intricate interactions.