頑張らないために頑張る

ゆるく頑張ります

Phaser3でプログレスバー付きのローディングシーンを追加する

Posted at — Sep 11, 2021

ああ、よくあるよね

ゲームのオブジェクトを読み込むときによくあるのがローディング画面。ゲームにもよりますが、だいたい「Loading…」みたいな文字列を表示しつつ、プログレスバーがだんだん進行していって、ロードが100%終了するとゲームが始まるような画面のことをここではローディング画面と表現しています。今回は、このローディング画面をPhaser3で実装しようという話。

実際どうやるの

基本的に、Phaser3はゲームで使うオブジェクト(アセットと言います)をpreload()関数内でロードします。このアセットのロードがすべて終了するところを100%として、「今xx%までロードできましたー」という情報をPhaserからもらいつつ、その進行具合で画面に表示したプログレスバーの長さを更新します。Phaserはその進行具合を情報として返してくれるので、こういう実装ができるのですね。

ここではわかりやすく0%でプログレスバーの表示がなし、100%でプログレスバーが画面右まで到達している状態とします。また、ロードが100%になった時点で自動的にローディング画面からゲーム本編に遷移するものとします。ロードの進捗率が100%になったら画面をクリック、とか毎度したくないしね。

なお、「ゲーム内のSDキャラクターがちょこちょこと走り回る」ような、コンテンツ的にリッチなローディング画面もゲームによってはあったりますが、今回は考えないことにします。というのは、「ゲームで使うアセットをロードするためのローディング画面で、ローディング画面に使うアセットをロードする必要がある」という入れ子構造になってしまい、そのアセットを事前にどこかでロードしておく必要があるためです。ただ、事前にどこかでロードさえしておけば、Phaser3ではアセットのキャッシュはどのシーンからでもアクセスが可能です。なので、序盤のどこかでロードしておいてそれ以降のローディング画面ではアセットを表示する、というのも十分検討できる手段だと思います。

ただし、前述のとおりここではプログレスバーの長方形について、進捗率にしたがって幅を変えるだけにします。

サンプル

codepenで実装してみました。

See the Pen loading scene with phaser3 by ysko909 (@ysko909) on CodePen.

wikimediaにあるファイルを読み込んでいるのですが、うまく動作しないことがあります。そういう場合は更新してみてください。なお、ネットワーク環境が良好だとすぐダウンロードできるので、ローディング画面が一瞬で終わってしまうことがあるようです。そのときは心の目で見てください(ェ

コード

class BaseScene extends Phaser.Scene {
    
    nextSceneName: string;
    canMoveToNextSceneWithClick: boolean;    
    progressBar!: Phaser.GameObjects.Rectangle;
    game!: Phaser.Game;
    wholeCanvas!: Phaser.GameObjects.Zone;

    constructor(sceneName: string, nextSceneName: string, canMoveToNextSceneWithClick = false) {
        super(sceneName);
        this.nextSceneName = nextSceneName;
        this.canMoveToNextSceneWithClick = canMoveToNextSceneWithClick;


    }

    preload() {
        this.game = this.sys.game;

        this.progressBar = this.add.rectangle(0, this.game.canvas.height / 2, 0, 8, 0xffffff);
        this.loadingText = this.add.text(this.game.canvas.width / 2, this.game.canvas.height / 2 - 30, 'loading...', {});
        this.loadingText.setOrigin(0.5);

        // ロード進行中はプログレスバーの伸縮を進捗率に応じて変化させる。
        this.scene.scene.load.on('progress', this._updateBar.bind(this));
        // 個別のファイルのロードが完了したとき、ロードできたファイルの情報をコンソールに表示する。
        this.scene.scene.load.on('load', this._displayLoadedFileInfo.bind(this));
        // すべてのファイルのロードが完了したらローディング画面はフェードアウトしてメインのシーンに遷移する。
        this.scene.scene.load.on('complete', this._fadeoutMainCamera.bind(this));

    }

    /** 
     * ローディングプログレスバーの幅を進捗率に応じて伸縮する。
     * @param percentage ローディングの進捗率。 
     */
    private _updateBar(percentage: number) {

        this.progressBar.width = this.game.canvas.width * percentage;
    }

    /**
     * ロードが成功したファイルの情報をコンソールに表示するデバッグ用メソッド。
     * @param file ロードしたファイルオブジェクト。
     */
    private _displayLoadedFileInfo(file: any) {
        const src = file.src ? file.src : 'unknown/path';
        const key = file.src ? file.key : 'unknown/key';

        console.log(`load asset: key=${key}, src=${src}`);
    }

    /**
     * メインカメラを1秒かけてフェードアウトさせる。
     */
    private _fadeoutMainCamera() {

        this.cameras.main.fadeOut(1000, 0, 0, 0);

    }

    create() {

        // フェードアウトが完了したとき
        this.cameras.main.once(Phaser.Cameras.Scene2D.Events.FADE_OUT_COMPLETE, () => {

            if (this.progressBar) {
                this.progressBar.destroy();
                this.loadingText.destroy();
            }

            this.cameras.main.fadeIn(1000, 0, 0, 0);
          
            this.displayBackground();
            this.displayText(this.nextSceneName);

        });

        const { width, height } = this.game.canvas;
      
        // クリックによる画面遷移を条件により実装
        if (this.canMoveToNextSceneWithClick) {

            /** キャンバス全体を領域とするゲームオブジェクト。クリック要素を持たせるために使う */
            this.wholeCanvas = this.add.zone(width / 2, height / 2, width, height);

            this.wholeCanvas.setInteractive({
                useHandCursor: true
            });

            this.wholeCanvas.on('pointerdown', () => {
                this.wholeCanvas.removeInteractive();
                this.changeScene(this.nextSceneName);
            });
        };

    }

    public changeScene(destinationSceneName: string) {

        this.cameras.main.fadeOut(800, 0, 0, 0);
        this.cameras.main.once(Phaser.Cameras.Scene2D.Events.FADE_OUT_COMPLETE, () => {
            this.scene.start(destinationSceneName);
            
        });
    }

    public displayText(text: string){
      if (text){
        this.add.text(this.game.canvas.width / 2, 200, 'Go to ' + text).setOrigin(0.5);
      }
        
    }

    public displayBackground(){
      
    }

}

class Scene1 extends BaseScene {
  constructor(){
    super('scene1', 'scene2', true);
    
  }
  
  preload(){
    super.preload();
    
    this.load.image('space', 'https://upload.wikimedia.org/wikipedia/commons/e/ee/Artemis_Base_Camp.png');
  }
  
  create(){
    super.create();
    
  }
    
  public displayBackground(){
    this.add.image(0, 0, 'space');
  }
  
}

class Scene2 extends BaseScene {
  constructor(){
    super('scene2', '', false);
  }
  preload(){
    super.preload();
    
    this.load.image('galaxy', 'https://upload.wikimedia.org/wikipedia/commons/5/5e/Galaxy2.png');
  }
  
  create(){
    super.create();
  }
  public displayBackground(){
    this.add.image(0, 0, 'galaxy');
  }
}

const config: Phaser.Types.Core.GameConfig = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  resolution: window.devicePixelRatio,
  parent: 'game-app',
  scene: [Scene1, Scene2]
};

new Phaser.Game(config);

Sceneクラスを継承して、BaseSceneという新しいクラスを作成します。新クラスの中でローディングに関する実装を行うことで、ローディング画面を実装したシーンを作成したい場合は、BaseSceneクラスを継承して新しいクラスを作成すればいいので楽です。

this.progressBar = this.add.rectangle(0, this.game.canvas.height / 2, 0, 8, 0xffffff);
this.loadingText = this.add.text(this.game.canvas.width / 2, this.game.canvas.height / 2 - 30, 'loading...', {});
this.loadingText.setOrigin(0.5);

// ロード進行中はプログレスバーの伸縮を進捗率に応じて変化させる。
this.scene.scene.load.on('progress', this._updateBar.bind(this));
// 個別のファイルのロードが完了したとき、ロードできたファイルの情報をコンソールに表示する。
this.scene.scene.load.on('load', this._displayLoadedFileInfo.bind(this));
// すべてのファイルのロードが完了したらローディング画面はフェードアウトしてメインのシーンに遷移する。
this.scene.scene.load.on('complete', this._fadeoutMainCamera.bind(this));

実際に、ローディング画面に関するコードを記述しているのは上記の部分です。ロード中はその進捗具合に応じて長方形の幅を更新し、すべてのアセットがロードできたら、ローディング画面に表示した長方形やテキストなどのオブジェクトは破棄してからメインの画面を表示します。ローディング画面からメイン画面へ切り替えるときは、フェードイン・フェードアウトさせます。シーンの切り替えの際も同様です。

サンプルを実行してみるとわかりますが、一瞬ローディング画面が表示されてロードが終了したら自動的にフェードアウトし、背景が表示されます。クリックすると次のシーンに遷移しますが、同じクラスを用いたシーンなので同じ挙動をします。ただし、画面へのクリックが可能なのは最初のシーンだけにしたかったので、「画面クリックを可能にするか否か」をコンストラクタにて指定することにしました。

シーンの切り替えにフェードイン・フェードアウトを実装しているのは、単純に趣味です。ロードが終わった瞬間に余韻なくスパッと画面が切り替わるような実装がお好みである場合、フェードイン・フェードアウトに関係する部分のコードを削除するだけでOKです。

まとめ

ここでは独自クラスを作って、その中にローディング画面の表示機能を実装しました。アセットを事前にロードしておけばこのクラスから参照するようにコードを改変することで、「単純な長方形が伸びるだけ」の味気ないローディング画面を、より表現豊かな画面に作り変えることもできます。これだけのコードで実装できるので、やっぱりPhaser3はすごい。

参考

  1. Creating a Preloading Screen in Phaser 3
  2. phaser3:プログレスバーを作る
  3. Phaser3 Loadingシーン
comments powered by Disqus