Getting Started in Phaser 3 (ES6) – Create a Boomdots game part 2

Getting Started in Phaser 3 (ES6) – Create a Boomdots game part 2

Welcome to the second part of getting started in phaser 3. We are trying to create a clone of the famous Boomdots game in phaser 3 in ES6.

In the previous part, we completed the development setup and added a scrolling bg. In this post, we are going to add the rocket and the alien, set the collisions and add some particle effects to make it look better.

Add rocket

Add the rocket.png into the assets folder.

Load the image in the preloader.

preload () {
    ...
    this.load.image('rocket', 'assets/rocket.png')
}

In game.js create the rocket game object like this.

create () {
    ...
    this.rocket = this.add.sprite(0, 160, 'rocket')
}

Add alien

Let us add the alien into the scene similar to how we added the rocket.

preloader.js

preload () {
    ...
    this.load.image('rocket', 'assets/alien.png')
}

game.js

create () {
    ...
    this.alien = this.add.sprite(0, -150, 'alien')
}

Remember, our game center position is 0,0, so we need a negative value for the alien to display above the mid position.

If we open up our browser, we can see the alien in the scene.

Creating and animating the alien

Now, change the alien starting position from -150 to -300. And call a resetAlien method which we are going to create.

this.alien = this.add.sprite(0, -300, 'alien')
this.resetAlien()

The resetAlien method,

resetAlien () {
    this.canUpdateAlien = true
    this.alien.x = 0
    this.alien.y = -300
}

We can call this resetAlien method whenever the user hits it. Ok, we will go through that later. To animate the alien, create a method called moveAlien with arguments time and delta.

constructor () {
    ...
    this.alienTargetY = 0
    this.canUpdateAlien = false
}
...
update (time, delta) {
    this.scrollingBg.tilePositionY -= 1
    if (this.canUpdateAlien) {
    	this.moveAlien(time, delta)
    }
}
moveAlien (time, delta) {
    // Moves the alien down to this.alienTargetY position
    this.alien.y += (this.alienTargetY - this.alien.y) * 0.3
    
    // Moves the horizontal position back and forth
    // Multiplying the time to reduce the movement speed
    // 80 is the maximum horizontal amount to move
    this.alien.x = Math.sin(time * 0.005) * 80
}

Let’s check how it appears.

Move rocket

In the create method, listen for the pointerdown event and move the rocket upwards and collide it with the alien.

constructor () {
    super({
        key: 'game',
        physics: {
            default: 'arcade',
            arcade: {
            	debug: true,
            	// gravity: 0 //We don't need gravity for this game
        	}
        }
    })
    ...
}
create () {
    ...
    //Enable physics on rocket and alien sprites
    this.physics.world.enable([this.rocket, this.alien])
    
    //Reset alien so it will spawn from the top and start moving
    this.resetAlien()
    
    //Listen for pointerdown event and launch the rocket
    this.input.on('pointerdown', this.launchRocket, this)
}
update (time, delta) {
    ...
    //Listen for overlapping between rocket and alien and call rocketCollidedWithAlien when an overlap occurs
    if (this.canUpdateAlien) {
    this.moveAlien(time, delta)
       	this.physics.add.overlap(this.rocket, this.alien, this.rocketCollidedWithAlien, null, this)
    }
}
...
launchRocket () {
    //Launching means decrease the y velocity
    this.rocket.body.setVelocity(0, -3000)
}
rocketCollidedWithAlien (rocket, alien) {
    //Stop updating alien movement
    this.canUpdateAlien = false
    
    //Stop moving the rocket
    this.rocket.body.setVelocity(0)
    
    //Move the alien out of our screen for now
    this.alien.y = -300
}
...
shutdown () {
    this.input.off('pointerdown', this.launchRocket, this)
}

Add collision particles

First, we need to load the particle sprite in preloader.js

this.load.image('particle', 'assets/squareparticle.png')

Create the particle manager and an emitter. We are also going to add a camera shake.

constructor () {
    ...
    this.particles = null
    this.emitter = null
}
...
create () {
    ...
    this.particles = this.add.particles('particle')
    this.emitter = this.particles.createEmitter({
        angle: { min: 0, max: 360 },
        speed: { min: 50, max: 200 },
        quantity: { min: 40, max: 50 },
        lifespan: { min: 200, max: 500},
        alpha: { start: 1, end: 0 },
        scale: { min: 0.5, max: 0.5 },
        rotate: { start: 0, end: 360 },
        gravityY: 800,
        on: false
    })
}
...
rocketCollidedWithAlien (rocket, alien) {
    if (!this.canUpdateAlien) { //Overlap runs multiple frames, we only want it to run once
    return
    }
    this.canUpdateAlien = false
    this.rocket.body.setVelocity(0)
    this.particles.emitParticleAt(this.alien.x, this.alien.y)
    this.alien.y = -300
    this.cameras.main.shake(100, 0.01, 0.01) //Duration, intensity, force
}

After hitting the alien, we need to reset the rocket back to the original position. We also need to scroll the bg a little.

constructor () {
    ...
    this.isRocketResetting = false
}
update (time, delta) {
    if (this.canUpdateAlien) {
    	this.moveAlien(time, delta)
    	this.physics.add.overlap(this.rocket, this.alien, this.rocketCollidedWithAlien, null, this)
    }
    if (this.isRocketResetting) {
    	//Scroll the bg
        this.scrollingBg.tilePositionY -= delta
        
        //Move rocket down
        this.rocket.y += delta
        if (this.rocket.y >= 160) {
            this.rocket.y = 160
            this.isRocketResetting = false
            
            //After movement reset alien so the next alien comes
            this.resetAlien()
        }
    }
}
resetAlien () {
    this.canUpdateAlien = true
    this.alien.x = 0
    this.alien.y = -300
    this.alienTargetY = phaser.Math.Between(-200, 0)
}
rocketCollidedWithAlien (rocket, alien) {
    if (!this.canUpdateAlien) {
    	return
    }
    this.canUpdateAlien = false
    this.rocket.body.setVelocity(0)
    this.particles.emitParticleAt(this.alien.x, this.alien.y)
    this.alien.y = -300
    this.cameras.main.shake(100, 0.01, 0.01)
    this.time.delayedCall(200, this.resetRocket, [], this)
}

resetRocket () {
    this.isRocketResetting = true
}

Let us place some triangle spikes on top and the bottom end of the screen.

Phaser 3 has various built-in position align helpers. We can use the camera rectangle and align the spikes to the top and bottom of that rectangle. But there is a problem in that, we are using the camera zoom. So for the correct dimensions, we need to divide the camera dimensions by the camera zoom amount. Like this.

constructor () {
    ...
    this.topSpikes = null
    this.bottomSpikes = null
    this.cameraRect = null
}
...
create () {
    this.topSpikes = this.add.sprite(0, 0, 'spike')
    this.topSpikes.setOrigin(0.5, 0)
    this.bottomSpikes = this.add.sprite(0, 0, 'spike')
    this.bottomSpikes.setOrigin(0, 1)
    this.bottomSpikes.flipY = true
    this.cameraRect = this.add.zone(0, 0, 0, 0)
    ...
    this.physics.world.enable([this.rocket, this.alien, this.topSpikes])
    this.topSpikes.body.immovable = true
}
...
update (time, delta) {
    ...
    if (!this.isGameOver) {
    	this.physics.add.overlap(this.rocket, this.topSpikes, this.rocketCollidedWithSpike, null, this)
    }
    ...
}
resize () {
    ...
    this.cameraRect.x = cam.x
    this.cameraRect.y = cam.y
    this.cameraRect.width = cam.width/cam.zoom
    this.cameraRect.height = cam.height/cam.zoom
    
    phaser.Display.Align.In.TopCenter(this.topSpikes, this.cameraRect)
    phaser.Display.Align.In.BottomCenter(this.bottomSpikes, this.cameraRect)
}
...
rocketCollidedWithSpike (rocket, spike) {
    if (this.isGameOver) {
    	return
    }
    this.canUpdateAlien = false
    this.isGameOver = true
    this.rocket.body.setVelocity(0)
    this.particles.emitParticleAt(this.rocket.x, this.rocket.y-this.rocket.height)
    this.cameras.main.shake(100, 0.01, 0.01)
    this.alien.destroy()
    this.rocket.destroy()
}
...

That’s it. The core game is complete. There are only small things remaining like scoring and gameover scenes that you can manage yourself.

The complete code for preloader.js

import { Scene } from 'phaser'

export class Preloader extends Scene{
    constructor(){
        super({
            key: 'preloader'
        })
    }

    preload () {
        this.load.image('bg-static', 'assets/square.png')
        this.load.image('bg-overlay', 'assets/bg.png')
        this.load.image('rocket', 'assets/rocket.png')
        this.load.image('alien', 'assets/alien.png')
        this.load.image('particle', 'assets/squareparticle.png')
        this.load.image('spike', 'assets/spikes.png')
    }

    create () {
        this.scene.start('game')
    }

}

And for game.js

import phaser, { Scene } from 'phaser'

export class Game extends Scene {
    constructor () {
        super({
            key: 'game',
            physics: {
                default: 'arcade',
                arcade: {
                    debug: true
                }
            }
        })
        this.staticBg = null
        this.scrollingBg = null
        this.rocket = null
        this.scrollSpeed = 0
        this.alien = null
        this.alienTargetY = 0
        this.canUpdateAlien = false
        this.particles = null
        this.emitter = null
        this.isRocketResetting = false
        this.topSpikes = null
        this.bottomSpikes = null
        this.cameraRect = null
        this.isGameOver = false
    }

    create () {
        this.staticBg = this.add.image(0, 0, 'bg-static')
        this.staticBg.setTint(0x444444)
        this.staticBg.setOrigin(0.5)
        this.scrollingBg = this.add.tileSprite(0,0,396,529,'bg-overlay')
        this.scrollingBg.setOrigin(0.5)

        this.topSpikes = this.add.sprite(0, 0, 'spike')
        this.topSpikes.setOrigin(0.5, 0)
        this.bottomSpikes = this.add.sprite(0, 0, 'spike')
        this.bottomSpikes.setOrigin(0, 1)
        this.bottomSpikes.flipY = true
        this.cameraRect = this.add.zone(0, 0, 0, 0)

        this.sys.game.events.on('resize', this.resize, this)
        this.resize()
        this.events.once('shutdown', this.shutdown, this)
        
        this.rocket = this.add.sprite(0, 160, 'rocket')
        this.alien = this.add.sprite(0, -300, 'alien')

        this.physics.world.enable([this.rocket, this.alien, this.topSpikes])
        this.topSpikes.body.immovable = true

        this.resetAlien()

        this.particles = this.add.particles('particle')
        this.emitter = this.particles.createEmitter({
            angle: { min: 0, max: 360 },
            speed: { min: 50, max: 200 },
            quantity: { min: 40, max: 50 },
            lifespan: { min: 200, max: 500},
            alpha: { start: 1, end: 0 },
            scale: { min: 0.5, max: 0.5 },
            rotate: { start: 0, end: 360 },
            gravityY: 800,
            on: false
        })

        this.input.on('pointerdown', this.launchRocket, this)
    }

    resize () {
        let cam = this.cameras.main
        cam.setViewport(0,0,window.innerWidth, window.innerHeight)
        cam.centerToBounds()
        cam.zoom = Math.max(window.innerWidth/270, window.innerHeight/480)
        // cam.zoom = Math.min(window.innerWidth/270, window.innerHeight/480)

        this.cameraRect.x = cam.x
        this.cameraRect.y = cam.y
        this.cameraRect.width = cam.width/cam.zoom
        this.cameraRect.height = cam.height/cam.zoom

        phaser.Display.Align.In.TopCenter(this.topSpikes, this.cameraRect)
        phaser.Display.Align.In.BottomCenter(this.bottomSpikes, this.cameraRect)
    }

    update (time, delta) {
        if (this.canUpdateAlien) {
            this.moveAlien(time, delta)
            this.physics.add.overlap(this.rocket, this.alien, this.rocketCollidedWithAlien, null, this)
        }
        if (!this.isGameOver) {
            this.physics.add.overlap(this.rocket, this.topSpikes, this.rocketCollidedWithSpike, null, this)
        }
        if (this.isRocketResetting) {
            this.scrollingBg.tilePositionY -= delta
            this.rocket.y += delta
            if (this.rocket.y >= 160) {
                this.rocket.y = 160
                this.isRocketResetting = false
                this.resetAlien()
            }
        }
    }

    resetAlien () {
        this.canUpdateAlien = true
        this.alien.x = 0
        this.alien.y = -300
        this.alienTargetY = phaser.Math.Between(-100, 0)
    }

    moveAlien (time, delta) {
        this.alien.y += (this.alienTargetY - this.alien.y) * 0.3
        this.alien.x = Math.sin(time * 0.005) * 80
    }

    launchRocket () {
        this.rocket.body.setVelocity(0, -2000)
    }

    rocketCollidedWithAlien (rocket, alien) {
        if (!this.canUpdateAlien) { //Overlap runs multiple frames, we only want it to run once
            return
        }
        this.canUpdateAlien = false
        this.rocket.body.setVelocity(0)
        this.particles.emitParticleAt(this.alien.x, this.alien.y)
        this.alien.y = -300
        this.cameras.main.shake(100, 0.01, 0.01)
        this.time.delayedCall(200, this.resetRocket, [], this)
    }

    rocketCollidedWithSpike (rocket, spike) {
        if (this.isGameOver) {
            return
        }
        this.canUpdateAlien = false
        this.isGameOver = true
        this.rocket.body.setVelocity(0)
        this.particles.emitParticleAt(this.rocket.x, this.rocket.y-this.rocket.height)
        this.cameras.main.shake(100, 0.01, 0.01)
        this.alien.destroy()
        this.rocket.destroy()
    }

    resetRocket () {
        this.isRocketResetting = true
    }

    shutdown () {
        this.input.off('pointerdown', this.launchRocket, this)
        this.sys.game.events.off('resize', this.resize, this)
    }
}

Thanks for reading.

If you want to get notified about new articles like these, please subscribe to our Newsletter.