【pixi.js】Spriteを学ぶ前にGUIスペースを確保しよう

javascript

GUI ゲーム画面

こんにちは。継続の錬金術士なおキーヌです。

ブログ毎日更新は119日目になります。

【pixi.js】マップチップを配列化して画面に敷き詰めようの続きですね。

マップチップ敷き詰めSprite編に入る前にGraphisクラスのおさらいとして、
ゲームのGUI画面用のスペースを確保しておきましょう。

最近のゲームではあまりない表現かもしれませんが、レトロゲームの時代は1画面にGUIを表示して、
ゲーム画面を一部だけに表示するという手法が良く使われていました。

ファミコン時代でも3D風ダンジョンや選択式アドベンチャーの場合はGUIスペースを確保しているゲームが多かったです。

RPGで例えるなら(正確にはアクションRPG)元々PCゲームだったイースとかがわかりやすいでしょうか。

今回そんなレトロゲームに習って、作っていくゲームはGUIフレームを使用するゲームにしようと思います。

PIXI.Graphicsを使ってGUIスペースを確保しよう

GUIスペースの大きさはシステム側で数値を設定しておきましょう。

src/ts/config/system.tsに追記

GUIのサイズと位置情報をsystem.tsに記述して使いまわせるようにしましょう。

// GUIのサイズと位置設定
const guiParams = [
  { x:0, y:0, width:296, height:8 },
  { x:0, y:8, width:8, height:208 },
  { x:296, y:0, width:104, height:216 },
  { x:0, y:216, width:400, height:84 },
];

このように配列を用意してそれぞれの要素にkey:value型のオブジェクトを記述します。

これも関数にして数値だけを与えたら自動的にオブジェクトが生成されるようにしたほうがいいのでしょうが
今はゲームを作ることが優先なので冗長な書き方になっています。

外部で使えるように設定する

このままだとこのファイル内でしか使えない状態なのでexportしているオブジェクトに組み込みましょう。

export const SYSTEM  = {
~~~省略~~~
  guiParams:guiParams,
}

既存の記述は省略しています。

他のグリッドサイズを呼び出せるようにGUIフレームサイズもSYSTEM.guiParamsとして使えるようにしています。

GUIフレームを描写しよう

やっていることはマップチップとなんらかわりないのですが、おさらいも含めて軽く説明します。

まずはコードを見てください。

    // GUIフレーム用意関数
    private GuiFrameInit() {
        // GUI分生成
        for (let i=0; i<SYSTEM.guiParams.length; i++) {
            // i番目にnewしておく 
            this.GuiFrame[i] = new PIXI.Graphics();
            // 座標とサイズを設定
            this.GuiFrame[i].x = SYSTEM.guiParams[i].x;
            this.GuiFrame[i].y = SYSTEM.guiParams[i].y;
            this.GuiFrame[i].width = SYSTEM.guiParams[i].width;
            this.GuiFrame[i].height = SYSTEM.guiParams[i].height;
            // 塗りつぶしを開始
            switch (i) {
                case 1:
                this.GuiFrame[i].beginFill(0x333333);
                break;
                case 2:
                this.GuiFrame[i].beginFill(0x666666);
                break;
                case 3:
                this.GuiFrame[i].beginFill(0x999999);
                break;
                default:
                this.GuiFrame[i].beginFill(0xcccccc);
                break;
            }
            // i番目の矩形を指定位置、指定サイズで塗りつぶす
            this.GuiFrame[i].drawRect(0,0,SYSTEM.guiParams[i].width,SYSTEM.guiParams[i].height);
            // 塗りつぶしを終了
            this.GuiFrame[i].endFill();
        }
    }

今回繰り返す回数はすでに分かっているので直接4と書いてもいいのですが
なるべく関数の処理にリテラルを使わないようにしたいので設定したパラメーターの配列の数だけ回す設定にしました。

コチラもちゃんと描写されているのかわからないのでループ回数によって色を変化させています。

最終的にはドット絵に差し替えるので一旦わかりやすいようにしているだけですね。

後は本当にマップチップと同じやり方で描写しています。

GUIフレームをステージに追加しよう

マップチップをステージに繰り返しで追加したように、
GUIフレームも追加してみましょう。

マップチップをステージに追加するコードをコピペして配列の指定を変更するだけでもいいのですが、
プログラミング的に同じコードを書くのはナンセンスなので関数に切り分けてしまいましょう。

    // 初回ステージ追加
    private addStageInit() {
        // マップチップ配列をステージに追加
        this.addStageArray(this.MapChips);
        // GUIフレーム配列をステージに追加
        this.addStageArray(this.GuiFrame);
    }

    // 配列ステージ追加メソッド
    private addStageArray(ary:Array<PIXI.Graphics>) {
        ary.map( (item) => {
            this.App.stage.addChild(item);
        });
    }

mapメソッドを使ったものは引数に配列を受け取って処理する関数に切り分けました。

そして、その関数を使ってマップチップもGUIフレームもステージに追加しましょう。

こうするとコードがすっきりして見やすくなりますし今後ステージに追加する配列が増えても
同じメソッドを使って追加していきましょう。

コンパイルして実行してみよう

それでは毎回恒例のちゃんと描写されているか見てみましょう。

npm run renderer-dev

コンパイルが終わったら起動してみましょう。

npm run dev

以下の画像のようになればOKです。

pixi.js コンパイル結果

ステージに追加メソッドはもう少し改良が可能だが……

熟練のプログラマが見たら、まだまだこのステージ追加メソッドのコードを完結に書けるかもしれません。

しかし何でもかんでもコードを効率化しまくってもいいわけではありません。

コード自体はスッキリしますがコードの可読性が下がってしまう可能性があります。

例えるとIf分を使ってtrueかfalseを返す場合、下記のような書き方をします。

if(hoge) {
    return true;
} else {
    return false;
}

何をやっているかがよくわかりますよね?

これは三項演算子といった書き方に置き換えると1行で表現することが出来ます。

※使用する言語によってはないかもしれません

return hoge ? true : false;

このコードはhogeがtrueであればtrueを返してfalseであればfalseを返すという意味になります。

三項演算子を知っていれば何をしているのかわかりますが、例えば自分のコードを
プログラミング初心者に渡してみると、プログラミング初心者は何をしているのか理解するのに時間がかかります。

たとえ話なのでこれぐらいもわからないのは初心者であっても、
プログラマとしては失格でしょっていうのは無しでお願いします(笑)

なので何でもかんでも省略して書けばいいというものではありません。

私の場合はチーム開発する気はないですが、期間を開けてからソースコードを見ても理解しやすいように
if分のようにあえて冗長に書くときもありますが、明らかにコピペしているようなソースコードは最適化します。

Spriteの予習をしよう

このままだと記事の文字数が少なくて質の悪い記事と判定されるかもしれないので
最後にSpriteの予習をしましょう。

Spriteとは何かはリンク先で見てください。

なんのこっちゃわからないという人は、プログラミングで画像を表示させるための仕組みと覚えておくと良いです。

仕組みを知るのは良いことですが、それだけで数記事分消費するぐらいの文章量になりそうなので割愛します。

今表示しているマップチップはPIXI.Graphicsというクラスを利用しているので用意した画像を表示することはできません。

それではゲームぽくならないので、今後はPIXI.Spriteクラスに置き換えて画像を表示していきます。

マップチップもGUIフレームも画像にしなければいけないのですが
基本的な使い方はGraphisとさほど変わりません。

ステップアップも兼ねてGraphicsから説明をしていたわけです。

それに画像がない場合はGraphicsを使うと簡単に矩形を表示できる仕組みを持っていますので
開発中には意外と便利なのです。

もう一つ言うとわざわざSpriteで画像を使う必要のないところは、
Graphicsにすることで処理を軽くできますし読み込む画像も減ります。

分かりやすい例で言えばファミコン時代のFFやドラクエのメッセージウィンドウでしょうか。

FFはフレーム枠があるのでちょっと面倒ですがGraphicsでも十分表現できるシンプルさです。

メッセージウィンドウを豪華にしたい場合はSpriteを使えばいいですが、
シンプルなメッセージウィンドウでいい場合はGraphicsを使えばOKです。

Spriteを使うには画像のロードが必須

次回の記事ではコード付きで説明しますが今回は文字のみで予習しておきましょう。

Spriteで使うための画像はプログラム側でロードしておかなければいけません。

よくゲームの最初にロード画面が入ると思うのですが、そこで次のシーンに使う画像を読み込んでいるわけです。

読み込みという処理は思った以上に重い処理で、非同期処理を使わないと最悪プログラムが停止してしまいます。

ローディング画面で何かが動いているのは、プログラムがフリーズしていないということを証明するためでもあるわけですね。

ロードは基本的にプログラムの初期化の時点でやるのがセオリーです。

容量が大きい画像等は、ローディング画面を設けてロードが終わるまでは次の画面に進まないようにしなければいけません。

最近のゲームではシームレスなオープンワールドゲーが増えていますが、
あれも移動中に一定の位置にたどり着いたら裏でロード(非同期処理)をしているわけですね。

最初のロードですべてロードをしておくというタイプのものもありますが、
かなりロード時間が長くなりますしメモリも大量に消費してしまいます。

なので必要な時に必要なもの+αというローディングを心掛けるのがよさそうです。

例のごとく最後にソースコードを乗せておきます。

それでは。

import * as PIXI from 'pixi.js'
import { SYSTEM } from '../../config/system';

class PIXI_MainProcess {

    /**
     * ----------------------------------------------------
     * エイリアス作成
     * ----------------------------------------------------
     *  */ 
    // pixi.jsアプリケーション
    private PIXI_Application = PIXI.Application
    // ローダー
    private PIXI_loader = PIXI.loader
    // リソースローダー
    private PIXI_resources= PIXI.loader.resources

    /**
     * ----------------------------------------------------
     * クラス変数
     * ----------------------------------------------------
     *  */ 
    // pixiアプリケーション生成
    private App:PIXI.Application = this.pixiApplicationCreate(SYSTEM.width,SYSTEM.height);

    //--------------------------------------------


    // マップ用変数
    private MapChips:Array<PIXI.Graphics> = [];

    // GUI用変数
    private GuiFrame:Array<PIXI.Graphics> = [];

    //--------------------------------------------

    // コンストラクタ
    constructor() {

        // 初期化プロセス
        this.initializePIXI();
    }

    // 初期化プロセス
    private initializePIXI() {

        // ピクセル倍率変更
        this.pixelScaleUP();

        // htmlのbodyにPIXIAPPを追加
        document.body.appendChild(this.App.view);

        // マップチップ初期化
        this.MapChipsInit();

        // GUI初期化
        this.GuiFrameInit();

        // オブジェクトをステージに追加
        this.addStageInit();

    }

    // PIXIアプリケーションオブジェクト作成
    private pixiApplicationCreate(screenWidth:number,screenHeight:number) {
        return new this.PIXI_Application({
            width:screenWidth,
            height:screenHeight,
            antialias: false,
            transparent: false,
            resolution: 1
            }
        );
    }

    // ピクセル倍率を2倍に変更
    private pixelScaleUP() {
        this.App.renderer.roundPixels = true;
        this.App.renderer.resize(SYSTEM.width, SYSTEM.height);
        this.App.stage.scale.set(2,2);
        this.App.stage.interactive = true;
        PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
    }


    // // セットアップ関数
    private setup(app:PIXI.Application) {

        app.ticker.add(delta => this.gameloop(delta));

        // // .load()は「undifinedを指定して何も返さない」としなければtslintでエラーが出る
        // // void型にして下記を消すとクラス変数がnullにされてしまうのでこれで対応
        // return undefined;
    }

    // メインループ
    private gameloop(delta:number) {
    }

// マップチップ初期化
    private MapChipsInit() {
        // y座標折り返しカウント
        let yCount:number = 0;
        // 折り返し時に色変更するためのフラグ
        let sw = false
        // 画面サイズ分のマップチップ配列生成
        for (let i=0; i<(SYSTEM.gridX * SYSTEM.gridY); i++ ) {
          // i番目の配列にnewする
          this.MapChips[i] = new PIXI.Graphics();
          // i番目のx座標設定
          this.MapChips[i].x = ((i%SYSTEM.gridX) * SYSTEM.chipSize);
          // X座標が端っこまで行ったら改行する
          if(i !== 0 && i%SYSTEM.gridX === 0) {
            // y座標折り返しカウントをインクリメント
            yCount++;
            // 折り返したら色を変更するフラグを切り替え
            sw = !sw
          } 
          // i番目のy座標設定
          this.MapChips[i].y = yCount * SYSTEM.chipSize;
          // i番目の横幅設定
          this.MapChips[i].width = SYSTEM.chipSize;
          // i番目の縦幅設定
          this.MapChips[i].height = SYSTEM.chipSize;
          // swフラグによって色を切り替えてi番目の塗りつぶしを準備
          if(sw) {
            this.MapChips[i].beginFill(0x00ff00);
          }
          else {
            this.MapChips[i].beginFill(0xff00ff);
          }
          // i番目の矩形を指定位置、指定サイズで塗りつぶす
          this.MapChips[i].drawRect(0,0,SYSTEM.chipSize,SYSTEM.chipSize);
          // 塗りつぶしを終了
          this.MapChips[i].endFill();
        }
    }

    // GUIフレーム用意関数
    private GuiFrameInit() {
        // GUI分生成
        for (let i=0; i<SYSTEM.guiParams.length; i++) {
            // i番目にnewしておく 
            this.GuiFrame[i] = new PIXI.Graphics();
            // 座標とサイズを設定
            this.GuiFrame[i].x = SYSTEM.guiParams[i].x;
            this.GuiFrame[i].y = SYSTEM.guiParams[i].y;
            this.GuiFrame[i].width = SYSTEM.guiParams[i].width;
            this.GuiFrame[i].height = SYSTEM.guiParams[i].height;
            // 塗りつぶしを開始
            switch (i) {
                case 1:
                this.GuiFrame[i].beginFill(0x333333);
                break;
                case 2:
                this.GuiFrame[i].beginFill(0x666666);
                break;
                case 3:
                this.GuiFrame[i].beginFill(0x999999);
                break;
                default:
                this.GuiFrame[i].beginFill(0xcccccc);
                break;
            }
            // i番目の矩形を指定位置、指定サイズで塗りつぶす
            this.GuiFrame[i].drawRect(0,0,SYSTEM.guiParams[i].width,SYSTEM.guiParams[i].height);
            // 塗りつぶしを終了
            this.GuiFrame[i].endFill();
        }
    }

    // 初回ステージ追加
    private addStageInit() {
        // マップチップ配列をステージに追加
        this.addStageArray(this.MapChips);
        // GUIフレーム配列をステージに追加
        this.addStageArray(this.GuiFrame);
    }

    // 配列ステージ追加メソッド
    private addStageArray(ary:Array<PIXI.Graphics>) {
        ary.map( (item) => {
            this.App.stage.addChild(item);
        });
    }


}

export default PIXI_MainProcess;

const GameFrame:PIXI_MainProcess = new PIXI_MainProcess;