logo My Digital Garden

Learn Javascript the fun way, Build a Game

By James Kolean on Nov 18, 2020
Source repository: https://gitlab.com/jameskolean/first-phaser3-game
GameJavaScript
banner

Here is a fun way to practice our programming skills; let’s build a simple game. We will use Phaser3, a Javascript ‘framework’ for creating all sorts of games.

Game Screenshot

Play ‘game’ is here

Prerequisites

You will need these to install before you start.

  • NodeJS
  • NPM
  • VSCode (or other IDE)

Set up the project.

Let’s set up a boilerplate Javascript project with the Parcel Bundler (so we can use modern Javascript), ESLint (so our code isn’t crappy), and Prettier (so the code looks nice). In your terminal console, move to a folder you want to create the game below.

mkdir first-paser3-game
cd first-paser3-game
npm init -y
npm install parcel-bundler --save-dev 
npm install eslint parcel-plugin-clean-easy parcel-plugin-static-files-copy --save-dev
npm install babel-core babel-eslint --save-dev
mkdir public
mkdir src
touch src/index.html
touch src/main.js
code .

Now add this content to the files we just created.

src/index.html

<html>

<head>
    <title>First Phaser3 Game</title>
</head>

<body>
    <h1>Hello from Html</h1>
    <script src="main.js"></script>
</body>

</html>

src/main.js

console.log('hello')

Add scripts and parcel configuration to package.json

package.json

{
  "name": "first-phaser3-game",
  "version": "1.0.0",
  "description": "",
  "main": "index.html",
  "scripts": {
    "start": "parcel src/index.html -p 8000",
    "build": "parcel build src/index.html --out-dir dist",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "eslint": "^7.13.0",
    "parcel-bundler": "^1.12.4"
  },
  "parcelCleanPaths": ["dist"],
  "staticFiles": {
    "staticPath": "public",
    "watcherGlob": "**"
  }
}

Now let’s add some Prettier and ESLint configuration.

.prettierrc

{
  "trailingComma": "all",
  "tabWidth": 2,
  "semi": false,
  "singleQuote": true
}

.eslintrc.js

(module.exports = {
  "root": true,
  "parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module"
  },
  "env": {
    "es6": true,
    "browser": true
  },
  "extends": ["eslint:recommended"],
  "rules": {}
})

Test it

Run the application.

npm start

Open a browser to http://localhost:8000/ and inspect the console for our ‘hello’ message.

#Make the Game Let’s add Phaser3 to the project.

npm install phaser

Download some free assets from https://itch.io. I’m using https://audrey.itch.io/doggy-; you can find it by searching for ‘Doggy game assets.’ If you are following along, then your file stricture should look like this.

Asset File Structure

If you are looking for a tutorial on Phaser3, there are many out there. Our goal is to have fun with Javascript, so we will only be placing a sprite on the screen and moving it around. It’s just a taste that will hopefully get you interested in playing more.

Create the Phaser game by editing main.js.

src/main.js

import Phaser from 'phaser'
import MyScene from './scenes/my-scene'

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    physics: {
        default: 'arcade',
        arcade: {
            gravity: {
                y: 100
            },
        },
    },
    scene: [MyScene],
}

export default new Phaser.Game(config)

Now create a simple scene.

src/scenes/my-scene.js

import Phaser from 'phaser'

export default class MyScene extends Phaser.Scene {
    constructor() {
        super('MyScene')
    }

    preload() {
        this.load.image('dog', 'assets/doggy/Tiles/dogBrown.png')

        this.cursors = this.input.keyboard.createCursorKeys()
    }

    create() {
        this.dog = this.add.sprite(100, 100, 'dog')
    }

    update() {
        const speed = 1
        if (this.cursors.up.isDown) {
            this.dog.y -= speed
        }
        if (this.cursors.down.isDown) {
            this.dog.y += speed
        }
        if (this.cursors.left.isDown) {
            this.dog.x -= speed
        }
        if (this.cursors.right.isDown) {
            this.dog.x += speed
        }
    }
}

Phaser has lifecycle methods called on the scene (a scene is like a stage in a game). In preload() we load our assets. in create() we use assets to create game entities like sprites in this case. Phaser calls update() in a loop while the scene is active. This loop is where we get a chance to move the sprites and detect state changes.

Test it

Run the application.

npm start

Open a browser to http://localhost:8000/ and use the arrow keys to move the dog.

Add some Physics

In src/main.js , we set physics to ‘Arcade.’ Let’s turn on debug so we can see what’s going on.

src/main.js

// omitted code
physics: {
    default: 'arcade',
    arcade: {
        gravity: {
            y: 100
        },
        debug: true,
    },
},
// omitted code

You’ll notice that our dog sprite does not obey the laws of physics. He just floats in space. Let’s fix that by changing the way we add our dog to the scene.

src/main.js

// omitted code
create() {
        this.dog = this.physics.add.sprite(100, 100, 'dog')
        // omitted code

There we go, now our dog falls out of the game, so let’s add ground.

src/main.js

// omitted code
preload() {
    this.load.image('dog', 'assets/doggy/Tiles/dogBrown.png')
    this.load.image('ground', 'assets/doggy/Tiles/grassHalfCenter.png')

    this.cursors = this.input.keyboard.createCursorKeys()
}

create() {
    this.dog = this.physics.add.sprite(100, 100, 'dog')
    this.dog.body.collideWorldBounds = true

    const platforms = this.physics.add.staticGroup()
    const gameWidth = this.physics.world.bounds.width
    const groundTileWidth = this.game.textures.get('ground').source[0].width
    const tilesToCoverGround = Math.ceil(gameWidth / groundTileWidth)

    for (let index = 0; index < tilesToCoverGround; index++) {
        platforms.create(
            index * groundTileWidth,
            this.physics.world.bounds.bottom,
            'ground',
        )
    }
    this.physics.add.collider(this.dog, platforms)
}
// omitted code

We can do a better job of moving our dog too.

src/main.js

// omitted code
update() {
    const speed = 4
    const jumpPower = -200
    if (this.cursors.up.isDown && !this.dog.body.touching.none) {
        this.dog.setVelocityY(jumpPower)
    }
    if (this.cursors.left.isDown) {
        this.dog.x -= speed
    }
    if (this.cursors.right.isDown) {
        this.dog.x += speed
    }
}
// omitted code

Practice Javascript skill with a refactor

The code to build the ground is messy in my-scene.js , so let’s move it into its own file.

src/components/ground.js

/**
 * @param {Phaser.Scene} scene
 * @param {string} groundTileTexture
 */
const createGround = (scene, groundTileTexture) => {
    const platforms = scene.physics.add.staticGroup()
    const gameWidth = scene.physics.world.bounds.width
    const groundTexture = scene.game.textures.get(groundTileTexture)
    const groundTileWidth = groundTexture.source[0].width
    const tilesToCoverGround = Math.ceil(gameWidth / groundTileWidth)

    for (let index = 0; index < tilesToCoverGround; index++) {
        platforms.create(
            index * groundTileWidth,
            scene.physics.world.bounds.bottom,
            groundTexture
        )
    }
    return platforms
}

export default createGround

Now use this component in our scene.

src/scenes/my-scene.js

import Phaser from 'phaser'
import createGround from '../components/ground'

export default class MyScene extends Phaser.Scene {
    constructor() {
        super('MyScene')
    }

    preload() {
        this.load.image('dog', 'assets/doggy/Tiles/dogBrown.png')
        this.load.image('ground', 'assets/doggy/Tiles/grassHalfCenter.png')

        this.cursors = this.input.keyboard.createCursorKeys()
    }

    create() {
        this.dog = this.physics.add.sprite(100, 100, 'dog')
        this.dog.body.collideWorldBounds = true

        const ground = createGround(this, 'ground')
        this.physics.add.collider(this.dog, ground)
    }

    update() {
        const speed = 4
        const jumpPower = -200
        if (this.cursors.up.isDown && !this.dog.body.touching.none) {
            this.dog.setVelocityY(jumpPower)
        }
        if (this.cursors.left.isDown) {
            this.dog.x -= speed
        }
        if (this.cursors.right.isDown) {
            this.dog.x += speed
        }
    }
}

That’s great, but we can do more. Let’s play with classes to clean up the Dog code.

src/components/dog.js

import Phaser from 'phaser'

export default class Dog extends Phaser.Physics.Arcade.Sprite {
    /**
     * @param {Phaser.Scene} scene
     * @param {number} x
     * @param {number} y
     * @param {string} texture
     */
    constructor(scene, x, y, texture) {
        super(scene, x, y, texture)
        scene.add.existing(this)
        scene.physics.add.existing(this)
        this.body.collideWorldBounds = true
    }

    /**
     * @param {Phaser.Scene} scene
     * **/
    update(scene) {
        const speed = 4
        const jumpPower = -200
        if (scene.cursors.up.isDown && !scene.dog.body.touching.none) {
            scene.dog.setVelocityY(jumpPower)
        }
        if (scene.cursors.left.isDown) {
            scene.dog.x -= speed
        }
        if (scene.cursors.right.isDown) {
            scene.dog.x += speed
        }
    }
}

Now use it.

src/scenes/my-scene.js

import Phaser from 'phaser'
import createGround from '../components/ground'
import Dog from '../components/dog'

export default class MyScene extends Phaser.Scene {
    constructor() {
        super('MyScene')
    }

    preload() {
        this.load.image('dog', 'assets/doggy/Tiles/dogBrown.png')
        this.load.image('ground', 'assets/doggy/Tiles/grassHalfCenter.png')

        this.cursors = this.input.keyboard.createCursorKeys()
    }

    create() {
        this.dog = new Dog(this, 100, 100, 'dog')

        const ground = createGround(this, 'ground')
        this.physics.add.collider(this.dog, ground)
    }

    update() {
        this.dog.update(this)
    }
}

I like that!

Next

There is so much more we could do. I hope this was inspirational.

© Copyright 2023 Digital Garden cultivated by James Kolean.