# data[].attributes.attachment_link.title
"Dashy Panda" HTML5 game prototype improved with coins to collect, powered by Phaser and Arcade physics

# data[].attributes.attachment_link.description
"Dashy Panda" tutorial series gets a new update with coins to collect.

# data[].attributes.attachment_link.readable_text
Check ou my new developpement blog: lesscake.com

Learn how to make your own 2D games with the tutorials on this website. The explanations are simple, clear, and with lots of animated gifs to make everything easy to understand.

Phaser HTML5 Tutorials

Unity 2D Tutorials

Pixel Art Tutorials

Game Design Tutorials

# data[].attributes.attachment_link.description
Check ou my new developpement blog: lesscake.com Learn how to make your own 2D games with the tutorials on this website. The explanations are simple, clear, and with lots of animated gifs to make everything easy to understand.

# data[].attributes.attachment_link.readable_text
Część 1 - Wprowadzenie

Witaj w pierwszym tutorialu dotyczącym tworzenia gier w Phaserze 3. Nauczysz się z niego jak stworzyć małą grę, w której gracz będzie biegał i skakał po platformach, kolekcjonował gwiazdki i unikał przeciwników. Podczas nauki przeprowadzimy Cię przez podstawowe funkcje frameworka.

Czym jest Phaser?

Phaser jest opartym na HTML5 frameworkiem służącym do tworzenia gier. Jego celem jest usprawnienie i przyśpieszenie procesu pisania zaawansowanych, przenośnych gier. Został on stworzony po to by wykorzystać zalety nowoczesnych przeglądarek, zarówno tych mobilnych jak i desktopowych. Jedynym wymaganiem Phasera jest obsługa znacznika canvas.

Wymagania

Pobierz ten plik zip - zawiera on każdy krok tego poradnika w postaci kodu oraz wymaganych plików.

Wymagana jest bardzo podstawowa wiedza na temat języka JavaScript.

Pamiętaj również by przeczytać Przewodnik wprowadzający, pokaże Ci on jak pobrać framework, skonfigurować lokalne środowisko, a także pozwoli rzucić okiem na strukturę projektu w Phaserze oraz jego podstawowe funkcje.

Jeżeli przebrnąłeś już przez Przewodnik wprowadzający, masz skonfigurowane środowisko testowe i możesz rozpocząć kodowanie. Pobierz pliki tego tutoriala i wypakuj je w głównym folderze Twojego serwera WWW.

Otwórz plik part1.html w Twoim ulubionym edytorze. Przyszedł czas na zapoznaniem się z kodem. Po linijkach dołączających Phaser oraz zawierających podstawowy HTML5 znajdziesz:

var config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    scene: {
        preload: preload,
        create: create,
        update: update
    }
};

var game = new Phaser.Game(config);

function preload ()
{
}

function create ()
{
}

function update ()
{
}

Obiekt config zawiera ustawienia Twojej gry. Istnieje wiele opcji, które możesz deklarować w tym obiekcie. Dowiesz się o nich stopniowo powiększając wiedzę o Phaserze. W tym poradniku ustawimy jedynie sposób renderowania, wymiary i domyślną scenę.

Instancja objektu Phaser.Game jest przypisana do lokalnej zmiennej nazwanej po prostu game i obiekt zawierający konfigurację jest do niej przekazywany. Tak rozpoczyna działanie framework.
W wersji drugiej Phasera obiekt game zachowywał się jak interfejs udostępniający wszystkie wewnętrzne systemy i często wykorzystywany był poprzez globalną zmienną. Phaser 3 nie działa w taki sposób i nie udostępnia już globalnej instancji gry.
Właściwosć type może przyjąć wartości Phaser.CANVAS, Phaser.WEBGL albo Phaser.AUTO. Opcja ta określa metodę renderowania grafiki. Rekomendowana wartość Phaser.AUTO sprawia, że Phaser spróbuje uruchomić się w trybie WebGL i automatycznie zmieni sposób renderowania na canvas jeżeli tryb ten nie jest obsługiwany przez przeglądarkę. Element canvas, w którym renderowana jest gra, zostanie dodany do dokumentu w miejscu wywołania skryptu lub można go umiejscowić za pomocą odpowiedniej konfiguracji w innym elemencie nadrzędnym dokumentu.

Właściwości width i height odpowiadają za rozmiar tworzonego przez Phaser elementu canvas. W tym przypaddku będzie to 800 na 600 pikseli. Świat gry może skalować się do dowolnych rozmiarów ale zostanie wygenerowany właśnie w takiej rozdzielczości.

Parametr scene zostanie dokładniej opisany w dalszej części tego poradnika.

# data[].attributes.attachment_link.title
Pierwsza gra w Phaserze 3: Część 1 - Wprowadzenie - Learn - Phaser

# data[].attributes.attachment_link.description
Witaj w pierwszym tutorialu dotyczącym tworzenia gier w Phaserze 3. Nauczysz się z niego jak stworzyć małą grę, w której gracz będzie biegał i skakał po platformach, kolekcjonował gwiazdki i unikał przeciwników. Podczas nauki przeprowadzimy Cię przez podstawowe funkcje frameworka. Phaser jest opartym na HTML5 frameworkiem służącym do tworzenia gier.

# data[].attributes.headline
Phaser gra Runner z tłem, które się przesuwa 

# data[].attributes.subject
Phaser gra Runner z tłem, które się przesuwa 

# data[].attributes.attachment_link.readable_text
How to scale your HTML5 games if your framework does not feature a scale manager - or if you do not use any framework. Finally, the scale manager is back in Phaser 3, and it also comes with a full screen feature. Look at theendless runner game I am blogging about during these days: Click or tap to jump or double jump. Once the focus is on the iframe hosting the game, pressing SPACEBAR will make the game run in full screen. What changed since previous version? First, the code in the page hosting the game has no CSS, switching from this code:

<!DOCTYPE html&gt;
<html&gt;
	<head&gt;
        <style type = "text/css"&gt;
            body{
                background: #000000;
                padding: 0px;
                margin: 0px;
            }
            canvas{
                display:block;
                margin: 0;
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
            }
        </style&gt;
        <script src = "phaser.min.js"&gt;</script&gt;
        <script src = "game.js"&gt;</script&gt;
    </head&gt;
	<body&gt;
	</body&gt;
</html&gt;

To this code:

<!DOCTYPE html&gt;
<html&gt;
	<head&gt;
        <style type = "text/css"&gt;
            body{
                background: #000000;
                padding: 0px;
                margin: 0px;
            }
        </style&gt;
        <script src = "phaser.min.js"&gt;</script&gt;
        <script src = "game.js"&gt;</script&gt;
    </head&gt;
	<body&gt;
        <div id = "thegame"&gt;</div&gt;
	</body&gt;
</html&gt;

It's much cleaner now, with no CSS to center the canvas on the browser window. I just added a div where to place the game itself What about the game script? When you define the object with game configuration, you will be able to define the scale mode, like in this case:

scale: {
    mode: Phaser.Scale.FIT,
    autoCenter: Phaser.Scale.CENTER_BOTH,
    parent: "thegame",
    width: 1334,
    height: 750
}

In the previous code I am saying I want the game to fit the largest area of the browser while keeping the whole content visible, centering the canvas both horizontally and vertically, using the canvas whose id is thegame, with a 1334×750 size. Full screen management is even easier:

if(!this.scale.isFullscreen){
    this.scale.startFullscreen();
}

Look at the whole code in action, where everything is handled by Phaser:

let game;

// global game options
let gameOptions = {

    // platform speed range, in pixels per second
    platformSpeedRange: [300, 300],

    // mountain speed, in pixels per second
    mountainSpeed: 80,

    // spawn range, how far should be the rightmost platform from the right edge
    // before next platform spawns, in pixels
    spawnRange: [80, 300],

    // platform width range, in pixels
    platformSizeRange: [90, 300],

    // a height range between rightmost platform and next platform to be spawned
    platformHeightRange: [-5, 5],

    // a scale to be multiplied by platformHeightRange
    platformHeighScale: 20,

    // platform max and min height, as screen height ratio
    platformVerticalLimit: [0.4, 0.8],

    // player gravity
    playerGravity: 900,

    // player jump force
    jumpForce: 400,

    // player starting X position
    playerStartPosition: 200,

    // consecutive jumps allowed
    jumps: 2,

    // % of probability a coin appears on the platform
    coinPercent: 25,

    // % of probability a fire appears on the platform
    firePercent: 25
}

window.onload = function() {

    // object containing configuration options
    let gameConfig = {
        type: Phaser.AUTO,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: "thegame",
            width: 1334,
            height: 750
        },
        scene: [preloadGame, playGame],
        backgroundColor: 0x0c88c7,

        // physics settings
        physics: {
            default: "arcade"
        }
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}

// preloadGame scene
class preloadGame extends Phaser.Scene{
    constructor(){
        super("PreloadGame");
    }
    preload(){
        this.load.image("platform", "platform.png");

        // player is a sprite sheet made by 24x48 pixels
        this.load.spritesheet("player", "player.png", {
            frameWidth: 24,
            frameHeight: 48
        });

        // the coin is a sprite sheet made by 20x20 pixels
        this.load.spritesheet("coin", "coin.png", {
            frameWidth: 20,
            frameHeight: 20
        });

        // the firecamp is a sprite sheet made by 32x58 pixels
        this.load.spritesheet("fire", "fire.png", {
            frameWidth: 40,
            frameHeight: 70
        });

        // mountains are a sprite sheet made by 512x512 pixels
        this.load.spritesheet("mountain", "mountain.png", {
            frameWidth: 512,
            frameHeight: 512
        });
    }
    create(){

        // setting player animation
        this.anims.create({
            key: "run",
            frames: this.anims.generateFrameNumbers("player", {
                start: 0,
                end: 1
            }),
            frameRate: 8,
            repeat: -1
        });

        // setting coin animation
        this.anims.create({
            key: "rotate",
            frames: this.anims.generateFrameNumbers("coin", {
                start: 0,
                end: 5
            }),
            frameRate: 15,
            yoyo: true,
            repeat: -1
        });

        // setting fire animation
        this.anims.create({
            key: "burn",
            frames: this.anims.generateFrameNumbers("fire", {
                start: 0,
                end: 4
            }),
            frameRate: 15,
            repeat: -1
        });

        this.scene.start("PlayGame");
    }
}

// playGame scene
class playGame extends Phaser.Scene{
    constructor(){
        super("PlayGame");
    }
    create(){

        // group with all active mountains.
        this.mountainGroup = this.add.group();

        // group with all active platforms.
        this.platformGroup = this.add.group({

            // once a platform is removed, it's added to the pool
            removeCallback: function(platform){
                platform.scene.platformPool.add(platform)
            }
        });

        // platform pool
        this.platformPool = this.add.group({

            // once a platform is removed from the pool, it's added to the active platforms group
            removeCallback: function(platform){
                platform.scene.platformGroup.add(platform)
            }
        });

        // group with all active coins.
        this.coinGroup = this.add.group({

            // once a coin is removed, it's added to the pool
            removeCallback: function(coin){
                coin.scene.coinPool.add(coin)
            }
        });

        // coin pool
        this.coinPool = this.add.group({

            // once a coin is removed from the pool, it's added to the active coins group
            removeCallback: function(coin){
                coin.scene.coinGroup.add(coin)
            }
        });

        // group with all active firecamps.
        this.fireGroup = this.add.group({

            // once a firecamp is removed, it's added to the pool
            removeCallback: function(fire){
                fire.scene.firePool.add(fire)
            }
        });

        // fire pool
        this.firePool = this.add.group({

            // once a fire is removed from the pool, it's added to the active fire group
            removeCallback: function(fire){
                fire.scene.fireGroup.add(fire)
            }
        });

        // adding a mountain
        this.addMountains()

        // keeping track of added platforms
        this.addedPlatforms = 0;

        // number of consecutive jumps made by the player so far
        this.playerJumps = 0;

        // adding a platform to the game, the arguments are platform width, x position and y position
        this.addPlatform(game.config.width, game.config.width / 2, game.config.height * gameOptions.platformVerticalLimit[1]);

        // adding the player;
        this.player = this.physics.add.sprite(gameOptions.playerStartPosition, game.config.height * 0.7, "player");
        this.player.setGravityY(gameOptions.playerGravity);
        this.player.setDepth(2);

        // the player is not dying
        this.dying = false;

        // setting collisions between the player and the platform group
        this.platformCollider = this.physics.add.collider(this.player, this.platformGroup, function(){

            // play "run" animation if the player is on a platform
            if(!this.player.anims.isPlaying){
                this.player.anims.play("run");
            }
        }, null, this);

        // setting collisions between the player and the coin group
        this.physics.add.overlap(this.player, this.coinGroup, function(player, coin){

            this.tweens.add({
                targets: coin,
                y: coin.y - 100,
                alpha: 0,
                duration: 800,
                ease: "Cubic.easeOut",
                callbackScope: this,
                onComplete: function(){
                    this.coinGroup.killAndHide(coin);
                    this.coinGroup.remove(coin);
                }
            });

        }, null, this);

        // setting collisions between the player and the fire group
        this.physics.add.overlap(this.player, this.fireGroup, function(player, fire){

            this.dying = true;
            this.player.anims.stop();
            this.player.setFrame(2);
            this.player.body.setVelocityY(-200);
            this.physics.world.removeCollider(this.platformCollider);

        }, null, this);

        // checking for input
        this.input.on("pointerdown", this.jump, this);

        this.add.text(20, 0, "Press SPACEBAR to enter fullscreen", {
            fontFamily: "Arial",
            fontSize: 64,
            color: "#ffffff"
        });

        this.input.keyboard.on("keydown_SPACE", function(){
            if(!this.scale.isFullscreen){
                this.scale.startFullscreen();
            }
        }, this);
    }

    // adding mountains
    addMountains(){
        let rightmostMountain = this.getRightmostMountain();
        if(rightmostMountain < game.config.width * 2){
            let mountain = this.physics.add.sprite(rightmostMountain + Phaser.Math.Between(100, 350), game.config.height + Phaser.Math.Between(0, 100), "mountain");
            mountain.setOrigin(0.5, 1);
            mountain.body.setVelocityX(gameOptions.mountainSpeed * -1)
            this.mountainGroup.add(mountain);
            if(Phaser.Math.Between(0, 1)){
                mountain.setDepth(1);
            }
            mountain.setFrame(Phaser.Math.Between(0, 3))
            this.addMountains()
        }
    }

    // getting rightmost mountain x position
    getRightmostMountain(){
        let rightmostMountain = -200;
        this.mountainGroup.getChildren().forEach(function(mountain){
            rightmostMountain = Math.max(rightmostMountain, mountain.x);
        })
        return rightmostMountain;
    }

    // the core of the script: platform are added from the pool or created on the fly
    addPlatform(platformWidth, posX, posY){
        this.addedPlatforms ++;
        let platform;
        if(this.platformPool.getLength()){
            platform = this.platformPool.getFirst();
            platform.x = posX;
            platform.y = posY;
            platform.active = true;
            platform.visible = true;
            this.platformPool.remove(platform);
            let newRatio =  platformWidth / platform.displayWidth;
            platform.displayWidth = platformWidth;
            platform.tileScaleX = 1 / platform.scaleX;
        }
        else{
            platform = this.add.tileSprite(posX, posY, platformWidth, 32, "platform");
            this.physics.add.existing(platform);
            platform.body.setImmovable(true);
            platform.body.setVelocityX(Phaser.Math.Between(gameOptions.platformSpeedRange[0], gameOptions.platformSpeedRange[1]) * -1);
            platform.setDepth(2);
            this.platformGroup.add(platform);
        }
        this.nextPlatformDistance = Phaser.Math.Between(gameOptions.spawnRange[0], gameOptions.spawnRange[1]);

        // if this is not the starting platform...
        if(this.addedPlatforms &gt; 1){

            // is there a coin over the platform?
            if(Phaser.Math.Between(1, 100) <= gameOptions.coinPercent){
                if(this.coinPool.getLength()){
                    let coin = this.coinPool.getFirst();
                    coin.x = posX;
                    coin.y = posY - 96;
                    coin.alpha = 1;
                    coin.active = true;
                    coin.visible = true;
                    this.coinPool.remove(coin);
                }
                else{
                    let coin = this.physics.add.sprite(posX, posY - 96, "coin");
                    coin.setImmovable(true);
                    coin.setVelocityX(platform.body.velocity.x);
                    coin.anims.play("rotate");
                    coin.setDepth(2);
                    this.coinGroup.add(coin);
                }
            }

            // is there a fire over the platform?
            if(Phaser.Math.Between(1, 100) <= gameOptions.firePercent){
                if(this.firePool.getLength()){
                    let fire = this.firePool.getFirst();
                    fire.x = posX - platformWidth / 2 + Phaser.Math.Between(1, platformWidth);
                    fire.y = posY - 46;
                    fire.alpha = 1;
                    fire.active = true;
                    fire.visible = true;
                    this.firePool.remove(fire);
                }
                else{
                    let fire = this.physics.add.sprite(posX - platformWidth / 2 + Phaser.Math.Between(1, platformWidth), posY - 46, "fire");
                    fire.setImmovable(true);
                    fire.setVelocityX(platform.body.velocity.x);
                    fire.setSize(8, 2, true)
                    fire.anims.play("burn");
                    fire.setDepth(2);
                    this.fireGroup.add(fire);
                }
            }
        }
    }

    // the player jumps when on the ground, or once in the air as long as there are jumps left and the first jump was on the ground
    // and obviously if the player is not dying
    jump(){
        if((!this.dying) &amp;&amp; (this.player.body.touching.down || (this.playerJumps &gt; 0 &amp;&amp; this.playerJumps < gameOptions.jumps))){
            if(this.player.body.touching.down){
                this.playerJumps = 0;
            }
            this.player.setVelocityY(gameOptions.jumpForce * -1);
            this.playerJumps ++;

            // stops animation
            this.player.anims.stop();
        }
    }

    update(){

        // game over
        if(this.player.y &gt; game.config.height){
            this.scene.start("PlayGame");
        }

        this.player.x = gameOptions.playerStartPosition;

        // recycling platforms
        let minDistance = game.config.width;
        let rightmostPlatformHeight = 0;
        this.platformGroup.getChildren().forEach(function(platform){
            let platformDistance = game.config.width - platform.x - platform.displayWidth / 2;
            if(platformDistance < minDistance){
                minDistance = platformDistance;
                rightmostPlatformHeight = platform.y;
            }
            if(platform.x < - platform.displayWidth / 2){
                this.platformGroup.killAndHide(platform);
                this.platformGroup.remove(platform);
            }
        }, this);

        // recycling coins
        this.coinGroup.getChildren().forEach(function(coin){
            if(coin.x < - coin.displayWidth / 2){
                this.coinGroup.killAndHide(coin);
                this.coinGroup.remove(coin);
            }
        }, this);

        // recycling fire
        this.fireGroup.getChildren().forEach(function(fire){
            if(fire.x < - fire.displayWidth / 2){
                this.fireGroup.killAndHide(fire);
                this.fireGroup.remove(fire);
            }
        }, this);

        // recycling mountains
        this.mountainGroup.getChildren().forEach(function(mountain){
            if(mountain.x < - mountain.displayWidth){
                let rightmostMountain = this.getRightmostMountain();
                mountain.x = rightmostMountain + Phaser.Math.Between(100, 350);
                mountain.y = game.config.height + Phaser.Math.Between(0, 100);
                mountain.setFrame(Phaser.Math.Between(0, 3))
                if(Phaser.Math.Between(0, 1)){
                    mountain.setDepth(1);
                }
            }
        }, this);

        // adding new platforms
        if(minDistance &gt; this.nextPlatformDistance){
            let nextPlatformWidth = Phaser.Math.Between(gameOptions.platformSizeRange[0], gameOptions.platformSizeRange[1]);
            let platformRandomHeight = gameOptions.platformHeighScale * Phaser.Math.Between(gameOptions.platformHeightRange[0], gameOptions.platformHeightRange[1]);
            let nextPlatformGap = rightmostPlatformHeight + platformRandomHeight;
            let minPlatformHeight = game.config.height * gameOptions.platformVerticalLimit[0];
            let maxPlatformHeight = game.config.height * gameOptions.platformVerticalLimit[1];
            let nextPlatformHeight = Phaser.Math.Clamp(nextPlatformGap, minPlatformHeight, maxPlatformHeight);
            this.addPlatform(nextPlatformWidth, game.config.width + nextPlatformWidth / 2, nextPlatformHeight);
        }
    }
};

A comprehensive guide about scale management will be part of the next update of my book about HTML5 game development so grab your copy today and have all future updates for free, meanwhile download the source code of this project.

# data[].attributes.attachment_link.title
Give your HTML5 games a "full screen" option and manage game scaling and resizing in a couple of lines of code thanks to Phaser 3 version 3.16

# data[].attributes.attachment_link.description
How to scale your HTML5 games if your framework does not feature a scale manager - or if you do not use any framework. Finally, the scale manager is back in Phaser 3, and it also comes with a full screen feature.

# data[].attributes.body
Ksiązka, Tutoriale, przykłady

# data[].attributes.attachment_link.title
Emanuele Feronato - italian geek and PROgrammer

# data[].attributes.attachment_link.description
To celebrate the first year of activity of the developer portal, Crazy Games is hosting a contest with $2,000 for the winner, $500 for the 2nd place and $250 for the 3rd place. Do you think you aren't going to win? You will get revenue share anyway.

# data[].attributes.attachment_link.readable_text
Introduction to Phaser 3: Building Breakout

Introduction

Game development is a unique branch of software development that can be as rewarding as it is complex. When thinking of creating games, we usually think of an application to install and play on our computers or consoles.

The HTML5 spec introduced many APIs to enable game development on the web, allowing our games to reach many users on different computing devices. Phaser is a popular game framework that enables us to quickly build games for the web.

The best way to master game development is to make games. We'll use Phaser to create a Breakout clone - a version of the classic and everlasting Atari game released in 1976.

This tutorial contains some very basic HTML and CSS. You will need to be comfortable with JavaScript functions and objects. It also makes light use of ES2015 features.

The Game Loop

All games run within a loop. After setting up our game world, we enter the game loop which performs the following tasks:

1. Processes input

2. Updates the game world

3. Renders the changes

Let's see how the game loop works in a game like Megaman. After sifting through the menu to start a level, the game decides where to place platforms and loads the music to be played. That setup usually happens during the loading screen.

When the game starts you are in control of Megaman in a world with platforms, enemies, and a particular song for that level. You can use your joystick to move Megaman, and press a button to jump or shoot. The game loop is processing the input, updating the position of Megaman and rendering those changes many times in a second.

What is Phaser?

Phaser is a HTML5 game framework. It uses many HTML5 APIs like Canvas, WebGL, Audio, Gamepad, etc. and adds some helpful logic like managing the game loop and providing us with a physics engines.

With Phaser, we can build 2D games with nothing but HTML, CSS, and JavaScript.

Breakout Rules

Before we use Phaser to build our Breakout clone, let's first define the scope of the game:

This single player game has one level with 30 bricks, a paddle, and a ball

The goal is to get the ball to destroy every brick, while ensuring it does not leave the bottom of the game screen.

The player will control a paddle, which can move left and right

The game is built for Desktop web users, so the keyboard will be used for input

Setting up Phaser

Phaser is a JavaScript library, to develop and play our game we'll need some basic HTML to load the JS. Create a directory called breakout in one of your workspaces.

Create the following files and folders in your directory:

Game assets are art, sound, video, and other data used by the game. For this simple Breakout clone, there aren't many assets that necessitate organizing with folders. However, it is good practice to keep your assets separate from your code and to separate your assets by their type.

Add the following code to your index.html file:

<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <title>Breakout</title> <style> html, body { margin: 0 auto; padding: 0; width: 100%; height: 100%; } #game { margin: 10px auto; padding: 0; width: 800px; height: 640px; } </style> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="game"></div> <script src="//cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js"></script> <script src="breakout.js"></script> </body> </html>

This basic HTML code does the following:

Removes browser margins and padding from HTML and body tag

Adds a game div element which will contain our Breakout clone

Loads Phaser v3.17 via their CDN

Loads our breakout.js file which currently does nothing but will contain our game logic

To effectively develop games with Phaser we'll need these files to be served by a web server. Without a web server, our browser won't allow our game script to load our assets for security reasons.

Luckily there's no need to set up Apache or Nginx to get a running web server. If you use VisualStudio Code like me then you can install the Live Server extension. Most IDEs and text editors have a plugin with similar functionality.

If you have Python version 3 installed you can go to your workspace via the terminal and enter python3 -m http.server. There are other CLI tools which provide simple web servers, choose the one that gives you the quickest time to develop your game.

Lastly, download the image assets we've created for this game from this link. Copy and paste the PNG files into the images folder.

Development tip - when you're developing a game you probably want to have the JavaScript console visible so you can see any errors that pop up. If you're using Chrome or Firefox, right click on the page and select "Inspect Element". A box should pop up from the bottom or side of your browser window. Select the "Console" tab to see updates errors or logs from our JavaScript code.

Creating our Game World

With our HTML and CSS set up, let's edit our breakout.js file to set up our game world.

Starting Phaser

First, we need to configure Phaser and create our Game instance. The Game instance is the central controller for a Phaser game, it does all the setup and kick starts the game loop for us.

Let's configure and create our Game instance:

// This object contains all the Phaser configurations to load our game const config = { type: Phaser.AUTO, parent: 'game', width: 800, heigth: 640, scale: { mode: Phaser.Scale.RESIZE, autoCenter: Phaser.Scale.CENTER_BOTH }, scene: { preload, create, update, }, physics: { default: 'arcade', arcade: { gravity: false }, } }; // Create the game instance const game = new Phaser.Game(config);

The type property tells Phaser what renderer to use. Phaser can render our game using HTML5's WebGL or Canvas element. By setting the type to Phaser.AUTO, we're telling Phaser to first try rendering with WebGL and if that fails, render using Canvas.

The parent property indicates the ID of the HTML element our game will be played in. We define our game dimensions in pixels with width and height. The scale object does two things for us:

mode tells Phaser how to use the space of our parent element, in this case, we ensure that the game fits the size of the parent div

autoCenter tells Phaser how to center our game within our parent div if we want to. In this case, we center our game vertically and horizontally within the parent div. This property is more useful when the game does not take up the entire space of the parent div, it's shown here as it's a frequently asked question.

In Phaser, our game logic is defined in Scenes. Think of scenes as various states in our game: the title screen is one scene, each level of a game would be their own scene, a cut scene would be its own scene, etc. Phaser provides a Scene object but it can also work with a regular JavaScript object containing the preload(), create() and update() functions defined.

The last configuration tells Phaser which physics engine to use. Phaser can use 3 different physic engines: Arcade, Impact and Matter. Arcade is the simplest one to get started with and is sufficient for our game needs.

Breakout does not need gravity to work, so we disable the property in our physics engine. If we were building a platformer we would probably enable gravity, so that when our players jump they will fall naturally back to the ground.

To ensure our game set up works we need to add the preload(), create() and update() functions. Add the following blank functions to after creating our game instance:

function preload() { } function create() { } function update() { }

With your web server running, navigate to the page where your game is running. You should see a blank screen like this:

The assets in this game consist of 5 images. In other games you may create, your assets can be huge. High definition images, audio, and video files could take up megabytes of space. The larger the asset, the longer it takes the load. For that reason, Phaser has a preload() function where we can load all the assets before we start running the game. It's never a nice user-experience to be playing a game and it suddenly slows down because it's trying to load new assets.

Change the preload() function to the following so we can load our images before the game loop kicks off:

function preload() { this.load.image('ball', 'assets/images/ball_32_32.png'); this.load.image('paddle', 'assets/images/paddle_128_32.png'); this.load.image('brick1', 'assets/images/brick1_64_32.png'); this.load.image('brick2', 'assets/images/brick2_64_32.png'); this.load.image('brick3', 'assets/images/brick3_64_32.png'); }

The first argument is the key we'll use later to reference the image, the second argument is the location of the image.

Note: - when we use this in our preload(), create(), and update() functions, we are referring to the scene run by the game instance that was created earlier.

With the images loaded, we want to place sprites on the screen. At the top of the breakout.js, add these variables that will contain our sprite data:

let player, ball, violetBricks, yellowBricks, redBricks, cursors;

Once they are globally defined, all our functions are able to use them.

Adding Sprites

A sprite is any 2D image that's part of a game scene. In Phaser, a sprite encapsulates an image along with its position, velocity, physical properties, and other properties. Let's begin by creating our player sprite via the create() function:

player = this.physics.add.sprite( 400, // x position 600, // y position 'paddle', // key of image for the sprite );

You should now be able to see a paddle on the screen:

The first argument of the sprite() method is the X Coordinate to place the sprite. The second argument is the Y Coordinate, and the last argument is key to the image asset added in the preload() function.

It's important to understand how Phaser and most 2D game frameworks use coordinates. The graphs we learned in school usually place the origin i.e. point (0, 0) at the center. In Phaser, the origin is at the top left of the screen. As x increases, we are essentially moving to the right. As y increases, we are moving downward.

Our game has a width of 800 pixels and a height of 640 pixels, so our game coordinates would look like this:

Let's add the ball to sit above the player. Add the following code to the create() function:

ball = this.physics.add.sprite( 400, // x position 565, // y position 'ball' // key of image for the sprite );

As the ball is above our player, the value of the Y Coordinate is lower than the player's Y Coordinate.

Adding Sprite Groups

While Phaser makes it easy to add sprites, it would quickly become tedious if every sprite would have to be defined individually. The bricks in Breakout are pretty much identical. The positions are different, but their properties, like color and how they interact with the ball, are the same. Instead of creating 30 brick sprite objects, we can create sprite groups to better manage them.

Let's add the first row of violet bricks via the create() function:

// Add violet bricks violetBricks = this.physics.add.group({ key: 'brick1', repeat: 9, setXY: { x: 80, y: 140, stepX: 70 } });

Instead of this.physics.add.sprite() we use this.physics.add.group() and pass a JavaScript object. The key property references the image key that all the sprites in the sprite group will use. The repeat property tells Phaser how many more times to create a sprite. Every sprite group creates one sprite. With repeat set to 9, Phaser will create 10 sprites in that sprite group. The setXY object has three interesting properties:

x is the X Coordinate of the first sprite

y is the Y Coordinate of the second sprite

stepX is the length in pixels between repeated sprites on the x-axis.

There is a stepY property as well but we don't need to use it for this game. Let's add the two other remaining sprite group for bricks:

// Add yellow bricks yellowBricks = this.physics.add.group({ key: 'brick2', repeat: 9, setXY: { x: 80, y: 90, stepX: 70 } }); // Add red bricks redBricks = this.physics.add.group({ key: 'brick3', repeat: 9, setXY: { x: 80, y: 40, stepX: 70 } });

Our game is already coming together, your screen should look like this:

Winning and Losing

It's good game development (and programming) practice to keep the end in sight. In Breakout, we can lose a game if our ball falls to the bottom of the screen. In Phaser, for the ball to be below the screen then the Y Coordinate of the ball is greater than the height of the game world. Let's create a function which checks this, add the bottom of the breakout.js add the following:

function isGameOver(world) { return ball.body.y > world.bounds.height; }

Our function takes the world object from the scene's physics property, which will be available in the update() function. It checks if the Y-coordinate of the ball sprite is greater than the height of the game world boundaries.

To win the game we need to get rid of all the bricks. Sprites in Phaser all have an active property. We can use that property to determine if we won or not. Sprite groups can count the number of active sprites contained within them. If there are no active sprites in each of the brick sprite groups i.e. there are 0 active brick sprites, then the player won the game.

Let's update the breakout.js file, by adding a check at the bottom:

function isWon() { return violetBricks.countActive() + yellowBricks.countActive() + redBricks.countActive() === 0; }

We accept each of the sprite groups as parameters, add the number of active sprites within them and check if it's equal to 0.

Now that we have defined our winning and losing conditions, we want Phaser to check them at the beginning of the game loop. As soon as the player wins or loses, the game should stop.

Let's update the update() function:

function update() { // Check if the ball left the scene i.e. game over if (isGameOver(this.physics.world)) { // TODO: Show "Game over" message to the player } else if (isWon()) { // TODO: Show "You won!" message to the player } else { // TODO: Logic for regular game time } }

Moving the Player with Keyboard Input

The player's movement depends on keyboard input. To be able to track keyboard input. It's time to use the cursors variable.

And at the bottom of our create() function:

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

Cursor keys in Phaser track the usage of 6 keyboard keys: up, right, down, left, shift and space.

Now we need to react to the state of our cursors object to update the position of our player. In the else clause of our update() function add the following:

// Put this in so that the player stays still if no key is being pressed player.body.setVelocityX(0); if (cursors.left.isDown) { player.body.setVelocityX(-350); } else if (cursors.right.isDown) { player.body.setVelocityX(350); }

Now we can move our player left to right!

You would notice that the player sprite is able to leave the game screen, ideally, it should not. We'll address that later when we handle collisions.

Waiting to Start

Before we add logic to move the ball, it would help if the game waited on user input before it moved. It's not a good experience to load a game and be immediately forced start. The player wouldn't have a fair time to react!

Let's move the ball upward after the player presses the spacebar. If the user moves the paddle to the left or the right, the ball will be moved as well so that's it's always at the center of the paddle.

First, we need our own variable to track whether a game was started or not. At the top of the breakout.js, after the declaration of our game variables, let's add:

let gameStarted = false;

Now, in the else clause of our update function:

if (!gameStarted) { ball.setX(player.x); if (cursors.space.isDown) { gameStarted = true; ball.setVelocityY(-200); } }

If the game has not started, set the X-coordinate of our ball to the player's center. A game object's coordinates is based on their center, so the x and y properties of sprites relate point to the center of our sprites.

Note that while it's fine to get a property's value like x by referencing it directly when setting properties we always try to use the appropriate setter function. The setter functions can include logic to validate our input, update another property or fire an event. It makes our code more predictable.

Like before with moving the player, we check if the space bar was pressed. If it was pressed, we switch the gameStarted flag to true so the ball would no longer follow the player's horizontal position, and set the Y-velocity of the ball to -200. Negative y-velocities send objects upward. For positive velocities, larger values move objects downward quicker. For negative velocities, smaller values move objects upward quicker.

Now when we move the player, the ball follows and if we press the spacebar the ball shoots upward:

You would observe a few things from how our game behaves so far:

1. The ball is rendered behind the bricks

2. The player can leave the bounds of the screen

3. The ball can leave the bounds of the screen

The ball is rendered behind the bricks because it was added to the game in our create function before the brick sprite groups. In Phaser, and generally with the HTML5 canvas element, the most recently added image is drawn on top of previous images.

The last two issues can be solved by adding some world collision.

Handling Collisions

World Collision

All our sprite interactions are defined in the create function. Enabling collision with the world scene is very easy with Phaser, add the following to the end of the create function:

player.setCollideWorldBounds(true); ball.setCollideWorldBounds(true);

It should give us output like this:

While the player movement is fine, the ball seems stuck at the top. To rectify this, we need to set the bounce property of the ball sprite. The bounce property would tell Phaser how much velocity to maintain after colliding with an object.

Add this to end of your create() function:

ball.setBounce(1, 1);

This tells phaser that the ball should maintain all of its X and Y-velocity. If we release the ball with the spacebar the ball should be bouncing up and down the game world. We need to disable the collision detection from the bottom part of the game world.

If we don't, we'll never know when it's game over. Disable collision with the bottom of the game world by adding this line at the end of the create function:

this.physics.world.checkCollision.down = false;

We should now have a game like this:

Brick Collision

Now that our moving sprites correctly collide with our game world, let's work on the collision between the ball and the bricks and then the ball and the player.

In our create() function add the following lines of code to the end:

this.physics.add.collider(ball, violetBricks, hitBrick, null, this); this.physics.add.collider(ball, yellowBricks, hitBrick, null, this); this.physics.add.collider(ball, redBricks, hitBrick, null, this);

The collider method tells Phaser's physics system to execute the hitBrick() function when the ball collides with various brick sprite groups.

Every time we press the space bar, the ball shoots upward. There's no X-velocity so the ball would come straight back to the paddle. That would be a boring game. Therefore, when we first hit a brick we'll set a random X-velocity.

At the bottom of the breakout.js define hitBrick below:

function hitBrick(ball, brick) { brick.disableBody(true, true); if (ball.body.velocity.x === 0) { randNum = Math.random(); if (randNum >= 0.5) { ball.body.setVelocityX(150); } else { ball.body.setVelocityX(-150); } } }

The hitBrick() function accepts the previous two arguments that were used in the collider() method, for example, ball and violetBricks. The disableBody(true, true) call on the brick tells Phaser to make it inactive and to hide it from the screen. If the X-velocity of the ball is 0, then we give the ball a velocity depending on the value of a random number.

If a small ball rolls towards your foot at a slow pace, on collision it would come to a halt. The Arcade Physics engine models the impact collision has on velocity by default. For our game, we don't want the ball to lose velocity when it hits a brick. We need to set the immovable property to our sprite groups to true.

Update the definitions of violetBricks, yellowBricks and redBricks to the following:

// Add violet bricks violetBricks = this.physics.add.group({ key: 'brick1', repeat: 9, immovable: true, setXY: { x: 80, y: 140, stepX: 70 } }); // Add yellow bricks yellowBricks = this.physics.add.group({ key: 'brick2', repeat: 9, immovable: true, setXY: { x: 80, y: 90, stepX: 70 } }); // Add red bricks redBricks = this.physics.add.group({ key: 'brick3', repeat: 9, immovable: true, setXY: { x: 80, y: 40, stepX: 70 } });

Our brick collision is now complete and our game should work like this:

Development tip - when developing the physics of your game, you may want to enable debug mode to see your sprites' boundary boxes and how they collide with one another. In your game config object, within the arcade property where we defined gravity, you can enable debugging by adding this to the object:

Player Collision

Managing collisions between the ball and player are a similar endeavor. First, let's ensure that the player is immovable. At the end of the create() function add the following:

player.setImmovable(true);

And then we add a collider between the ball and player:

this.physics.add.collider(ball, player, hitPlayer, null, this);

When the ball hits the player, we want two things to happen:

The ball should move a bit faster, to gradually increase the difficulty of the game

The ball's horizontal direction depends on which side of the player it hit - if the ball hits the left side of the player then it should go left, if it hits the right side of the player then it should go to the right.

To accommodate for these, let's update breakout.js with the hitPlayer() function:

function hitPlayer(ball, player) { // Increase the velocity of the ball after it bounces ball.setVelocityY(ball.body.velocity.y - 5); let newXVelocity = Math.abs(ball.body.velocity.x) + 5; // If the ball is to the left of the player, ensure the X-velocity is negative if (ball.x < player.x) { ball.setVelocityX(-newXVelocity); } else { ball.setVelocityX(newXVelocity); } }

Note: A sprite can collide with another sprite, a sprite can collide with a sprite group, and sprite groups can collide with each other. Phaser is smart enough to use the collision function we define appropriate in the context.

Now our game has both player and brick collision:

Adding Text

While our game is fully functional, someone playing this game would have no idea how to start or know when the game is over.

Let's add 3 new global variables that will store our text data after the gameStarted declaration at the top of the breakout.js:

let openingText, gameOverText, playerWonText;

Opening Text

Let's add some text when the game is loaded to tell the player to press space. In the create() function add the following code:

openingText = this.add.text( this.physics.world.bounds.width / 2, this.physics.world.bounds.height / 2, 'Press SPACE to Start', { fontFamily: 'Monaco, Courier, monospace', fontSize: '50px', fill: '#fff' } ); openingText.setOrigin(0.5);

The first two arguments of the text method are the X and Y-coordinates of the text box. We use the game scene's width and height to determine where it's placed - in the center. The third argument is the text to display. The fourth argument is a JS object that contains font related data.

Unlike sprites, text objects X and Y-coordinates refer to their top-left most point of the object, not their center. That's why we use the setOrigin() method to make the coordinates system work like sprites, in this case, it makes it easier to position in the center.

When we are playing, we don't want to see the opening text anymore. In the update() function, change the if statement that checks if the space bar was pressed to the following:

if (cursors.space.isDown) { gameStarted = true; ball.setVelocityY(-200); openingText.setVisible(false); }

Text objects are not sprites, we can't disable their bodies. We can make them invisible when we don't need to see them. Our game now begins like this:

Game Over and Game Won

Like before we need to add the text objects in the create() function, and make them invisible so they won't be seen when the game is started:

// Create game over text gameOverText = this.add.text( this.physics.world.bounds.width / 2, this.physics.world.bounds.height / 2, 'Game Over', { fontFamily: 'Monaco, Courier, monospace', fontSize: '50px', fill: '#fff' } ); gameOverText.setOrigin(0.5); // Make it invisible until the player loses gameOverText.setVisible(false); // Create the game won text playerWonText = this.add.text( this.physics.world.bounds.width / 2, this.physics.world.bounds.height / 2, 'You won!', { fontFamily: 'Monaco, Courier, monospace', fontSize: '50px', fill: '#fff' } ); playerWonText.setOrigin(0.5); // Make it invisible until the player wins playerWonText.setVisible(false);

Now they're defined, we have to change their visibility in the update() function:

// Check if the ball left the scene i.e. game over if (isGameOver(this.physics.world)) { gameOverText.setVisible(true); ball.disableBody(true, true); } else if (isWon()) { playerWonText.setVisible(true); ball.disableBody(true, true); } else { ... }

We disable the ball's body so it would stop being updated and displayed as it's no longer needed.

If we lose the game, we'll see this:

If we win the game, we'll see this:

Our Breakout clone is complete!

Conclusion

Phaser is an HTML5 game development framework that allows us to quickly build video games on the web. Aside from abstracting over the HTML5 APIs, it also provides us with useful utilities like physics engines and manages the game loop - the execution lifecycle of all games.

The best way to improve your game development skills is to keep building games. If you would like to learn more about game development with Phaser, then have a look at the official website's introductory tutorial.

You can view the annotated source code for the game here.

# data[].attributes.attachment_link.title
Introduction to Phaser 3: Building Breakout

# data[].attributes.attachment_link.description
Introduction Game development is a unique branch of software development that can be as rewarding as it is complex. When thinking of creating games, we usually think of an application to install and play on our computers or consoles. The HTML5 spec introduced many APIs to enable game development on

# data[].attributes.attachment_link.readable_text
Building a fast and scalable multiplayer game is no easy task. However, we're excited that you've landed on this tutorial, and our hope is by the end of it, you've got a good grasp on building and deploying a fully functioning multiplayer game.

Our demo multiplayer game is Ninja Platformer, a collaborative puzzle game that encourages you to work with your friends to collect the keys in clever ways. It's a perfect example as it combines a number of core multiplayer features, all bundled into one game.

Check out the demo below to see what you'll be building below. For the multiplayer experience, open it in two browsers, or play with a friend. Also check out the GitHub repo to for the entire source code.

Multiplayer Game Overview

Ninja Platformer is written in less than a 1000 lines of code! This feat is incredible for the amount of functionality this game has. The game uses PubNub's Data Stream Network to manage network traffic between players, and Functions to manage game state between devices.

It's written in JavaScript and the levels are generated via JSON files with information on the position of the platforms and game objects. Using Phasers Arcade Physics Library, each character and object has its own physics body with its own set of physics properties. Overall, it's an excellent showcase of both Phaser and PubNub's capabilities.

With that, let's get started! In the table of contents below, you'll see our major topics. We'll start with the basics and walk you through the bare bone basics of setting up an online game from scratch using simple assets and scripts.

Table of Contents

Setup Your Machine

In order to prepare your machine for HTML5 game development, you are going to need a few tools on your system. Luckily, you may already have some if you have done development in the past. This tutorial is aimed at people from all levels since it uses the minimum amount of tools necessary for development.

What you will need to make an HTML5 game:

Text Editor

Access to terminal / console on your computer

A local web server

Text Editor

You will use a text editor to code your game. There are tons of text editors out there. Choose whichever one you like best:

Terminal

You are going to need access to the terminal / command prompt in order to launch your local web server.

On Windows, it's called <b>Command Prompt</b> and you can access it by going to Start > Run > cmd

On MacOS, it's called <b>Terminal</b> app and you can find it in the Applications folder or by typing hitting the Command + Space for Spotlight then type in Terminal.

On Linux, most distributions have the terminal icon in the dock. In Ubuntu the application is called Terminal.

Launch a Local Server

In order for the Phaser game engine to run in your browser, you are going to need a local web server. The reason loading the files without a webserver will not work is because browsers block files from loading from different domains. Create a folder on the desktop and call it Ninja-Multiplayer-Platformer.

If you have Node

Execute this line to install the http-server package:
npm -g install http-server
Then navigate to the directory where you are going to create your files.

cd Desktop
cd Ninja-Multiplayer-Platformer
http-server

A Better Alternative

You can download a tool called Browser Sync which will automatically reload the browser every time you modify a file.
npm -g install browser-sync
This will launch the server and reload the browser when you modify any Javascript file.

cd Desktop
cd Ninja-Multiplayer-Platformer
browser-sync start --server --files ="**/*.js"

Mac OS or Linux (or Python installed on your system)

Go to your terminal application and run this:

cd Desktop
cd Ninja-Multiplayer-Platformer
python -m SimpleHTTPServer

If you don't have Node or Python, checkout this link for more details on how to setup your system.

Setting Up PubNub

Pub/Sub In a Nutshell

PubNub utilizes a Publish/Subscribe model for realtime data streaming and device signaling which lets you establish and maintain persistent socket connections to any device and push data to global audiences in less than ¼ of a second.

You can publish messages to any given channel, and subscribing clients receive only messages associated with that channel. The message payload can be any JSON data including numbers, strings, arrays, and objects.

Pub/Sub Use Cases and Scenarios

Chat: Sending and receiving messages, typing indicators, buddy lists, security

Locations and Connected Car: Dispatching taxi cabs, live tracking

Smart Sensors: Receiving data from a sensor for data visualizations and triggering device action

Healthcare: Monitoring heart rate from a patient's wearable device

Multiplayer Gaming

Interactive Media: Audience-participating voting system

Setting up Your PubNub Account

To build an application that leverages the PubNub Data Stream, you need to sign up for your account to obtain API keys.

Once you have successfully signed up you will be taken to the admin page.

You can edit the app name by clicking the App name ("My First PubNub App") and on the next screen by clicking the pencil icon next to the App name.

You should have your Publish Key and Subscribe Key under your app. (You do not need the Secret Key for now. You will only need this when you are using Access Manager APIs).

You can add additional apps by clicking the APPS tab on the top menu bar, then clicking New App.

You will get a new set of Publish and Subscribe Keys each time you create a new app.

Initialize Phaser

Download the project assets here.

Now in order to view your website, you have to launch your local web server. The instructions on how to do that are above. However when you cd to your project folder directory, you can run the python -m SimpleHTTPServer command in terminal. Then go to your web browser, and type in the IP and port that is listed in terminal. In some cases you can type in localhost:8000 or http://0.0.0.0:8000 Then navigate to the index.html file in your web browser and click on the link. You should now see a blank screen.

To begin, open up your main.js file. In the following code below that you should copy and paste, we are going to load the other Javascript files and initialize the phaser window.

// Load External Javascript files
const loadHeroScript = document.createElement('script');
loadHeroScript.src = './js/heroScript.js';
document.head.appendChild(loadHeroScript);

const loadLoadingState = document.createElement('script');
loadLoadingState.src = './js/loadingState.js';
document.head.appendChild(loadLoadingState);

const loadPlaystate = document.createElement('script');
loadPlaystate.src = './js/playState.js';
document.head.appendChild(loadPlaystate);

// =============================================================================
// Load the various phaser states and start game
// =============================================================================

window.addEventListener('load', () => {
  const game = new window.Phaser.Game(960, 600, window.Phaser.AUTO, 'game');
  game.state.disableVisibilityChange = true; // This allows two windows to be open at the same time and allow both windows to run the update function
  game.state.add('play', window.PlayState);
  game.state.add('loading', window.LoadingState);
  //window.createMyPubNub(0); // Connect to the PubNub network and run level code 0
  //window.StartLoading = function () {
    game.state.start('loading'); // Run the loading function once you successfully connect to the PubNub network
  //};
});

Now if you refresh your HTML window, you should see that the phaser window has been created. It should be completely black since there are no assets loaded in the scene yet. The window. Phaser.AUTO parameter is to specify whether we want a 2D canvas or a WebGL canvas. In this case, we will use WebGL by default but will fall back to 2D if it's not supported.

loadingState.js

Now lets create the loading state to load the assets into the scene. Open up your loadingState.js file and copy and paste the following code:

'use strict';

// =============================================================================
// Loading state
// =============================================================================

window.LoadingState = { // Create an object with all of the loading information inside of it
  init() {
    // keep crispy-looking pixels
    this.game.renderer.renderSession.roundPixels = true; // Make the phaser sprites look smoother
  },

  preload() {
    this.game.stage.disableVisibilityChange = true;

    // Load JSON levels
    this.game.load.json('level:0', 'data/level00.json');
    this.game.load.json('level:1', 'data/level01.json');
    this.game.load.json('level:2', 'data/level02.json');


    this.game.load.image('font:numbers', 'images/numbers.png');
    this.game.load.image('icon:coin', 'images/coin_icon.png');
    this.game.load.image('background', 'images/bg.png');
    this.game.load.image('invisible-wall', 'images/invisible_wall.png');
    this.game.load.image('ground', 'images/ground.png');
    this.game.load.image('grass:8x1', 'images/grass_8x1.png');
    this.game.load.image('grass:6x1', 'images/grass_6x1.png');
    this.game.load.image('grass:4x1', 'images/grass_4x1.png');
    this.game.load.image('grass:2x1', 'images/grass_2x1.png');
    this.game.load.image('grass:1x1', 'images/grass_1x1.png');
    this.game.load.image('key', 'images/key.png');

    this.game.load.spritesheet('decoration', 'images/decor.png', 42, 42);
    this.game.load.spritesheet('herodude', 'images/hero.png', 36, 42);
    this.game.load.spritesheet('hero', 'images/gameSmall.png', 36, 42);
    this.game.load.spritesheet('coin', 'images/coin_animated.png', 22, 22);
    this.game.load.spritesheet('door', 'images/door.png', 42, 66);
    this.game.load.spritesheet('icon:key', 'images/key_icon.png', 34, 30);

    this.game.load.audio('sfx:jump', 'audio/jump.wav');
    this.game.load.audio('sfx:coin', 'audio/coin.wav');
    this.game.load.audio('sfx:key', 'audio/key.wav');
    this.game.load.audio('sfx:stomp', 'audio/stomp.wav');
    this.game.load.audio('sfx:door', 'audio/door.wav');
    this.game.load.audio('bgm', ['audio/bgm.mp3', 'audio/bgm.ogg']);
  },

  create() {
    this.game.state.start('play', true, false, { level: window.globalCurrentLevel }); // Start Game
  }
};

Now lets look at what this code is doing. We created a object called window.LoadingState with all of the loading state information inside of it. In the init() function, we made the sprite objects in the game look smoother by using the Phaser API this.game.renderer.renderSession.roundPixels = true;

In the preload function, we load the JSON level information from the data folder. This data information will be used to generate the levels. Then every asset that we will use in the game needs to be preloaded into cache. We also load the various spritesheets and even preload the audio (we won't be using audio in this tutorial, however you can easily add it by uncommenting out code).

Lastly we run the create() function that starts the game and loads whatever the window.globalCurrentLevel is. Lets go set that variable in main.js.

Display Assets on the Screen

Navigate to main.js and at the top of the document add the code:

window.syncOtherPlayerFrameDelay = 0; //30 frames allows for 500ms of network jitter, to prevent late frames
window.currentChannelName; // Global variable for the current channel that your player character is on
window.currentFireChannelName; // Global variable that checks the current stage you are on to send the correct information to the PubNub Block
window.globalCurrentLevel = 0; // Global variable for the current level (index starts at 0)
window.UniqueID = window.PubNub.generateUUID(); // Generate a unique id for the player. Generated by the PubNub Network
window.globalLevelState = null; // Sets the globalLevelState to null if you aren't connected to the network. Once connected, the level will generate to the info that was on the block.
window.globalWasHeroMoving = true;
// console.log('UniqueID', UniqueID); // Print out your clientsr Unique ID
window.text1 = 'Level 1 Occupancy: 0'; // Global text objects for occupancy count
window.text2 = 'Level 2 Occupancy: 0';
window.text3 = 'Level 3 Occupancy: 0';
let textResponse1;
let textResponse2;
let textResponse3;
window.updateOccupancyCounter = false; // Occupancy Counter variable to check if the timer has already been called in that scene
window.keyMessages = [];

This created global variables necessary for the next part of the tutorial. Now go to playState.js and instantiate the PlayState game state and set up some variables:

const keyStates = {};
let keyCollected = false;
window.frameCounter = 0;

window.PlayState = {

}

Now inside of the window.PlayState object, add the first part of the code:

init(data) {
  this.keys = this.game.input.keyboard.addKeys({
    left: window.Phaser.KeyCode.LEFT,
    right: window.Phaser.KeyCode.RIGHT,
    up: window.Phaser.KeyCode.UP
  });
  this.coinPickupCount = 0;
  keyCollected = false;
  this.level = (data.level || 0);
},
create() {
  window.globalGameState = this;
  // fade in  (from black)
  this.camera.flash('#000000');
  // create sound entities
  this.sfx = {
    jump: this.game.add.audio('sfx:jump'),
    coin: this.game.add.audio('sfx:coin'),
    key: this.game.add.audio('sfx:key'),
    stomp: this.game.add.audio('sfx:stomp'),
    door: this.game.add.audio('sfx:door')
  };
  // create level entities and decoration
  this.game.add.image(0, 0, 'background');
  window.textObject1 = this.game.add.text(700, 5, window.text1, { font: 'Bold 200px Arial', fill: '#000000', fontSize: '20px' });
  window.textObject2 = this.game.add.text(700, 35, window.text2, { font: 'Bold 200px Arial', fill: '#000000', fontSize: '20px' });
  window.textObject3 = this.game.add.text(700, 65, window.text3, { font: 'Bold 200px Arial', fill: '#000000', fontSize: '20px' });
  if (window.globalLevelState === null) {
    window.globalLevelState = {
      time: 0,
      coinCache: this.game.cache.getJSON(`level:${this.level}`)
    };
  }

  this._loadLevel(window.globalLevelState.coinCache);
  // this._loadLevel(window.globalLevelState.value);
  // create UI score boards
  this._createHud();
},

Now lets look at each part of the code. In the init(data) function, we set this.keys to be the command to detect which key on the keyboard has been pressed. Since this is the initialize function in the play state, certain variables must be set for later use.

In the create() {} function, we set some more variables and also make the screen fade in upon loading the web page by using the Phaser API call this.camera.flash('#000000');

Then we setup our sound effect variables in the this.sfx object. Currently in this tutorial, all of the sound effects are commented out, however you can easily go uncomment them later.

We then set the background image by calling the command this.game.add.image(0, 0, 'background');

Next we set the text objects that will be used to detect presence events in each room. We have an if statement that checks to see if the window.globalLevelState is equal to null. If it is equal, we set the coinCache equal to level:0 since we want the scene to load the first level if it doesn't receive any information from the PubNub Block.

We then call the _loadLevel() function and also the _createHud() function.

If we run this code as is, we will get errors since we are calling functions that we haven't created yet. Let's add some more code inside of window.playState = {} and below the create(){} function:

_loadLevel(data) {
  // console.log(data)
  // create all the groups/layers that we need
  this.bgDecoration = this.game.add.group();
  this.platforms = this.game.add.group();
  this.coins = this.game.add.group();

  // spawn hero and enemies
  this._spawnCharacters({ hero: data.hero, spiders: data.spiders });

  // spawn level decoration
  data.decoration.forEach(function (deco) {
    this.bgDecoration.add(
      this.game.add.image(deco.x, deco.y, 'decoration', deco.frame));
  }, this);

  // spawn platforms
  data.platforms.forEach(this._spawnPlatform, this);

  // spawn important objects
  data.coins.forEach(this._spawnCoin, this);
  this._spawnKey(data.key.x, data.key.y);
  this._spawnDoor(data.door.x, data.door.y);

  // enable gravity
  const GRAVITY = 1200;
  this.game.physics.arcade.gravity.y = GRAVITY;
},
_spawnPlatform(platform) {
  const sprite = this.platforms.create(platform.x, platform.y, platform.image);
  // physics for platform sprites
  this.game.physics.enable(sprite);
  sprite.body.allowGravity = false;
  sprite.body.immovable = true;
},
_spawnCoin(coin) {
  const sprite = this.coins.create(coin.x, coin.y, 'coin');
  sprite.anchor.set(0.5, 0.5);
  // physics (so we can detect overlap with the hero)
  this.game.physics.enable(sprite);
  sprite.body.allowGravity = false;
  // animations
  sprite.animations.add('rotate', [0, 1, 2, 1], 6, true); // 6fps, looped
  sprite.animations.play('rotate');
},
_addOtherCharacter(uuid) {
  // console.log('Added another character to game');
  if (window.globalOtherHeros.has(uuid)) { return; }
  // console.log('_addOtherCharacter', uuid);
  this.hero2 = new window.Hero(this.game, 10, 10);
  this.hero2.lastKeyFrame = 0;
  this.game.add.existing(this.hero2);
  window.globalOtherHeros.set(uuid, this.hero2);
},
_removeOtherCharacter(uuid) {
  if (!window.globalOtherHeros.has(uuid)) { return; }
  window.globalOtherHeros.get(uuid).destroy();
  window.globalOtherHeros.delete(uuid);
},
_spawnCharacters(data) {
  this.hero = new window.Hero(this.game, 10, 10);
  this.hero.body.bounce.setTo(0);
  const playerText = this.game.add.text(this.hero.position.x - 10, this.hero.position.y - 550, 'me', { fill: '#000000', fontSize: '15px' });
  playerText.anchor.set(0.5);
  this.hero.addChild(playerText);
  // console.log(playerText.position.x, playerText.position.y);
  window.globalMyHero = this.hero;
  window.globalOtherHeros = this.otherHeros = new Map();
  this.game.add.existing(this.hero);
  // globalMyHero.alpha = 1; //compensating for lag
  // window.sendKeyMessage({}); // UNCOMMENT LATER
},
_spawnKey(x, y) {
  this.key = this.bgDecoration.create(x, y, 'key');
  this.key.anchor.set(0.5, 0.5);
  // enable physics to detect collisions, so the hero can pick the key up
  this.game.physics.enable(this.key);
  this.key.body.allowGravity = false;
  // add a small 'up & down' animation via a tween
  this.key.y -= 3;
  this.game.add.tween(this.key)
    .to({ y: this.key.y + 6 }, 800, window.Phaser.Easing.Sinusoidal.InOut)
    .yoyo(true)
    .loop()
    .start();
},

_spawnDoor(x, y) {
  this.door = this.bgDecoration.create(x, y, 'door');
  this.door.anchor.setTo(0.5, 1);
  this.game.physics.enable(this.door);
  this.door.body.allowGravity = false;
},
_createHud() {
  const NUMBERS_STR = '0123456789X ';
  this.coinFont = this.game.add.retroFont('font:numbers', 20, 26, NUMBERS_STR, 6);

  this.keyIcon = this.game.make.image(0, 19, 'icon:key');
  this.keyIcon.anchor.set(0, 0.5);

  const coinIcon = this.game.make.image(this.keyIcon.width + 7, 0, 'icon:coin');
  const coinScoreImg = this.game.make.image(coinIcon.x + coinIcon.width, coinIcon.height / 2, this.coinFont);
  coinScoreImg.anchor.set(0, 0.5);

  this.hud = this.game.add.group();
  this.hud.add(coinIcon);
  this.hud.add(coinScoreImg);
  this.hud.add(this.keyIcon);
  this.hud.position.set(10, 10);
},

Let's look at each section of the above code to see what it's doing.

_loadLevel(data) creates asset groups that we are need later on in the code. Then it spawns all of the level decorations (the mushrooms, grass etc) from the JSON file information that we stored into cache earlier in the code. Next we spawn the animated coins, the key and the door into the level along with setting the gravity constant and turning on gravity.

_spawnPlatform(platform) spawns each platform object and turns them into a sprite. Then they are set to not be affected by gravity and also set to be immovable so other sprite objects can't impact their position.

_spawnCoin(coin) creates each coin asset and places them on the screen, and adds their animations.

_addOtherCharacter(uuid) adds a hero that is not your own to the screen when someone else connects to the same PubNub channel.

_spawnCharacters(data) spawns the hero asset into the game. The hero information is defined in heroScript.js that we are going to create in a bit. We also set playerText to appear above your player so that way when we make the game multiplayer, you can tell who's apart.

_spawnKey(x,y) creates the key that unlocks the door. A tween is applied to give the key the animation effect.

_spawnDoor(x,y) places the door in a set position.

_createHud() creates the overlay at the top left of the screen that checks to see if you have collected the key for that level and also how many coins you have collected.

Keep in mind you will still get errors in the console since we haven't yet wrote the heroScript.js code. Lets do that now.

Hero Script

Open up your heroScript.js document and copy and paste the follow code then save:

'use strict';

// =============================================================================
// Create Player (Hero)
// =============================================================================
window.Hero = class Hero extends window.Phaser.Sprite {
  constructor(game) {
    super();
    window.Phaser.Sprite.call(this, game, 10, 523, 'hero');
    // anchor
    this.anchor.set(0.5, 0.5);
    // physics properties
    this.game.physics.enable(this);
    this.body.collideWorldBounds = true;
    // animations
    this.animations.add('stop', [0]);
    this.animations.add('run', [1, 2], 8, true); // 8fps looped
    this.animations.add('jump', [3]);
    this.animations.add('fall', [4]);
    // starting animation
    this.animations.play('stop');
  }

  move(direction) {
    // guard
    if (this.isFrozen) { return; }
    const SPEED = 200;

    this.body.velocity.x = direction * SPEED;

    // update image flipping & animations
    if (this.body.velocity.x < 0) {
      this.scale.x = -1;
    } else if (this.body.velocity.x > 0) {
      this.scale.x = 1;
    }
  }

  jump() {
    // Hero jumping code
    const JUMP_SPEED = 600;
    const canJump = this.body.touching.down && this.alive && !this.isFrozen;
    // console.log({
    //   canJump: canJump,
    //   'this.body.touching.down': this.body.touching.down,
    //   'this.alive': this.alive,
    //   'this.isFrozen': this.isFrozen
    // });

    if (canJump || this.isBoosting) {
      this.body.velocity.y = -JUMP_SPEED;
      this.isBoosting = true;
    }
    return canJump;
  }

  update() {
    // update sprite animation, if it needs changing
    const animationName = this._getAnimationName();
    if (this.animations.name !== animationName) {
      this.animations.play(animationName);
    }
  }

  freeze() { // When player goes through door do animation and remove player
    this.body.enable = false;
    this.isFrozen = true;
  }

  // returns the animation name that should be playing depending on
  // current circumstances
  _getAnimationName() {
    let name = 'stop'; // default animation
    if (this.isFrozen) {
      name = 'stop';
    } else if (this.body.velocity.y < 0) {
      name = 'jump';
    } else if (this.body.velocity.y >= 0 && !this.body.touching.down) {
      name = 'fall';
    } else if (this.body.velocity.x !== 0 && this.body.touching.down) {
      name = 'run';
    }
    return name;
  }
};

heroScript.js sets up the player animations and handles the player movement. In the move(direction) function, we determine what direction the player should be facing depending upon the velocity of the player.

In jump() we set the properties to determine if the player can jump or not. In update() we update the sprite animation only if it needs to be changed. In freeze() we play the animate of the player going through the door. In _getAnimationName() we set the various animation names depending upon the hero body's velocity.

Now if you copy and pasted this code correctly, save your document then refresh your browser window. You should see something like this:

Player Movement

Now we are going to add the code for the player movement to work. Below the create() {} function in playState.js, paste the following code:

update() {
  window.frameCounter++;
  this._handleCollisions();
  this._handleInput();
  // update scoreboards
  this.coinFont.text = `x${this.coinPickupCount}`;
  this.keyIcon.frame = keyCollected ? 1 : 0;
},

shutdown() {
  // this.bgm.stop();
},

_canHeroEnterDoor(hero) {
  return keyCollected && hero.body.touching.down;
},

The update() function adds one to the frame count every frame. This is used to sync the player movements across all devices without the need to send PubNub publishes every frame. It also calls the _handleInput() and _handleCollisions() function every frame.

The shutdown() function is used to stop the background music from playing if you so wish to enable it (for this tutorial sound effects are commented out).

The _canHeroEnterDoor(hero) function checks to see if the key was collect and the hero is touching the platform object in order to enter through the door.

Now lets add the _handleCollisions() function to the game. Add this code right under the last code you just copy and pasted:

_handleCollisions() {
   for (let i = 0; i < 2; i++) { // prevent collisions for pushing thru
     this.game.physics.arcade.collide(this.hero, this.platforms);
     for (const uuid of window.globalOtherHeros.keys()) {
       const otherplayer = window.globalOtherHeros.get(uuid);
       this.game.physics.arcade.collide(otherplayer, this.platforms, null, null, this);
       this.game.physics.arcade.overlap(otherplayer, this.coins, this._onHeroVsCoin, null, this);
       this.game.physics.arcade.overlap(otherplayer, this.key, this._onHeroVsKey, null, this);
       this.game.physics.arcade.overlap(otherplayer, this.door, this._onOtherHeroVsDoor, this._canHeroEnterDoor, this);
     }
     // hero vs coins (pick up)
     this.game.physics.arcade.overlap(this.hero, this.coins, this._onHeroVsCoin, null, this);
     // hero vs key (pick up)
     this.game.physics.arcade.overlap(this.hero, this.key, this._onHeroVsKey, null, this);
     // hero vs door (end level)
     this.game.physics.arcade.overlap(this.hero, this.door, this._onHeroVsDoor, this._canHeroEnterDoor, this);
     // ignore if there is no key or the player is on air
   }
 },

The _handleCollisions() function handles all of the scenes object collision events. For instance if the hero collides with a key, it will run the this._onHeroVsKey function.

Now lets add the _handleInput() function. Code is commented out here until we add the PubNub portion. Then we will go back and uncomment the code to send key event messages to the PubNub network. Add this code under the _handleCollisions() function:

_handleInput() {
  // handleKeyMessages(); // UNCOMMENT LATER
  //  logCurrentState(this.game);
  if (this.hero) { // Added this so we can control spawning of heros
    if (this.keys.left.isDown) {
      if (!keyStates.leftIsDown) {
        // console.log('left pushed');
        // window.sendKeyMessage({ left: 'down' });  // UNCOMMENT LATER
      }
      keyStates.leftIsDown = true;
    } else {
      if (keyStates.leftIsDown) {
        // console.log('left un-pushed');
        // window.sendKeyMessage({ left: 'up' }); // UNCOMMENT LATER
      }
      keyStates.leftIsDown = false;
    }

    if (this.keys.right.isDown) {
      if (!keyStates.rightIsDown) {
        // console.log('right pushed');
        // window.sendKeyMessage({ right: 'down' }); // UNCOMMENT LATER
      }
      keyStates.rightIsDown = true;
    } else {
      if (keyStates.rightIsDown) {
        // console.log('right un-pushed');
        // window.sendKeyMessage({ right: 'up' }); // UNCOMMENT LATER
      }
      keyStates.rightIsDown = false;
    }

    if (this.hero.body.touching.down) {
      if (this.keys.up.isDown) {
        if (!keyStates.upIsDown) {
          // window.sendKeyMessage({ up: 'down' }); // UNCOMMENT LATER
          window.globalMyHero.jump();
        }
        keyStates.upIsDown = true;
      } else {
        if (keyStates.upIsDown) {
          // console.log('up un-pushed');
          // window.sendKeyMessage({ up: 'up' }); // UNCOMMENT LATER
        }
        keyStates.upIsDown = false;
      }
    }

    if (this.keys.left.isDown) { // move hero left
      this.hero.move(-1);
    } else if (this.keys.right.isDown) { // move hero right
      this.hero.move(1);
    } else { // stop
      this.hero.move(0);
    }

    // handle jump
    const JUMP_HOLD = 10;// 200; // ms
    if (this.keys.up.downDuration(JUMP_HOLD)) {
      // let didJump = this.hero.jump();
      // if (didJump) { this.sfx.jump.play();}
    }

    for (const uuid of window.globalOtherHeros.keys()) {
      const otherplayer = window.globalOtherHeros.get(uuid);
      if (Date.now() + JUMP_HOLD <= otherplayer.jumpStart) {
        // otherplayer.jump();
      }
      if (otherplayer.goingLeft) { // move hero left
        otherplayer.move(-1);
      } else if (otherplayer.goingRight) { // move hero right
        otherplayer.move(1);
      } else { // stop
        otherplayer.move(0);
      }
    }
  }

  if (window.globalWasHeroMoving && this.hero.body.velocity.x === 0 && this.hero.body.velocity.y === 0 && this.hero.body.touching.down) {
    // window.sendKeyMessage({ stopped: 'not moving' }); // UNCOMMENT LATER
    console.log('stopped');
    window.globalWasHeroMoving = false;
  } else if (window.globalWasHeroMoving || this.hero.body.velocity.x !== 0 || this.hero.body.velocity.y !== 0 || !this.hero.body.touching.down) {
    window.globalWasHeroMoving = true;
  }
},

The _handleInput() function runs every frame and checks to see if your hero object exists on the screen. If it does, it checks every frame to see if any of the keys have been pressed down. If it has been pressed down, send a message that the button pressed is up. This is a basic boolean if statement.

The next part of the code actually moves the character by calling this.hero.move(-1), this.hero.move(1) or this.hero.move(0). That calls the move function in heroScript.js.

The next portion of the code is going to be used later on in the tutorial, but is there to check to see which UUID matches with what player object on the screen. If there is a match, it moves the players based off what messages it's receiving from the PubNub callback which we are going to code later on in this tutorial.

In the next portion, of code we check to see if the player has stopped moving entirely. If they have we send out a message to everyone declaring that they are standing still.

When you save all of your documents and refresh your web browser, you should see this screen and should be able to move your character around using the left, right and up arrows. Try it out:

Add Object Collisions

Now lets add the functions that allow the players on the screen to interact with the objects on screen. Each function is a function that is called when that specific collision event occurs. Most of the logic here is simply animations. The _goToNextLevel() function loads the next level and if the level is equal to the last level, it will restart you at the first level. Copy and past this code below the _handleInput() function:

_onHeroVsKey(hero, key) {
  // this.sfx.key.play();
  this.door.frame = 1;
  key.kill();
  keyCollected = true;
  window.sendKeyMessage({ keyCollected });
},

_onHeroVsCoin(hero, coin) {
  // this.sfx.coin.play();
  coin.kill();
  logCurrentStateCoin(this.game, coin);
  this.coinPickupCount++;
},

_onHeroVsDoor(hero, door) {
    // 'open' the door by changing its graphic and playing a sfx
  door.frame = 1;
    // this.sfx.door.play();
    // play 'enter door' animation and change to the next level when it ends
  hero.freeze();
  this.game.add.tween(hero)
    .to({ x: this.door.x, alpha: 0 }, 0, null, true)
    .onComplete.addOnce(this._goToNextLevel, this);
},

_onOtherHeroVsDoor(hero, door) {
  // 'open' the door by changing its graphic and playing a sfx
  door.frame = 1;
  // this.sfx.door.play();
  // play 'enter door' animation and change to the next level when it ends
  hero.freeze();
  this.game.add.tween(hero)
    .to({ x: this.door.x, alpha: 0 }, 500, null, true);
},
_goToNextLevel() {
  this.camera.fade('#000000');
  this.camera.onFadeComplete.addOnce(function () {
    window.globalUnsubscribe();
    window.updateOccupancyCounter = false;
    if (this.level === 2) {
      window.createMyPubNub(0);
    } else {
      window.createMyPubNub(this.level + 1);
    }
  }, this);
},

If you refresh the window, you will get an error since we haven't defined logCurrentStateCoin yet. Go to the top of playState.js and add the following code right below the window.frameCounter variable:

function logCurrentStateCoin(game, coin) {
  // Log Current Game State of Collected Coins
  for (const value of window.globalLevelState.coinCache.coins) {
    if (coin.x === value.x) {
      window.globalLevelState.coinCache.coins.splice(window.globalLevelState.coinCache.coins.indexOf(value), 1);
      // console.log(value)
    }
  }
  window.fireCoins();
  // console.log(window.globalLevelState.coinCache.coins)
}

When you copy and paste that code, you will still get an error since window.fireCoins() is a function that has not yet been defined. Go ahead and comment out that line of code by using the // tags in front of the statement.

Now refresh your window and you should be able to move your player around and collect the coins without trouble. However, if you try to collect the key, you will get an error since we have not yet defined the sendKeyMessage() function.

Handle Messages

Now lets add the handleKeyMessages() function to the game so we can start implementing the multiplayer components. This function handles all of the messages that get received by the client. Essentially what it's doing is syncing all the clients up to each other so the movements are accurately displayed on the screen. Copy and paste this code in playState.js right below the logCurrentStateCoin(game, coin) function:

function handleKeyMessages() {
  const earlyMessages = [];
  const lateMessages = [];
  window.keyMessages.forEach((messageEvent) => {
    if (window.globalOtherHeros) { // If player exists
      if (messageEvent.channel === window.currentChannelName) { // If the messages channel is equal to your current channel
        if (!window.globalOtherHeros.has(messageEvent.message.uuid)) { // If the message isn't equal to your uuid
          window.globalGameState._addOtherCharacter(messageEvent.message.uuid); // Add another player to the game that is not yourself

          const otherplayer = window.globalOtherHeros.get(messageEvent.message.uuid);
          otherplayer.position.set(messageEvent.message.position.x, messageEvent.message.position.y); // set the position of each player according to x y
          otherplayer.initialRemoteFrame = messageEvent.message.frameCounter;
          otherplayer.initialLocalFrame = window.frameCounter;
          window.sendKeyMessage({}); // Send publish to all clients about user information
        }
        if (messageEvent.message.position && window.globalOtherHeros.has(messageEvent.message.uuid)) { // If the message contains the position of the player and the player has a uuid that matches with one in the level
          window.keyMessages.push(messageEvent);
          const otherplayer = window.globalOtherHeros.get(messageEvent.message.uuid);
          const frameDelta = messageEvent.message.frameCounter - otherplayer.lastKeyFrame;
          const initDelta = otherplayer.initialRemoteFrame - otherplayer.initialLocalFrame;
          const frameDelay = (messageEvent.message.frameCounter - window.frameCounter) - initDelta + window.syncOtherPlayerFrameDelay;

          /*console.log({
            lastKeyFrame: otherplayer.lastKeyFrame,
            frameCounter: messageEvent.message.frameCounter,
            frameDelta,
            rf_lf: otherplayer.initialRemoteFrame - otherplayer.initialLocalFrame,
            frameDelay
          });*/
          if (frameDelay > 0) {
            if (!messageEvent.hasOwnProperty('frameDelay')) {
              messageEvent.frameDelay = frameDelay;
              otherplayer.totalRecvedFrameDelay += frameDelay;
              otherplayer.totalRecvedFrames++;
            //console.log('avgFrameDelay', otherplayer.totalRecvedFrameDelay / otherplayer.totalRecvedFrames);
            }
            earlyMessages.push(messageEvent);
            //console.log('initDelta', initDelta, 'early', frameDelay);
            //console.log('early', frameDelay);
            return;
          } else if (messageEvent.message.keyMessage.stopped === 'not moving') {
            console.log('initDelta', initDelta, 'stopping player');
            otherplayer.body.position.set(messageEvent.message.position.x, messageEvent.message.position.y);
            otherplayer.body.velocity.set(0, 0);
            otherplayer.goingLeft = false;
            otherplayer.goingRight = false;
            if (otherplayer.totalRecvedFrames > 0) {
              const avgFrameDelay = otherplayer.totalRecvedFrameDelay / otherplayer.totalRecvedFrames;
              const floorFrameDelay = Math.floor(avgFrameDelay);
            //console.log('otherplayer.initialRemoteFrame before', otherplayer.initialRemoteFrame);
              otherplayer.initialRemoteFrame += floorFrameDelay - 7;
            //console.log('otherplayer.initialRemoteFrame after', otherplayer.initialRemoteFrame);
              console.log('avg frame delay', avgFrameDelay, 'adjusting delta', floorFrameDelay);
            }
            otherplayer.totalRecvedFrameDelay = 0;
            otherplayer.totalRecvedFrames = 0;
          } else if (frameDelay < 0) {
            otherplayer.totalRecvedFrameDelay += frameDelay;
            otherplayer.totalRecvedFrames++;
            lateMessages.push(messageEvent);
            console.log('initDelta', initDelta, 'late', frameDelay);
            return;
          } else {
          //console.log('initDelta', initDelta, 'ontime', frameDelay);
          }

          otherplayer.lastKeyFrame = messageEvent.message.frameCounter;

          if (messageEvent.message.keyMessage.up === 'down') { // If message equals arrow up, make the player jump with the correct UUID
            otherplayer.jump();
            otherplayer.jumpStart = Date.now();
          } else if (messageEvent.message.keyMessage.up === 'up') {
            otherplayer.jumpStart = 0;
          }
          if (messageEvent.message.keyMessage.left === 'down') { // If message equals arrow left, make the player move left with the correct UUID
            otherplayer.goingLeft = true;
          } else if (messageEvent.message.keyMessage.left === 'up') {
            otherplayer.goingLeft = false;
          }
          if (messageEvent.message.keyMessage.right === 'down') { // If message equals arrow down, make the player move right with the correct UUID
            otherplayer.goingRight = true;
          } else if (messageEvent.message.keyMessage.right === 'up') {
            otherplayer.goingRight = false;
          }
        }
      }
    }
  });

  if (lateMessages.length > 0) {
  //console.log({ lateMessages, earlyMessages });
  }
  window.keyMessages.length = 0;
  earlyMessages.forEach((em) => {
    window.keyMessages.push(em);
  });
}

This function handles all messages coming from other clients that are connected to the game. The function won't do anything right now until we add the multiplayer components to the game.

However let's take a quick look at what this function is doing. We start out by taking the message data and checking to see if the message is equal to the current channel you are subscribed too and if you aren't the one sending the message. If you receive a message from someone who is not in the game, create a new player and set their position. We then send a message to update all clients about their new player position.

If you receive a message from someone who is not in the game, create a new player and set their position. We then send a message to update all clients about their new player position.

The handleKeyMessages() function also checks frame count to make sure all clients are in sync. Also we check the messageEvent.message.keyMessage for the input events of all other users and will update their players state on all clients.

Adding PubNub

Now we are going to add the PubNub portion of the code into the game to allow other players to join the game. In your main.js file, add this section of code after the variables you set up and above the javascript files you loaded into the scene:

window.createMyPubNub = function (currentLevel) {
  // console.log('createMyPubNub', currentLevel);
  window.globalCurrentLevel = currentLevel; // Get the current level and set it to the global level
  window.currentFireChannelName = 'realtimephaserFire2';
  window.currentChannelName = `realtimephaser${currentLevel}`; // Create the channel name + the current level. This way each level is on its own channel.
  let checkIfJoined = false; // If player has joined the channel

  // Setup your PubNub Keys
  window.pubnub = new window.PubNub({
    publishKey: 'ADD-YOUR-PUBNUB-PUBKEY-HERE',
    subscribeKey: 'ADD-YOUR-PUBNUB-SUBKEY-HERE',
    uuid: window.UniqueID,
  });

  // Subscribe to the two PubNub Channels
  window.pubnub.subscribe({
    channels: [window.currentChannelName, window.currentFireChannelName],
    withPresence: true,
  });

  // ADD LISTENER HERE

  // If person leaves or refreshes the window, run the unsubscribe function
  window.addEventListener('beforeunload', () => {
    navigator.sendBeacon(`https://pubsub.pubnub.com/v2/presence/sub_key/mySubKey/channel/ch1/leave?uuid=${window.UniqueID}`); // pub
    window.globalUnsubscribe();
  });

  // Unsubscribe people from PubNub network
  window.globalUnsubscribe = function () {
    try {
      // console.log('unsubscribing', window.currentChannelName);
      window.pubnub.unsubscribe({
        channels: [window.currentChannelName, window.currentFireChannelName],
        withPresence: true
      });
      window.pubnub.removeListener(window.listener);
    } catch (err) {
      // console.log("Failed to UnSub");
    }
  };
  window.pubnub.addListener(window.listener);
};

This function sets up some variables and channel names that PubNub is going to use for network communication. We set window.currentChannelName to equal whatever current level the user is on. We then setup the PubNub keys and subscribe to the channels specified. Then we add a listener that when the browser is unloaded, it sends a beacon that a user has left the channel so the presence event updates for all other clients. We also have a globalUnsubscribe function that removes the listener for the client and subscribes them from the channel.

Then we add a listener that when the browser is unloaded, it sends a beacon that a user has left the channel so the presence event updates for all other clients. We also have a globalUnsubscribe function that removes the listener for the client and subscribes them from the channel.

Setup Your PubNub Dashboard

Take a look at your publish and subscribe key. You will need to add your own Publish and Subscribe keys in order for the game to work. Now if you haven't already, create a PubNub account.

Once you're in the Admin Dashboard, name your application whatever you wish, and click the Create New App button. Once you create the application, click on the application to few the key information. You should see that you have two keys, a Publish Key, and a Subscribe Key. Click on the demo keyset, and it should load up a page that shows your keys in addition to Application Add-Ons. In the Application Add-Ons section, turn ON Presence and check Generate Leave on TCP FIN or RST and Global Here Now. Also turn ON PubNub Functions. Make sure to have Access Manager turned off or else the sample code won't work since you need to include a secret key.

Click on the demo keyset, and it should load up a page that shows your keys in addition to Application Add-Ons. In the Application Add-Ons section, turn ON Presence and check Generate Leave on TCP FIN or RST and Global Here Now. Also turn ON PubNub Functions. Make sure to have Access Manager turned off or else the sample code won't work since you need to include a secret key.

The code you have written so far still won't work since we haven't added the callback listener that will listen for all messages sent through the PubNub Network on your channel while the client is connected.

Let's add the following code where the comment says ADD LISTENER HERE:

// Create PubNub Listener for message events
window.listener = {
  status() {
    // Send fire event to connect to the block
    const requestIntMsg = { requestInt: true, currentLevel: window.globalCurrentLevel, uuid: window.UniqueID };
    window.pubnub.fire({
      message: requestIntMsg,
      channel: window.currentFireChannelName,
      sendByPost: false
    });
  },

  message(messageEvent) {
    if (messageEvent.message.uuid === window.UniqueID) {
      return; // this blocks drawing a new character set by the server, to lower latency
    }
    if (messageEvent.channel === window.currentFireChannelName) {
      window.globalLastTime = messageEvent.timetoken; // Set the timestamp for when you send fire messages to the block
      if (messageEvent.message.int === true && messageEvent.message.sendToRightPlayer === window.UniqueID) { // If you get a message and it matches with your UUID
        window.globalLevelState = messageEvent.message.value; // Set the globalLevelState to the information set on the block
        window.StartLoading(); // Call the game state start function in onLoad
      }
    }
    if (window.globalOtherHeros) { // If player exists
      if (messageEvent.channel === window.currentChannelName) { // If the messages channel is equal to your current channel
        if (!window.globalOtherHeros.has(messageEvent.message.uuid)) { // If the message isn't equal to your uuid
          window.globalGameState._addOtherCharacter(messageEvent.message.uuid); // Add another player to the game that is not yourself
          window.sendKeyMessage({}); // Send publish to all clients about user information
          const otherplayer = window.globalOtherHeros.get(messageEvent.message.uuid);
          otherplayer.position.set(messageEvent.message.position.x, messageEvent.message.position.y); // set the position of each player according to x y
          otherplayer.initialRemoteFrame = messageEvent.message.frameCounter;
          otherplayer.initialLocalFrame = window.frameCounter;
          otherplayer.totalRecvedFrameDelay = 0;
          otherplayer.totalRecvedFrames = 0;
        }
        if (messageEvent.message.position && window.globalOtherHeros.has(messageEvent.message.uuid)) { // If the message contains the position of the player and the player has a uuid that matches with one in the level
          window.keyMessages.push(messageEvent);
        }
      }
    }
  },

  presence(presenceEvent) { // PubNub on presence message / event
    let occupancyCounter;

    function checkFlag() {  // Function that reruns until response
      if (window.globalOtherHeros && checkIfJoined === true) { // If the globalother heros exists and if the player joined equals true
        clearInterval(occupancyCounter); // Destroy the timer for that scene
        window.updateOccupancyCounter = true; // Update the variable that stops the timer from running
        // Run PubNub HereNow function that controls the occupancy
        window.pubnub.hereNow(
          {
            includeUUIDs: true,
            includeState: true
          },
          (status, response) => {
            // If I get a valid response from the channel change the text objects to the correct occupancy count
            if (typeof (response.channels.realtimephaser0) !== 'undefined') {
              textResponse1 = response.channels.realtimephaser0.occupancy.toString();
            } else {
              textResponse1 = '0';
            }
            if (typeof (response.channels.realtimephaser1) !== 'undefined') {
              textResponse2 = response.channels.realtimephaser1.occupancy.toString();
            } else {
              textResponse2 = '0';
            }
            if (typeof (response.channels.realtimephaser2) !== 'undefined') {
              textResponse3 = response.channels.realtimephaser2.occupancy.toString();
            } else {
              textResponse3 = '0';
            }
            window.text1 = `Level 1 Occupancy: ${textResponse1}`;
            window.text2 = `Level 2 Occupancy: ${textResponse2}`;
            window.text3 = `Level 3 Occupancy: ${textResponse3}`;
            window.textObject1.setText(window.text1);
            window.textObject2.setText(window.text2);
            window.textObject3.setText(window.text3);
          }
        );
      }
    }

    if (window.updateOccupancyCounter === false) {
      occupancyCounter = setInterval(checkFlag, 200); // Start timer to run the checkflag function above
    }

    if (presenceEvent.action === 'join') { // If we receive a presence event that says a player joined the channel from the PubNub servers
      checkIfJoined = true;
      checkFlag();
      // text = presenceEvent.totalOccupancy.toString()
      if (presenceEvent.uuid !== window.UniqueID) {
        window.sendKeyMessage({}); // Send message of players location on screen
      }
    } else if (presenceEvent.action === 'leave' || presenceEvent.action === 'timeout') {
      checkFlag();
      try {
        window.globalGameState._removeOtherCharacter(presenceEvent.uuid); // Remove character on leave events if the individual exists
      } catch (err) {
        // console.log(err)
      }
    }
  }
};

Now let's go through this code to look at what it's doing. The listener is listening for events every frame but will only run on the initial connection status to PubNub, or when a message is sent on the channel or when a presence change occurs.

In the status(status) callback, it's sending a fire message to the block to request level information from the KV store.

In the message(messageEvent) callback function, we check to see if the message channel name is equal to the current fire channel name. If it is, call the start loading function to load the game. Then after that if statement, we check to see if the message channel is equal to the window.currentChannelName. If it is equal and it's not a message from yourself, add another player to the game and set its position in the correct location based on the message data.

In the presence(presenceEvent) callback function, we have a function that runs if someone joins, leaves or timeouts of the channel, or if window.updateOccupancyCounter is equal to false. We then run PubNub's hereNow API function that checks to see how many people are in the channel and outputs the current occupancy along with the UUID's in the channel. We are only checking the amount of players in the channel when a presence event is called which is optimized compared to calling the function every frame.

Now right below that function we just wrote but above the load external javascript files code, copy and paste these last two functions. One will send messages out to all clients connected to the channel. The message will contain player UUID information, position and frame count. The second function will send a message to the block telling it the current cache state of the user.

window.sendKeyMessage = (keyMessage) => {
  try {
    if (window.globalMyHero) {
      window.pubnub.publish({
        message: {
          uuid: window.UniqueID,
          keyMessage,
          position: window.globalMyHero.body.position,
          frameCounter: window.frameCounter
        },
        channel: window.currentChannelName,
        sendByPost: false, // true to send via posts
      });
    }
      // console.log("send message!")
  } catch (err) {
    console.log(err);
  }
};

window.fireCoins = () => {
  const message = {
    uuid: window.UniqueID,
    coinCache: window.globalLevelState.coinCache,
    currentLevel: window.globalCurrentLevel,
    time: window.globalLastTime
  };
  // console.log('fireCoins', message);
  window.pubnub.fire(
    {
      message,
      channel: window.currentFireChannelName,
      sendByPost: false, // true to send via posts
    });
};

Uncomment code

Now in order to make the function work, we have to go uncomment some code we left commented out before. Go to the bottom of main.js where the event listener loads the scene. Uncomment the following:

window.createMyPubNub(0); // Connect to the PubNub network and run level code 0
window.StartLoading = function () {
    game.state.start('loading'); // Run the loading function once you successfully connect to the PubNub network
};

Now go to playState.js uncomment window.fireCoins(); in the logCurrentStateCoin() function. Now go down to the _handleInput() function and uncomment:

handleKeyMessages();
...
window.sendKeyMessage({ left: 'down' });
...
window.sendKeyMessage({ left: 'up' });
...
window.sendKeyMessage({ right: 'down' });
...
window.sendKeyMessage({ right: 'up' });
...
window.sendKeyMessage({ up: 'down' });
...
window.sendKeyMessage({ up: 'up' });
...
window.sendKeyMessage({ stopped: 'not moving' });

Then in the _spawnCharacters() function uncomment:

window.sendKeyMessage({});

Now save your files and refresh your window. If you open up two separate windows of the game, you should be able to move your character on one window and see the character move on the other window with very low latency. This is all powered by PubNub's realtime Data Stream Network and API.

PubNub Functions to Manage Game State

You will notice that if you collect a coin in one window, then open up another window, all the coins will show up for the new player that joined but there will be coins missing on the other players screen. We can prevent this from happening by using a PubNub Function. Make sure to go uncomment window.fireCoins() that you commented earlier in the logCurrentStateCoin function.

Then go to https://pubnub.com and login to your dashboard. Once loaded, click on your application then on the left hand bar, click the Functions box. Click the Create Module button and name it whatever you wish. Then create a new Function and call it whatever you wish. Make sure you select Before Publish or Fire and the channel name is realtimephaserFire2.

Now copy and paste the following code into the portal:

export default (request) => {
    const pubnub = require('pubnub');
    const db = require('kvstore');
    const xhr = require('xhr');
    const keyName = "gamestate2_" + request.message.currentLevel;
    if(request.message.int || request.message.fromServer) {
        return request.ok(); // Return a promise when you're done
    }
    if(request.message.requestInt){
      db.get(keyName).then((value) => {
            pubnub.publish({
                "channel": "realtimephaserFire2",
                "message": {
                    value: value,
                    int: true,
                    sendToRightPlayer: request.message.uuid
                }
            }).then((publishResponse) => {
                //console.log(publishResponse);
            });
        });
        return request.ok(); // Return a promise when you're done
    }
    console.log("spitout", request.message)

    pubnub.time().then((timetoken) => {
        db.get(keyName).then((value) => {
            if(value === null || value.time < request.message.time || true) {
                value = {time: timetoken, coinCache: request.message.coinCache};
                db.set(keyName, value, 1);
                 console.log('set', keyName, value);
            }
            pubnub.publish({
                "channel": "realtimephaserFire2",
                "message": {value: value, fromServer: true}
            }).then((publishResponse) => {
                //console.log(publishResponse);
            });
        });
    });
    return request.ok(); // Return a promise when you're done
};

This code saves the game state in the PubNub Function. The information saved is the contents of the JSON level information. Essentially if JSON information exists in the Function, publish that information to the newly connected user. If there is no information, use whatever JSON information is local on the client. Also the only time the JSON updates in the PubNub Function is when a coin is collected in the scene by any player.

Now click the + button in the PubNub Functions dashboard and create a new function and name it onLeave except call it only After Presence and on the channel realtimephaserFire2. Now copy and paste the following code:

export default (request) => {
    const pubnub = require('pubnub');
    const db = require('kvstore');
    const xhr = require('xhr');
    if(request.message.occupancy === 0){
        for(var currentLevel = 0; currentLevel < 100; currentLevel++) {
            const keyName = "gamestate2_" + currentLevel;
            db.removeItem(keyName);
        }
    }
    return request.ok(); // Return a promise when you're done
}

This PubNub Function will only run when someone joins, leaves or timeouts of a channel. If the total occupancy of the game equals zero, it resets the current level cache to nothing so the coins will appear for anyone new that joins the game.

Wrapping Up

And that's it! We hope you enjoyed the tutorial, and can use it as the basis for launching a multiplayer game of your own. We're looking forward to launching more multiplayer gaming tutorials in the future so stay tuned.

Questions? Tweet me @NinjaPigStudios. I'm here to help!

Try PubNub today!
Try Our APIs

# data[].attributes.attachment_link.title
Building Your First Multiplayer Game (Full Tutorial and Demo) | PubNub

# data[].attributes.attachment_link.description
Building a fast and scalable multiplayer game is no easy task. However, we're excited that you've landed on this tutorial, and our hope is by the end of it, you've got a good grasp on building and deploying a fully functioning multiplayer game.

# data[].attributes.attachment_link.readable_text
Phaser 3 and Tiled: Building a Platformer

Introduction

Phaser 3 enables us to quickly create games in our browser with JavaScript. Some of our favorite 2D games are platformers - think of games like Mario, Sonic, Super Meat Boy, or Cuphead.

Tiled is a 2D map editor that's used to create game worlds. We'll explore how to create a platformer level with Tiled, integrate it with Phaser, and animate sprites to create a rich 2D platforming experience.

In this article we'll be creating a basic platformer game, where our player can move on jump in our world. If the player hits a spike, then we reset the position of the player. A playable demo of this game can be found here.

This tutorial is written for those familiar with Phaser 3. If you're not, get acquainted with the framework with one of our previous articles on Phaser.

Getting Started

To better follow along with this tutorial, download and unzip the project stackabuse-platformer.zip into your workspace. The folder should include the following assets:

Note: If you'd prefer, you can also follow along by viewing the code for the project on our GitHub repo.

Don't forget to run a server in your project folder, with your IDE or even with Python: python3 -m http.server. This is required for Phaser to be able to load these assets via HTTP. Again, for more info see our previous article on the topic (linked above).

All game assets were created and shared by Kenney. The atlas file was created with Atlas Phaser Packer.

Tiled Map Editor

Tiled is free and open source software to create game levels. It's available on all major desktop operating systems, so visit the website and download it to continue.

Creating a Tilemap

Open Tiled and click on "New Map". In the prompt, change the Tile layer format to "Base64 (uncompressed)", the width to 14 tiles and height to 7, and the Tile size to 64px each.

Save the file as "level1.tmx" in "assets/tilemaps".

Creating a Tileset

In the right pane, click "New Tileset...". In the popup, name the tileset "kenny_simple_platformer". Ensure that the "Embed in map" option is selected. Without that option, Phaser may experience problems loading your map correctly. In the "Source" property, select "platformPack_tilesheet.png" from the "assets/tilesets" directory.

The tilesheet's image width is 896px and height is 448px. It contains 98 images in total of equal size, they all fit into 7 rows and 14 columns. With basic maths we can deduce that each tile is 64px in width and height. Ensure that the tileset's width and height is 64px:

Designing our Level

Maps in Tiled are composed of layers. Each layer stores some design of the game world. Layers that are on top have their tiles shown over layers that are below. We get depth by using them. This basic game will have only two layers:

Platform: contains the world the player interacts with

Spikes: contains the dangerous spikes that can hurt the player.

The Platform Layer

Before we add our tiles to the map, let's first rename the layer. The names of the layers will be referenced in our Phaser code, so let's change "Tiled Layer 1" to "Platforms":

To create a level, simply select a tile from your tileset and click where you'd like to place it on the map. Let's create/add all our platforms:

Spikes in the Object Layer

In the Layers pane at the right of the screen, click the "New Layer" button and select "Object Layer". Name the layer "Spikes".

At the top toolbar, select the "Insert Object" option:

Now we can add the spike tiles from the tileset:

We've created our game level! Now we need to integrate it with Phaser.

Phaser cannot read the .tmx file that Tiled created. First, let's export our map into JSON. Click on "File -> Export As", select JSON as the format and name it "level1.json" in the tilemaps folder. As with all Phaser projects, our assets need to be loaded in our preload function:

function preload() { this.load.image('background', 'assets/images/background.png'); this.load.image('spike', 'assets/images/spike.png'); // At last image must be loaded with its JSON this.load.atlas('player', 'assets/images/kenney_player.png','assets/images/kenney_player_atlas.json'); this.load.image('tiles', 'assets/tilesets/platformPack_tilesheet.png'); // Load the export Tiled JSON this.load.tilemapTiledJSON('map', 'assets/tilemaps/level1.json'); }

Note: You may be wondering why do we have to load the spike image separately if it's included in the tilemap. Unfortunately, this bit of duplication is required for objects to be displayed correctly.

In our create function, let's first add the background and scale it for our resolution:

const backgroundImage = this.add.image(0, 0,'background').setOrigin(0, 0); backgroundImage.setScale(2, 0.8);

Then let's add our map:

const map = this.make.tilemap({ key: 'map' });

The key matches the name given in the preload function when we loaded the Tiled JSON. We also have to add the tileset image to our Phaser map object:

const tileset = map.addTilesetImage('kenney_simple_platformer', 'tiles');

The first argument of addTilesetImage is the name of the tileset we used in Tiled. The second argument is the key of the image we loaded in the preload function.

We can now add our platform layer:

const platforms = map.createStaticLayer('Platforms', tileset, 0, 200);

And should see this:

By default, Phaser does not manage collisions for our tiled layers. If we added our player now, it would fall completely through the platform tiles. Let's tell Phaser that the layer can collide with other objects:

platforms.setCollisionByExclusion(-1, true);

Every tile in our map was given an index by Tiled to reference what should be shown there. An index of our platform can only be greater than 0. setCollisionByExclusion tells Phaser to enable collisions for every tile whose index isn't -1, therefore, all tiles.

Texture Atlas

Our player animation is stored in a texture atlas - an image containing smaller images. Similar to sprite sheets, they reduce network activity by loading one file. Most texture atlases contain much more than just sprite information.

Let's have a look at our image file: "kenney_player.png":

Our atlas contains 8 frames: frames 0 to 3 are on top and frames 4 to 7 are below. By itself, this isn't that useful to Phaser, that's why it came with a JSON file: "kenney_player_atlas.json".

The file has a frames array which contains information about each individual picture that makes up the atlas.

To use the atlas you will need to know the filename property of the frames you are using.

Adding a Player

With our world set up we can add the player and have it interact with our platforms. In our create function let's add the following:

this.player = this.physics.add.sprite(50, 300, 'player'); this.player.setBounce(0.1); this.player.setCollideWorldBounds(true); this.physics.add.collider(this.player, platforms);

By default, Phaser uses the first frame of the atlas, if we wanted to begin on a different frame we could have added a next argument to the sprite method with the filename property of the atlas image e.g. robo_player_3.

The bounce property just adds a bit of vivacity when our player jumps and lands. And we set the player to collide with our game world and the platforms. We should now see our player standing on our platforms:

The purple box exists around our player because debug mode is enable for our physics engines. Debug mode shows the boundaries which determine how our sprites collide.

Adding Animations

Recall that our texture atlas had 8 frames for the player movement. Phaser allows us to create animations based on the frames of an atlas image. Let's create an animation for walking using the last two frames of the atlas' first row through our create function:

this.anims.create({ key: 'walk', frames: this.anims.generateFrameNames('player', { prefix: 'robo_player_', start: 2, end: 3, }), frameRate: 10, repeat: -1 });

The key property is the string we use to play the animation later. The frames property is an array of frames in our atlas' JSON file that contains the animation. The animation begins at the first frame in the array and ends at the last. We use the helper function generateFrameNames to create the list of frame names for us, a very useful function for large atlas files.

The frameRate defaults to 24 frames per second, which may be a bit too quick for our player so we set it to 10. When we set repeat to -1 we are telling Phaser to run this animation infinitely.

Let's add the animations for our idle sprite, the first frame of the atlas:

this.anims.create({ key: 'idle', frames: [{ key: 'player', frame: 'robo_player_0' }], frameRate: 10, });

Our idle animation is simply one frame. Let's add an animation for when our player jumps, which is also just one frame:

this.anims.create({ key: 'jump', frames: [{ key: 'player', frame: 'robo_player_1' }], frameRate: 10, });

With our animations added, we then need to enable cursor keys so we can move our player:

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

Animating our Player

If our player moves left or right, then we want to walk. If we press spacebar or up, we want to jump. Otherwise, we will stay in our idle position. Let's implement this in our update function:

// Control the player with left or right keys if (this.cursors.left.isDown) { this.player.setVelocityX(-200); if (this.player.body.onFloor()) { this.player.play('walk', true); } } else if (this.cursors.right.isDown) { this.player.setVelocityX(200); if (this.player.body.onFloor()) { this.player.play('walk', true); } } else { // If no keys are pressed, the player keeps still this.player.setVelocityX(0); // Only show the idle animation if the player is footed // If this is not included, the player would look idle while jumping if (this.player.body.onFloor()) { this.player.play('idle', true); } } // Player can jump while walking any direction by pressing the space bar // or the 'UP' arrow if ((this.cursors.space.isDown || this.cursors.up.isDown) && this.player.body.onFloor()) { this.player.setVelocityY(-350); this.player.play('jump', true); }

Animating a sprite is as easy as setting the animation to true. If you were observant, you'll notice that our atlas only has rightward facing movements. If we are moving left, whether walking or jumping, we want to flip the sprite on the x-axis. If we moving to the right, we want to flip it back.

We can achieve this goal with the following bit of code:

if (this.player.body.velocity.x > 0) { this.player.setFlipX(false); } else if (this.player.body.velocity.x < 0) { // otherwise, make them face the other side this.player.setFlipX(true); }

Now our player moves around the game in a well-animated style!

Adding Spikes

Phaser provides us with many ways to get sprites from our object layer. The spikes are stored within an array in our tiled map object. Each spike would force our player to start over if it hits them. It makes sense for us to put all spikes in a sprite group and set up collisions between the player and the group. When a collision is set up with a sprite group, it's applied to all sprites.

In the create function add the following:

// Create a sprite group for all spikes, set common properties to ensure that // sprites in the group don't move via gravity or by player collisions this.spikes = this.physics.add.group({ allowGravity: false, immovable: true }); // Let's get the spike objects, these are NOT sprites const spikeObjects = map.getObjectLayer('Spikes')['objects']; // Now we create spikes in our sprite group for each object in our map spikeObjects.forEach(spikeObject => { // Add new spikes to our sprite group, change the start y position to meet the platform const spike = this.spikes.create(spikeObject.x, spikeObject.y + 200 - spikeObject.height, 'spike').setOrigin(0, 0); });

We should get this:

The spike sprite's collision boundary is much higher than the spikes themselves. If left unchanged, that can create a bad game experience. Players would reset their position without hitting the sprite! Let's adjust the spikes' bodies to be smaller in size, particularly height. Replace the forEach with this:

spikeObjects.forEach(spikeObject => { const spike = this.spikes.create(spikeObject.x, spikeObject.y + 200 - spikeObject.height, 'spike').setOrigin(0, 0); spike.body.setSize(spike.width, spike.height - 20).setOffset(0, 20); });

To keep the bounding box correctly encompassing the spikes we add an offset that matches the height reduction. Now we have more appropriate spike sprites:

Collision with Player

If our player collides with a spike, their position is reset. It's common in platform games for players to have a 'lose' animation. Let's add a blinking animation when our player is reset. First, in the create let's add the collision:

this.physics.add.collider(this.player, this.spikes, playerHit, null, this);

The logic for the player reset will be in the playerHit function. Every time the player collides with a sprite from the spike sprite group, this function will be called. At the end of the file add the following:

function playerHit(player, spike) { player.setVelocity(0, 0); player.setX(50); player.setY(300); player.play('idle', true); player.setAlpha(0); let tw = this.tweens.add({ targets: player, alpha: 1, duration: 100, ease: 'Linear', repeat: 5, }); }

Quite a few things are happening here. Let's take each instruction line by line:

Set the velocity of the player to 0. It's much more predictable (and safer) to stop the player's movement on restart

Set the X and Y coordinates to the player's first position

Use the idle animation, just as it was when the player started

The alpha property controls the opacity of a sprite. It's a value between 0 and 1 where 0 is fully transparent and 1 is fully opaque

Create a tween - an 'animation' of a property of a game object. The tween is applied to the player object that collided with the spike. It sets the alpha property to 1 (i.e. makes our player fully visible). This tween lasts 100ms, and the opacity increases linearly as noted by the ease property. It also repeats 5 times, hence why it looks like it's blinking.

Now our game looks like this:

Note: Be sure to remove the debug: true property from the game configuration before you share it with friends, never leave debug mode in production!

Conclusion

With Tiled we can design both small and expansive 2D game worlds. It's best practice to create layers for depth within our game world. We then took the world we built in Tiled and added it to our Phaser game.

We added the platform layer as a static layer, making it immovable when the player collides. We then created a sprite group for the spikes and created a function to handle collisions between each spike and the player.

In addition to creating a vibrant game world, we learned how to animate our character using an atlas - a large image that contains multiple smaller images, accompanied by a JSON file detailing what image lies in each frame. We also used a tween to change a property of our sprite for a set period of time.

With these techniques, it's up to you to make the next best platformer with Phaser!

You can view the annotated source code for the game here.

# data[].attributes.attachment_link.title
Phaser 3 and Tiled: Building a Platformer

# data[].attributes.attachment_link.description
Introduction Phaser 3 enables us to quickly create games in our browser with JavaScript. Some of our favorite 2D games are platformers - think of games like Mario, Sonic, Super Meat Boy, or Cuphead. Tiled is a 2D map editor that's used to create game worlds. We'll explore how to

# data[].attributes.attachment_link.readable_text
Phaser is a fast, free, and fun open source HTML5 game framework that offers WebGL and Canvas rendering across desktop and mobile web browsers. Games can be compiled to iOS, Android and native apps by using 3rd party tools. You can use JavaScript or TypeScript for development.

Along with the fantastic open source community, Phaser is actively developed and maintained by Photon Storm. As a result of rapid support, and a developer friendly API, Phaser is currently one of the most starred game frameworks on GitHub.

Thousands of developers from indie and multi-national digital agencies, and universities worldwide use Phaser. You can take a look at their incredible games.

Grab the source and join the fun!

8th August 2019

I'm pleased to announce that Phaser 3.19 is now available. This release includes our brand new Spine plugin. Spine allows you to bring animation to life in your games, offering a dedicated 2D skeletal animation system and workflow. Our plugin makes integration with Phaser seamless and is fully updated for the Spine 3.7 Runtimes with support for WebGL and Canvas renderers. This version will properly batch Spine skeletons where possible, potentially saving hundreds of draw calls. The plugin is fully documented and exports both debug and minified files, suitable for ES6 'importing' or ES5 plugin inclusion. The whole plugin is just 68KB in size (min+gz), or a paltry 57KB if you only need the Canvas renderer! That's a really tiny payload for such a massive feature-set. You can find out more about Spine from the Esoteric Software website.

3.19 also introduces a huge overhaul to the Tween system. Tweens now have 100% documentation and we've extended their capabilities significantly. There are lots of new Tween Events to listen for, such as 'COMPLETE' or 'REPEAT' to allow you to trigger actions without needing to create callbacks. Tweens can now tween both 'from' and 'to' a value, with the ability to set a starting value for any tweened property. There are lots of new handy methods and properties, such as Tween.hasStarted and a rewrite of the Tween seeking function, so it now allows you to seek to any point in time across a tween. Finally, we've added in the great new 'StaggerBuilder'. This allows you to easily add staggered offsets to a bunch of tween targets, including all kinds of options such as staggering across a grid layout, stagger directions, starting values and a lot more. Please see the docs and examples for more details.

As usual, it doesn't end there, though :) Another very useful feature is Shader.setRenderToTexture. This allows you to redirect a shader to its own framebuffer / WebGL Texture instead of to the display list. This allows you to use the output of the shader as an input for another shader, by mapping a sampler2D uniform to it. It also allows you to save the Shader to the Texture Manager, allowing you to use it as a texture for any other texture based Game Object such as a Sprite. Combined with the new setSampler2DBuffer method you can now easily chain shaders together, using them as buffers for other shaders.

One thing I've been seeing asked for a lot on the Phaser Discord is the ability to 'save' a Render Texture to an image. So, I added the new methods RenderTexture.snapshot and snapshotArea. This allows you to grab whatever is on the Render Texture at that point in time and turn it into an Image. You could then save this image to the Texture Manager, if needed, or just save it out to the filesystem, or transmit it to as web service. Great for things like avatar creators or art packages.

You'll find loads more great new features, updates and fixes. So, as usual, please do spend some time digging through the Change Log. I assure you, it's worth while :)

A massive thank-you to everyone who supports Phaser on Patreon and PayPal. Your continued backing has allowed me to work on Phaser all year, and this great new releases is the very real result of that. If you've ever considered becoming a backer, now is the perfect time!

If you'd like to stay abreast of developments then I publish my Developer Logs in the Phaser World newsletter. Subscribe to stay in touch and get all the latest news from the core team and the wider community.

You can also follow Phaser on Twitter and chat with fellow Phaser devs in our Slack and Discord channels.

Phaser 3 wouldn't have been possible without the fantastic support of the community and Patreon. Thank you to everyone who supports our work, who shares our belief in the future of HTML5 gaming, and Phaser's role in that.

Happy coding everyone!

Cheers,

Rich - @photonstorm

Because Phaser is an open source project, we cannot charge for it in the same way as traditional retail software. What's more, we don't ever want to. After all, it's built on, and was born from, open web standards. It's part of our manifesto that the core framework will always be free, even if you use it commercially, as many of you do.
You may not realize it, but because of this, we rely 100% on community backing to fund development.
Those funds allow Phaser to improve, and when it improves, everyone involved benefits. Your support helps secure a constant cycle of updates, fixes, new features and planning for the future.

There are other benefits to backing Phaser, too:

We use Patreon to manage the backing and you can support Phaser from $1 per month. The amount you pledge is entirely up to you and can be changed as often as you like. Patreon renews monthly, just like Netflix. You can, of course, cancel at any point. Tears will be shed on this end, but that's not your concern.

Extra special thanks to the following companies who's support makes Phaser possible:

We publish the Phaser World newsletter. It's packed full of the latest Phaser games, tutorials, videos, meet-ups, talks, and more. The newsletter also contains our weekly Development Progress updates which let you know about the new features we're working on.

Over 140 previous editions can be found on our Back Issues page.

Phaser 3 is available via GitHub, npm and CDNs:

NPM

Install via npm:

npm install phaser

CDN

Phaser is on jsDelivr which is a "super-fast CDN for developers". Include the following in your html:

or the minified version:

API Documentation

Go to https://photonstorm.github.io/phaser3-docs/index.html to read the docs online. Use the drop-down menus at the top to navigate the namespaces, classes and Game Objects lists.

Or, if you wish to run the docs locally you can checkout the phaser3-docs repository and then read the documentation by pointing your browser to the docs/ folder.

The documentation for Phaser 3 is an on-going project. Please help us by searching the Phaser code for any instance of the string [description] and then replacing it with some documentation.

TypeScript Definitions

The TypeScript definitions can be found inside the types folder. They are also referenced in the types entry in package.json.

Depending on your project, you may need to add the following to your tsconfig.json file:

"typeRoots": [ "./node_modules/phaser/types" ], "types": [ "Phaser" ]

The defs are automatically generated from the JSDoc comments found in the Phaser source code. If you wish to help refine them then you must edit the Phaser JSDoc blocks directly, not the defs file. You can find more details about the parser we built in the scripts/tsgen folder.

Webpack

We use Webpack to build Phaser and we take advantage of its conditional build flag feature to handle renderer swapping. If you wish to use Webpack with Phaser then please use our Phaser 3 Project Template as it's already set-up to handle the build conditions Phaser needs. Recent changes to our build steps mean you should now be able to use any other packager, like Parcel, without any config changes.

License

Phaser is released under the MIT License.

Tutorials and guides on Phaser 3 development are being published every week.

Also, please subscribe to the Phaser World newsletter for details about new tutorials as they are published.

Facebook Instant Games

Phaser 3.13 introduced the new Facebook Instant Games Plugin. The plugin provides a seamless bridge between Phaser and version 6.2 of the Facebook Instant Games SDK. Every single SDK function is available via the plugin and we will keep track of the official SDK to make sure they stay in sync.

The plugin offers the following features:

Easy integration with the Phaser Loader so load events update the Facebook progress circle.

Events for every plugin method, allowing the async calls of the SDK to be correctly inserted into the Phaser game flow. When SDK calls resolve they will surface naturally as a Phaser event and you'll know you can safely act upon them without potentially doing something mid-way through the game step.

All Plugin methods check if the call is part of the supported APIs available in the SDK, without needing to launch an async request first.

Instant access to platform, player and locale data.

Easily load player photos directly into the Texture Manager, ready for use with a Game Object.

Subscribe to game bots.

The plugin has a built-in Data Manager which makes dealing with data stored on Facebook seamless. Just create whatever data properties you need and they are automatically synced.

Support for FB stats, to retrieve, store and increment stats into cloud storage.

Save Session data with built-in session length validation.

Easy context switching, to swap between game instances and session data retrieval.

Easily open a Facebook share, invite, request or game challenge window and populate the text and image content using any image stored in the Texture cache.

Full Leaderboard support. Retrieve, scan and update leaderboard entries, as well as player matching.

Support for in-app purchases, with product catalogs, the ability to handle purchases, get past purchases and consume previously unlocked purchases.

Easily preload a set of interstitial ads, in both banner and video form, then display the ad at any point in your game, with in-built tracking of ads displayed and inventory available.

Plus other features, such as logging to FB Analytics, creating short cuts, switching games, etc.

We've 3 tutorials related to Facebook Instant Games and Phaser:

A special build of Phaser with the Facebook Instant Games Plugin ready-enabled is available on jsDelivr. Include the following in your html:

or the minified version:

The build files are in the git repository in the dist folder, and you can also include the plugin in custom builds.

Source Code Examples

During our development of Phaser 3, we created hundreds of examples with the full source code and assets ready available. These examples are now fully integrated into the Phaser website. You can also browse them on Phaser 3 Labs via a more advanced interface, or clone the examples repo. We are constantly adding to and refining these examples.

Create Your First Phaser 3 Example

Create an index.html page locally and paste the following code into it:

This is a standard empty webpage. You'll notice there's a script tag that is pulling in a build of Phaser 3, but otherwise this webpage doesn't do anything yet. Now let's set-up the game config. Paste the following between the <script></script> tags:

var config = { type: Phaser.AUTO, width: 800, height: 600, physics: { default: 'arcade', arcade: { gravity: { y: 200 } } }, scene: { preload: preload, create: create } };

config is a pretty standard Phaser 3 Game Configuration object. We tell config to use the WebGL renderer if it can, set the canvas to a size of 800x600 pixels, enable Arcade Physics, and finally call the preload and create functions. preload and create have not been implemented yet, so if you run this JavaScript code, you will have an error. Add the following after config:

var game = new Phaser.Game(config); function preload () { this.load.setBaseURL('http://labs.phaser.io'); this.load.image('sky', 'assets/skies/space3.png'); this.load.image('logo', 'assets/sprites/phaser3-logo.png'); this.load.image('red', 'assets/particles/red.png'); } function create () { }

game is a Phaser Game instance that uses our configuration object config. We also add function definitions for preload and create. The preload function helps you easily load assets into your game. In preload, we set the Base URL to be the Phaser server and load 3 PNG files.

The create function is empty, so it's time to fill it in:

function create () { this.add.image(400, 300, 'sky'); var particles = this.add.particles('red'); var emitter = particles.createEmitter({ speed: 100, scale: { start: 1, end: 0 }, blendMode: 'ADD' }); var logo = this.physics.add.image(400, 100, 'logo'); logo.setVelocity(100, 200); logo.setBounce(1, 1); logo.setCollideWorldBounds(true); emitter.startFollow(logo); }

Here we add a sky image into the game and create a Particle Emitter. The scale value means that the particles will initially be large and will shrink to nothing as their lifespan progresses.

After creating the emitter, we add a logo image called logo. Since logo is a Physics Image, logo is given a physics body by default. We set some properties for logo: velocity, bounce (or restitution), and collision with the world bounds. These properties will make our logo bounce around the screen. Finally, we tell the particle emitter to follow the logo - so as the logo moves, the particles will flow from it.

Run it in your browser and you'll see the following:

(Got an error? Here's the full code)

This is a tiny example, and there are hundreds more for you to explore, but hopefully it shows how expressive and quick Phaser is to use. With just a few easily readable lines of code, we've got something pretty impressive up on screen!

Subscribe to our newsletter for further tutorials and examples.

There are both plain and minified compiled versions of Phaser in the dist folder of the repository. The plain version is for use during development, and the minified version is for production use. You can also create your own builds.

Custom Builds

Phaser 3 is built using Webpack and we take advantage of the Webpack definePlugin feature to allow for conditional building of the Canvas and WebGL renderers and extra plugins. You can custom the build process to only include the features you require. Doing so can cut the main build file size down to just 70KB.

Read our comprehensive guide on creating Custom Builds of Phaser 3 for full details.

Building from Source

If you wish to build Phaser 3 from source, ensure you have the required packages by cloning the repository and then running npm install on your source directory.

You can then run webpack to create a development build in the build folder which includes source maps for local testing. You can also npm run dist to create a minified packaged build in the dist folder. For a list of all commands available use npm run help.

Version 3.19.0 - Naofumi - 8th August 2019

Tween Updates

All Tween classes and functions have 100% complete JSDocs :)

Tween now extends the Event Emitter class, allowing it to emit its own events and be listened to.

Tween.ACTIVE_EVENT is a new event that is dispatched when a tween becomes active. Listen to it with tween.on('active').

Tween.COMPLETE_EVENT is a new event that is dispatched when a tween completes or is stopped. Listen to it with tween.on('complete').

Tween.LOOP_EVENT is a new event that is dispatched when a tween loops, after any loop delay expires. Listen to it with tween.on('loop').

Tween.REPEAT_EVENT is a new event that is dispatched when a tween property repeats, after any repeat delay expires. Listen to it with tween.on('repeat').

Tween.START_EVENT is a new event that is dispatched when a tween starts. Listen to it with tween.on('start').

Tween.UPDATE_EVENT is a new event that is dispatched when a tween property updates. Listen to it with tween.on('update').

Tween.YOYO_EVENT is a new event that is dispatched when a tween property yoyos, after any hold delay expires. Listen to it with tween.on('yoyo').

Tween.onActive is a new callback that is invoked the moment the Tween Manager brings the tween to life, even though it may not have yet started actively tweening anything due to delay settings.

Tween.onStart is now only invoked when the Tween actually starts tweening a value. Previously, it was invoked as soon as the Tween Manager activated the Tween. This has been recoded and this action is now handled by the onActive callback. Fix #3330 (thanks @wtravO)

Tween.seek has been rewritten so you can now seek to any point in the Tween, regardless of repeats, loops, delays and hold settings. Seeking will not invoke any callbacks or events during the seek. Fix #4409 (thanks @cristib84)

You can now set from and to values for a property, i.e. alpha: { from: 0, to: 1 } which would set the alpha of the target to 0 and then tween it to 1 after any delays have expired. Fix #4493 (thanks @BigZaphod)

Tween.hasStarted is a new property that holds a flag signifying if the Tween has started or not. A Tween that has started is one that is actively tweening a property and not just in a delayed state.

Tween.startDelay is a new property that is set during the Tween init to hold the shortest possible time before the Tween will start tweening a value. It is decreased each update until it hits zero, after which the onStart callback is invoked.

Tween.init and Tween.play have been rewritten so they are not run multiple times when a Tween is paused before playback, or is part of a Timeline. This didn't cause any problems previously, but it was a redundant duplication of calls.

Tween.onLoop will now be invoked after the loopDelay has expired, if any was set.

Tween.onRepeat will now be invoked after the repeatDelay has expired, if any was set.

easeParams would be ignored for tweens that didn't use a string for the ease function name. Fix #3826 (thanks @SBCGames)

You can now specify easeParams for any custom easing function you wish to use. Fix #3826 (thanks @SBCGames)

All changes to Tween.state are now set before any events or callbacks, allowing you to modify the state of the Tween in those handlers (thanks @Cudabear)

Tween.dispatchTweenEvent is a new internal method that handles dispatching the new Tween Events and callbacks. This consolidates a lot of duplicate code into a single method.

Tween.dispatchTweenDataEvent is a new internal method that handles dispatching the new TweenData Events and callbacks. This consolidates a lot of duplicate code into a single method.

Tween.isSeeking is a new internal boolean flag that is used to keep track of the seek progress of a Tween.

Timeline.onLoop will now be invoked after the loopDelay has expired, if any was set.

Timeline.onComplete will now be invoked after the completeDelay has expired, if any was set.

All changes to Timeline.state are now set before any events or callbacks, allowing you to modify the state of the Timeline in those handlers.

The TIMELINE_LOOP_EVENT has had the loopCounter argument removed from it. It didn't actually send the number of times the Timeline had looped (it actually sent the total remaining).

When a TweenData completes it will set the exact start or end value into the target property.

TweenData.getActiveValue is a new property that, if not null, returns a value to immediately sets the property value to on activation.

Tweens created via TweenManager.create wouldn't start when Tween.play was called without first making them active manually. They now start automatically. Fix #4632 (thanks @mikewesthad)

Spine Updates

The Spine Plugin is now 100% complete. It has been updated to use the Spine 3.7 Runtimes. Improvements have been made across the entire plugin, including proper batched rendering support in WebGL, cleaner skin and slot functions and lots and lots of updates. It's fully documented and there are lots of examples to be found. The following legacy bugs have also been fixed:

Adding Spine to physics causes position to become NaN. Fix #4501 (thanks @hizzd)

Destroying a Phaser Game instance and then re-creating it would cause an error trying to re-create Spine Game Objects ("Cannot read property get of null"). Fix #4532 (thanks @Alex-Badea)

Rendering a Spine object when a Camera has renderToTexture enabled on it would cause the object to be vertically flipped. It now renders correctly in both cases. Fix #4647 (thanks @probt)

New Features

Shader.setRenderToTexture is a new method that will redirect the Shader to render to its own framebuffer / WebGLTexture instead of to the display list. This allows you to use the output of the shader as an input for another shader, by mapping a sampler2D uniform to it. It also allows you to save the Shader to the Texture Manager, allowing you to use it as a texture for any other texture based Game Object such as a Sprite.

Shader.setSampler2DBuffer is a new method that allows you to pass a WebGLTexture directly into a Shader as a sampler2D uniform, such as when linking shaders together as buffers for each other.

Shader.renderToTexture is a new property flag that is set if you set the Shader to render to a texture.

Shader.framebuffer is a new property that contains a WebGLFramebuffer reference which is set if you set the Shader to render to a texture.

Shader.glTexture is a new property that contains a WebGLTexture reference which is set if you set the Shader to render to a texture.

Shader.texture is a new property that contains a Phaser Texture reference which is set if you set the Shader to save to the Texture Manager.

TextureManager.addGLTexture is a new method that allows you to add a WebGLTexture directly into the Texture Manager, saved under the given key.

TextureSource.isGLTexture is a new boolean property that reflects if the data backing the underlying Texture Source is a WebGLTexture or not.

TextureTintPipeline.batchSprite will now flip the UV if the TextureSource comes from a GLTexture.

Math.ToXY is a new mini function that will take a given index and return a Vector2 containing the x and y coordinates of that index within a grid.

RenderTexture.glTexture is a new property that holds a reference to the WebGL Texture being used by the Render Texture. Useful for passing to a shader as a sampler2D.

Pointer.locked is a new read-only property that indicates if the pointer has been Pointer Locked, or not, via the Pointer Lock API.

WebGLRenderer.snapshotFramebuffer, and the corresponding utility function WebGLSnapshot, allows you to take a snapshot of a given WebGL framebuffer, such as the one used by a Render Texture or Shader, and either get a single pixel from it as a Color value, or get an area of it as an Image object, which can then optionally be saved to the Texture Manager for use by Game Object textures.

CanvasRenderer.snapshotCanvas allows you to take a snapshot of a given Canvas object, such as the one used by a Render Texture, and either get a single pixel from it as a Color value, or get an area of it as an Image object, which can then optionally be saved to the Texture Manager for use by Game Object textures.

RenderTexture.snapshot is a new method that will take a snapshot of the whole current state of the Render Texture and return it as an Image object, which could then be saved to the Texture Manager if needed.

RenderTexture.snapshotArea is a new method that will take a snapshot of an area of a Render Texture and return it as an Image object, which could then be saved to the Texture Manager if needed.

RenderTexture.snapshotPixel is a new method that will take extract a single pixel color value from a Render Texture and return it as a Color object.

Game.CONTEXT_LOST_EVENT is a new event that is dispatched by the Game instance when the WebGL Renderer webgl context is lost. Use this instead of the old 'lostContextCallbacks' for cleaner context handling.

Game.CONTEXT_RESTORED_EVENT is a new event that is dispatched by the Game instance when the WebGL Renderer webgl context is restored. Use this instead of the old 'restoredContextCallbacks' for cleaner context handling.

WebGLRenderer.currentType contains the type of the Game Object currently being rendered.

WebGLRenderer.newType is a boolean that indicates if the current Game Object has a new type, i.e. different to the previous one in the display list.

WebGLRenderer.nextTypeMatch is a boolean that indicates if the next Game Object in the display list has the same type as the one being currently rendered. This allows you to build batching into separated Game Objects.

PluginManager.removeGameObject is a new method that allows you to de-register custom Game Object types from the global Game Object Factory and/or Creator. Useful for when custom plugins are destroyed and need to clean-up after themselves.

Circle.type is a new property containing the shapes geometry type, which can be used for quick type comparisons.

Ellipse.type is a new property containing the shapes geometry type, which can be used for quick type comparisons.

Line.type is a new property containing the shapes geometry type, which can be used for quick type comparisons.

Point.type is a new property containing the shapes geometry type, which can be used for quick type comparisons.

Polygon.type is a new property containing the shapes geometry type, which can be used for quick type comparisons.

Rectangle.type is a new property containing the shapes geometry type, which can be used for quick type comparisons.

Triangle.type is a new property containing the shapes geometry type, which can be used for quick type comparisons.

InputPlugin.enableDebug is a new method that will create a debug shape for the given Game Objects hit area. This allows you to quickly check the size and placement of an input hit area. You can customzie the shape outline color. The debug shape will automatically track the Game Object to which it is bound.

InputPlugion.removeDebug will remove a Debug Input Shape from the given Game Object and destroy it.

ScaleManager._resetZoom is a new internal flag that is set when the game zoom factor changes.

Texture.remove is a new method that allows you to remove a Frame from a Texture based on its name. Fix #4460 (thanks @BigZaphod)

Updates

The Shader will no longer set uniforms if the values are null, saving on GL ops.

The Animation Manager will now emit a console warning if you try and play an animation on a Sprite that doesn't exist.

The Animation component will no longer start an animation on a Sprite if the animation doesn't exist. Previously it would throw an error saying "Unable to read the property getFirstTick of null".

InputManager.onPointerLockChange is a new method that handles pointer lock change events and dispatches the lock event.

The SmoothedKeyControl minimum zoom a Camera can go to is now 0.001. Previously it was 0.1. This is to make it match the minimum zoom a Base Camera can go to. Fix #4649 (thanks @giviz)

TextureManager.getBase64 will now emit a console warning if you try to get a base64 from a non-image based texture, such as a WebGL Texture.

The WebAudioSoundManager will now remove the document touch handlers even if the Promise fails, preventing it from throwing a rejection handler error.

GameObjectFactory.remove is a new static function that will remove a custom Game Object factory type.

GameObjectCreator.remove is a new static function that will remove a custom Game Object creator type.

CanvasTexture.getPixels now defaults to 0x0 by width x height as the default area, allowing you to call the method with no arguments to get all the pixels for the canvas.

CreateDOMContainer will now use div.style.cssText to set the inline styles of the container, so it now works on IE11. Fix #4674 (thanks @DanLiamco)

TransformMatrix.rotation now returns the properly normalized rotation value.

PhysicsEditorParser has now been exposed under the Phaser.Physics.Matter namespace, so you can call methods on it directly.

Calling CanvasTexture.update will now automatically call refresh if running under WebGL. This happens for both draw and drawFrame, meaning you no longer need to remember to call refresh after drawing to a Canvas Texture in WebGL, keeping it consistent with the Canvas renderer.

Frame.destroy will now null the Frames reference to its parent texture, glTexture and clear the data and customData objects.

The Container renderer functions will now read the childs alpha property, instead of _alpha, allowing it to work with more variety of custom children.

Bug Fixes

The Scale Manager would throw the error 'TypeError: this.removeFullscreenTarget is not a function' when entering full-screen mode. It would still enter fullscreen, but the error would appear in the console. Fix #4605 (thanks @darklightcode)

Tilemap.renderDebug was calling out-dated Graphics API methods, which would cause the debug to fail (thanks @Fabadiculous)

Pointer.getDuration would return a negative / static value on desktop, or NaN on mobile, because the base time wasn't being pulled in from the Input Manager properly. Fix #4612 (thanks @BobtheUltimateProgrammer)

WebGLRenderer.setScissor will default the drawingBufferHeight if no argument is provided, stopping NaN scissor heights.

The Arc / Circle Game Object wasn't rendering centered correctly in WebGL due to an issue in a previous size related commit, it would be half a radius off. Fix #4620 (thanks @CipSoft-Components @rexrainbow)

Destroying a Scene in HEADLESS mode would throw an error as it tried to access the gl renderer in the Camera class. Fix #4467 (thanks @AndreaBoeAbrahamsen @samme)

Tilemap.createFromObjects would ignore the scene argument passed in to the method. It's now used (thanks @samme)

Fixed a bug in the WebGL and Canvas Renderers where a Sprite with a flipX or flipY value set would render the offset frames slightly out of place, causing the animation to appear jittery. Also, the sprite would be out of place by its origin. Fix #4636 #3813 (thanks @jronn @B3L7)

Animations with custom pivots, like those created in Texture Packer with the pivot option enabled, would be mis-aligned if flipped. They now render in the correct position, regardless of scale or flip on either axis. Fix #4155 (thanks @Zax37)

Removing a frame from a 2 frame animation would cause an error when a Sprite using that animation next tried to render. Fix #4621 (thanks @orlicgms)

Calling Animation.setRepeat() wouldn't reset the repeatCounter properly, causing Sprite bound animation instances to fail to change their repeat rate. Fix #4553 (thanks @SavedByZero)

The UpdateList.remove method wouldn't flag the Game Object for removal properly if it was active. It now checks that the Game Object is in the current update list and hasn't already been inserted into the 'pending removal' list before flagging it. Fix #4544 (thanks @jcyuan)

DynamicTilemapLayer.destroy will now no longer run its destroy sequence again if it has already been run once. Fix #4634 (thanks @CipSoft-Components)

StaticTilemapLayer.destroy will now no longer run its destroy sequence again if it has already been run once.

Shader.uniforms now uses Extend instead of Clone to perform a deep object copy, instead of a shallow one, avoiding multiple instances of the same shader sharing uniforms. Fix #4641 (thanks @davidmball)

Calling input.mouse.requestPointerLock() will no longer throw an error about being unable to push to the Input Manager events queue.

The POINTERLOCK_CHANGE event is now dispatched by the Input Manager again.

The Pointer.movementX and Pointer.movementY properties are now taken directly from the DOM pointer event values, if the pointer is locked, and no longer incremental. Fix #4611 (thanks @davidmball)

The DESTROY event hook wasn't removed from Group children when destroying the Group and destroyChildren was set to false. Now, the hook is removed regardless (thanks @rexrainbow)

The WebGL Lost and Restored Context callbacks were never removed, which could cause them to hold onto stale references. Fix #3610 (thanks @Twilrom)

Origin.updateDisplayOrigin no longer applies a Math.floor to the display origins, allowing you to have a 0.x origin for a Game Object that only has a width or height of 1. This fixes issues with things like 1x1 rectangles displaying incorrectly during rendering. Fix #4126 (thanks @rexrainbow)

InputManager.resetCursor will now check if the canvas element still exists before resetting the cursor on it. Fix #4662 (thanks @fromnowhereuser)

It was not possible to set the zoom value of the Scale Manager back to 1 again, having changed it to a different value. Fix #4633 (thanks @lgibson02 @BinaryMoon)

Examples, Documentation and TypeScript

My thanks to the following for helping with the Phaser 3 Examples, Docs and TypeScript definitions, either by reporting errors, fixing them or helping author the docs:

@vacarsu @KennethGomez @samme @ldd @Jazcash @jcyuan @LearningCode2023 @PhaserEditor2D

Please see the complete Change Log for previous releases.

The Contributors Guide contains full details on how to help with Phaser development. The main points are:

Found a bug? Report it on GitHub Issues and include a code sample. Please state which version of Phaser you are using! This is vitally important.

Before submitting a Pull Request run your code through ES Lint using our config and respect our Editor Config.

Before contributing read the code of conduct.

Written something cool in Phaser? Please tell us about it in the forum, or email support@phaser.io

Phaser is a Photon Storm production.

Created by Richard Davey. Powered by coffee, anime, pixels and love.

The Phaser logo and characters are © 2019 Photon Storm Limited.

All rights reserved.

"Above all, video games are meant to be just one thing: fun. Fun for everyone." - Satoru Iwata

# data[].attributes.attachment_link.readable_text
You can't perform that action at this time.

You signed in with another tab or window. You signed out in another tab or window. Reload to refresh your session. Reload to refresh your session.

# data[].attributes.attachment_link.description
You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or window. Reload to refresh your session. Reload to refresh your session.