DEV Community

Cover image for Working with Scenes and Data in Phaser
Cecelia Martinez
Cecelia Martinez

Posted on

Working with Scenes and Data in Phaser

It’s been a long time since I returned to this Phaser tutorial, and for that I apologize! A job role change caused this to fall through the cracks, but I wanted to make sure I wrapped things up for those following along.

As a disclaimer, some things have changed in the months since Part 2, including a major update of Capacitor to version 6, as well as some cool things happening with the new Phaser CLI. I’ll be digging more into these in future posts now that I’m back up and running. However, for the purposes of finishing this series, I’ll stay with the setup started in Part 1.

Now, enough time has passed already, so let’s get to it!

In this post in the series, we’ll create our ScoreScene and set up the other views in our Vue app: About and Scores. This will involve using events to access the scores from the Phaser game in the parent Vue app to create a “High Scores” page.

Table of Contents

Creating the Score Scene

This scene will display the player’s final score and display text letting them know they can tap to restart. We already created a ScoreScene.js file within our game directory in Part 2, and this is the file we’ll be using.

Right now, the file is empty. We’ll start by scaffolding out the basic structure of a Scene file.


import { Scene } from "phaser";

export class ScoreScene extends Scene {
    constructor () {
      super({ key: 'ScoreScene' })
    }
    create () {

    }
  }
Enter fullscreen mode Exit fullscreen mode

Then, we’ll set some values based on the screen size, like we did in the Play Scene. We’ll set the screenWidth, screenHeight, and the horizontal center of the screen with screenCenterX.

This will allow us to display text in the correct location on the screen. We’ll add “Game Over” text to the center like we had in our Play Scene using the gameOverText variable, as well as some animated text indicating the player should tap to restart with the restartText variable.

For the animation, we’ll use a tween that targets the restartText and repeatedly eases the text to 1.5 times larger and back to its original size on a loop. This will create a pulsing animation effect.

create () {
        // sets game values based on screen size
        this.screenWidth = this.scale.width;
        this.screenHeight = this.scale.height;
        this.screenCenterX = this.screenWidth / 2;

        // adds Game Over text to middle of screen
        this.gameOverText = this.add.text(this.screenCenterX, this.screenHeight / 2, 'Game Over', { fontSize: '32px', fill: 'red' }).setOrigin(0.5, 0.5);

        // adds Tap to Restart text underneath Game Over text
        this.restartText = this.add.text(this.screenCenterX, this.screenHeight / 3 + 200, 'Tap to restart', {fontSize: '15px', fill: '#ffffff'}).setOrigin(0.5, 0.5);

        // adds pulsing animation to restart text

        this.tweens.add({
            targets: this.restartText,
            scaleX: 1.5, // Scale it to 150% of its original size
            scaleY: 1.5,
            duration: 1000, // Duration of one pulse
            ease: 'Sine.easeInOut', // Smooth easing
            yoyo: true, // Reverse the tween on completion, creating the "pulse" effect
            repeat: -1 // Repeat forever
        })

    }
Enter fullscreen mode Exit fullscreen mode

We’ve created our scene, but it’s not connected to the rest of our game. Next, we’ll learn how to switch between our Play Scene and Score Scene in Phaser.

Here is the git commit with the changes for this section.

Switching Scenes

Phaser has a Scene Manager that contains built-in methods for managing the scenes in your game, including starting, stopping, switching, sleeping, and more. You can view the documentation here to see a full list of the available methods. For our game, we want to do the following:

  • Stop the Play Scene when the game is over, and start the Score Scene
  • Stop the Score Scene when the player taps to restart, and start the Play Scene

First, let’s update our Play Scene. Right now, the logic to restart the game is inside the overlap we declared for when our player hits a bomb (which ends the game). It looks like this:

// Adds overlap between player and bombs

  this.physics.add.overlap(this.player, this.bombs, function(object1, object2) {
    const bomb = (object1.key === 'player') ? object1 : object2;
    bomb.destroy();
    createStarLoop.destroy();
    createBombLoop.destroy();
    this.physics.pause();

    this.gameOverText = this.add.text(this.screenCenterX, this.screenHeight / 2, 'Game Over', { fontSize: '32px', fill: 'red' }).setOrigin(0.5, 0.5);

    this.input.on('pointerup', () => {
      this.score = 0;
      this.scene.restart();
    })
  }, null, this);
Enter fullscreen mode Exit fullscreen mode

Instead of displaying the “Game Over” text and directly restarting in the same Scene, we’ll instead stop the Play Scene and start the Score Scene. Your overlap should now look like this:

this.physics.add.overlap(this.player, this.bombs, function(object1, object2) {

    // Destroys bomb that triggered overlap
    const bomb = (object1.key === 'player') ? object1 : object2;
    bomb.destroy();

    // Stops game object generation, pauses physics, and resets score to zero
    createStarLoop.destroy();
    createBombLoop.destroy();
    this.physics.pause();
    this.score = 0;

    // stops Play Scene and starts Score Scene

    this.scene.stop('PlayScene')
    this.scene.start('ScoreScene');

  }, null, this);
Enter fullscreen mode Exit fullscreen mode

However, our game.js file does not currently import the ScoreScene, so Phaser does not know about it. Update your game.js with an import statement for ScoreScene, and change the scene property in the launch function and pass an array with both scenes instead.

import { Game, AUTO, Scale } from "phaser";
import { PlayScene } from "./PlayScene.js";
import { ScoreScene } from "./ScoreScene.js";

export function launch() {
    return new Game({
      type: AUTO,
      scale: {
        mode: Scale.RESIZE,
        width: window.innerWidth * window.devicePixelRatio,
        autoCenter: Scale.CENTER_BOTH,
        height: window.innerHeight * window.devicePixelRatio,
      },
      parent: "game",
      backgroundColor: "#201726",
      physics: {
        default: "arcade",
      },
      scene: [PlayScene, ScoreScene]
    });
  }
Enter fullscreen mode Exit fullscreen mode

If you save your changes and run the game, when your player is hit by a bomb, the scene will change to the Score scene! Let’s add similar logic to the Score scene to start a new game.

Put the following code inside the create() block in ScoreScene:

// Add pointerdown handler to restart game
this.input.once('pointerdown', () => {
        this.scene.stop('ScoreScene')
        this.scene.start('PlayScene')
});
Enter fullscreen mode Exit fullscreen mode

We can now seamlessly switch between scenes to end and start a new game!

Here is the git commit with changes for this section.

Passing Data Between Scenes

Right now, our Score Scene isn’t very exciting. Let’s make it more fun by displaying the final score, along with a rank for that score to let the player know how they did. To do this, we’ll need to pass the score from the PlayScene to the ScoreScene.

In Phaser, we can pass an object as the second parameter to the start() method to pass data to the new scene. Update the start method in the overlap of the PlayScene to add the score data.

Note: You’ll also need to REMOVE the line that reset the score to zero, otherwise you’ll just pass zero every time! Because we are now stopping the scene, the score will automatically be reset to zero when a new PlayScene is started again.

   // stops Play Scene and starts Score Scene

    this.scene.stop('PlayScene')
    this.scene.start('ScoreScene', {score: this.score});
Enter fullscreen mode Exit fullscreen mode

To make use of this data being passed to the ScoreScene, we’ll use an init() block in ScoreScene to capture and assign the score data.

Add the following block to your ScoreScene class, above the create() block:

 init(data) {
        this.finalScore = data.score;
}
Enter fullscreen mode Exit fullscreen mode

This takes the score passed to the start() method and assigns it to a new variable in the scope of our new scene, this.finalScore. We can now use this to display text with the score in the scene.

Let’s also calculate a rank based on the score and display that on the screen. Here is the scale I used, but feel free to have fun with yours!

The following should all be within the create() block on the ScoreScene.

// calculates rank based on score
this.scoreRank = this.finalScore > 500 ? 'A: Prime Protocol!' : this.finalScore > 300 ? 'B: Efficient Engine' : this.finalScore > 100 ? 'C: Routine Maintenance' : 'F: Gears Jammed'

// adds final score text to screen
this.scoreText = this.add.text(this.screenCenterX, this.screenHeight / 2 - 100, 'Score: ' + this.finalScore, {fontSize: '20px', fill: 'green'}).setOrigin(0.5, 0.5);

// adds score rank text to screen
this.rankText = this.add.text(this.screenCenterX, this.screenHeight / 2 - 50, 'Rank ' + this.scoreRank, {fontSize: '20px', fill: '#ffffff'}).setOrigin(0.5, 0.5)
Enter fullscreen mode Exit fullscreen mode

Now, our Score scene is a lot more interesting.

Screenshot of score scene

You can use this method of passing data via the start() method and retrieving it in the init() block to transfer data between your scenes as needed. This is helpful if you are not using global state for your game, or only need to pass some data that does not need to be stored in global state.

Here is the git commit with changes for this section.

Passing Score Data from Phaser to Vue

So now we know how to pass data between scenes within Phaser, but what about getting the data out of our game and back to our Vue app? Let’s tackle that in this section.

First, let’s update our Phaser game to emit the score data. We can do this using a JavaScript CustomEvent constructor, which you can read more about here.

When the game ends, we’ll create a custom event containing the score data from our game, and dispatch it to the window. Because Phaser is based on JavaScript, we can use JavaScript constructors like CustomEvent() right within our game code.

Add the custom event within the overlap for your bombs and player, in the same place where the rest of our game over logic is handled. Make sure it’s declared before you stop the scene. Your code should look like this:

  this.physics.add.overlap(this.player, this.bombs, function(object1, object2) {

    // Destroys bomb that triggered overlap
    const bomb = (object1.key === 'player') ? object1 : object2;
    bomb.destroy();

    // Stops game object generation, pauses physics, and resets score to zero
    createStarLoop.destroy();
    createBombLoop.destroy();
    this.physics.pause();

    // When the game ends, dispatch the event with the score and rating
    const gameEndEvent = new CustomEvent("gameEnded", {
       detail: { score: this.score }
    });

    window.dispatchEvent(gameEndEvent);

    // stops Play Scene and starts Score Scene

    this.scene.stop('PlayScene')
    this.scene.start('ScoreScene', {score: this.score});

  }, null, this);

  }
Enter fullscreen mode Exit fullscreen mode

We are now emitting an event called gameEnded that contains the score data. The next step is to update our Vue app to handle this event.

We are going to accomplish two tasks:

  • Add functionality within our PhaserContainer.vue to handle the event emitted by Phaser
  • Use Provide/Inject in Vue to update and display a list of scores

First, add the following code to the <script> tag in PhaserContainer.vue , after the handleClickStart() function:

// adds score when event emitted from Phaser
function handleGameEnded(event: Event) {
  const customEvent = event as CustomEvent;
  addGameScore(customEvent.detail.score);
}

// adds event listener for gameEnded event
onMounted(() => {
  window.addEventListener("gameEnded", handleGameEnded);
});

// removes event listener for gameEnded event
onUnmounted(() => {
  window.removeEventListener("gameEnded", handleGameEnded);
});
Enter fullscreen mode Exit fullscreen mode

You’ll need to add onMounted and onUnmounted to your Vue import statement as well:

import { onMounted, onUnmounted, ref } from 'vue'
Enter fullscreen mode Exit fullscreen mode

You’ll notice there is a addGameScore() function being called within the “gameEnded” event handler. This will be part of our Provide/Inject in Vue. Let’s move to the App.vue component to add this.

Within the <script> tag, add the following code:

import { provide, ref} from 'vue';

const gameScores = ref<number[]>([])

const addGameScore = (score: number) => {
  gameScores.value.push(score)
}

provide('gameScores', {
  gameScores, addGameScore
})
Enter fullscreen mode Exit fullscreen mode

This simply creates an array ref to store our scores, and defines a method to add new scores to the ref. We then provide both the gameScores ref and the addGameScore method to our app.

Let’s also create a types.ts file in the src directory, and export an interface for our Provider.

export interface GameScoresProvider {
    gameScores: number[];
    addGameScore: (score: number) => void;
  }
Enter fullscreen mode Exit fullscreen mode

Back in our PhaserContainer.vue, we can now inject the addGameScore method. Make sure to add injectto your Vue import statement.


import { GameScoresProvider } from '@/types'

// injects addGameScore method
const { addGameScore } = inject<GameScoresProvider>('gameScores')!;
Enter fullscreen mode Exit fullscreen mode

Finally, in our ScoresPage.vue view, we’ll display the list of scores. Update the template in your file to the following:

<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Scores</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content>
      <h3 v-show="!gameScores.length">No scores yet!</h3>
      <ion-list>
        <ion-item v-for="(score, index) in gameScores" :key="index">
          <ion-label>
            <h2>Score: {{ score }} </h2>
          </ion-label>
        </ion-item>
      </ion-list>
    </ion-content>
  </ion-page>
</template>
Enter fullscreen mode Exit fullscreen mode

And update the <script> tag to the following:

<script setup lang="ts">
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel } from '@ionic/vue';
import { inject } from 'vue'
import { GameScoresProvider } from '@/types'

// injects gameScores array ref
const { gameScores } = inject<GameScoresProvider>('gameScores')!;

</script>
Enter fullscreen mode Exit fullscreen mode

Now, as we play our game, we’ll see the list of scores update in the Scores tab!

Screenshot of Scores tab

As a final step, add some text to your AboutPage.vue. It can be whatever you want!

Here is the git commit with the changes for this section.

What’s Next

We now have ourselves a game app that launches your game, navigates between scenes within Phaser, and displays a list of your scores within the app. The next (and final) step is to add support for iOS and Android so you can play the game as intended — on a mobile device!

Let's go!

Top comments (0)