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

javascript

マップチップ配列 描写

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

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

【pixi.js】画面にマップチップを表示しよう:PIXI.Graphics編の続きですね。

今回は1つしかなかったマップチップを画面全体に引き伸ばしてみましょう。

変数だと1つしか格納できないので、配列化していきます。

早速変数を配列にしてしまいましょう。

マップチップ変数を配列化していこう

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

前回の変数をコメントアウトしてますが違いがわかるようにしているだけなので消してもらっても大丈夫です。

変数の型を「Array」にして<>の中に型を指定することで、PIXI.Graphics型の配列を作るという記述になります。

次に、変数で指定していた座標や大きさを指定してたmapChipInitメソッドをごっそりと変更してみましょう。

※mapChipInitからMapChipsInitに名称を変更しているので気を付けてください。

    // マップチップ初期化
    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();
        }
    }

追加部分だけ説明します。

y座標折り返しカウント

マップ配列は多次元配列を使ってもいいのですが、1次元配列と2次元配列では処理の重さが段違いなので
マップを作るときは基本的に1次元配列を使用して疑似的に2次元配列ぽく使用しています。

指定したx座標まで描写したら回数を増やして次の行を描写するようにするためのカウントです。

折り返し時に色変更するためのフラグ

現在紫の矩形を描写していますが、折り返しても紫のままだと
大きく形を描写しているようにしかみえないので、折り返すごとに色を変更するためのフラグを作りました。

画面サイズ分のマップチップ配列生成

今回のメインの部分がこのforを使った処理ですね。

for (let i=0; i<(SYSTEM.gridX * SYSTEM.gridY); i++ ) {

【pixi.js】Electronで動くゲームプログラミングをしよう準備編で軽く説明しましたが、
/src/ts/config/に格納してある「system.ts」にSYSTEMモジュールとして解像度やグリッドサイズなどの設定を記述して使いまわせるようにしています。

forループの条件設定に注目してください。

1ループずつnewをして配列にマップチップオブジェクトを追加していきたいので
条件を縦x横のグリッド数にして回します。

i番目のx座標設定

繰り返すたびにマップチップ分のx座標をずらして描写していくために、
x座標には計算式の結果を与えるようにしました。

this.MapChips[i].x = ((i%SYSTEM.gridX) * SYSTEM.chipSize);

日本語で意味を書くと

マップチップi番目のx座標に、iを横グリッド数で割ったものにチップサイズを掛けたものを格納します。

ゲームプログラミングにおいて割ったあまりというのはよく使う計算なので覚えておきましょう

ちなみマップチップは16×16のサイズなのでループする度に計算式の結果は以下のようになって行きます。

0,16,32,48,64…と16ずつ増えてズレていきます。

割ったあまりなのでiがグリッドサイズを越えたら0に戻るので、その時にy座標をずらせばマップチップを
一次元配列で敷き詰めることが出来ます。

現在描写解像度を2倍にしているのでピクセル数的には32×32になっていますが、内部的には実寸サイズなので
混乱しないように気を付けましょう。

X座標が端っこまで行ったら改行する

最後にx座標がグリッドサイズの最後まで行ったらy座標を下にマップチップ分ずらしたいので、
割ったあまりが0になったときに条件に一致するようにしました。

if(i !== 0 && i%SYSTEM.gridX === 0) {

この時、初回のiが0なので割ったあまりが0になり条件に一致してしまうので
アンド条件として「iが0じゃないとき」という条件も付けています。

条件に一致したらyカウントを1つ増やし、色を変更したいのでswフラグを反転させています。

これ以降の処理は基本的に配列風に変更したこととリテラル表記をなくしただけなので省略します。

ステージ追加

マップチップを配列化したことによって、全てをステージに追加しなければいけません。

forで回してもいいのですが、折角javascriptのES6記法を使えるということで
mapメソッドを使って1つずつ回してみましょう。

mapメソッドとは、配列型が持つメソッドです。

1つずつ取り出して処理を施して返していくと、新たな配列を作ることが出来ます。

foreach等でもいいのですが、処理が軽いらしいのでmapメソッドを使える時はmapメソッドを使いましょう。

使い方は慣れるまで難しいかもしれませんが、慣れてくると便利になります。

mapメソッドの使い方は「javascript ES6 map filter」で検索するといっぱい出てきます。

    // 初回ステージ追加
    private addStageInit() {
        // マップチップ配列をステージに追加
        this.MapChips.map( (item) => {
            this.App.stage.addChild(item);
        });
    }

余談ですが、filterメソッドという繰り返しが出来るメソッドもあります。

mapは1つずつ内部で処理をして返すことで新たな配列を作るのに対し、
filterは内部でtrueを返したものだけの配列にすることが出来ます。

このfilterは結構ゲーム制作に有効なんじゃないかと思ってます。

例えば、プレイヤーキャラクターを配列に組み込んで毎ターン開始時にfilterで回して
特定の値が0になっているキャラクターに何かしらの処理を施す。

という処理が容易になります。

工夫次第で色々使えそうですね。

話を戻しますが、mapメソッドで1つずつステージに追加しているだけですね。

returnの命令は書いていないので新たな配列は作成していません。

なので別にforでもforeachでもなんでも大丈夫ですが、
mapメソッドは記述量も少ないので見やすくて処理も軽くていいことづくめです。

初期化プロセス initializePIXI() の中身のメソッド名を変更

マップチップのメソッド名をmapChipInitからMapChipsInit変更したので、
初期化メソッドのところの名前も変えるのを忘れないでおきましょう。

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

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

npm run renderer-dev

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

npm run dev

どうでしたか?

以下の画像のように画面全部に描写されなかったでしょう。

pixi.js コンパイル結果

なぜかと言うと、解像度は320×240を2倍しているのに対し、画面の大きさは800×600です。
なので描写されたのは640×480というわけです。

失敗したなーと思ったのですが、そういえばUI表示するために全部に表示したらダメじゃないかと思ったので
このままでも問題ないなと思いそのままにしました。

次回はUIの描写部分に別の色を敷き詰めてみようと思います。

その為にはマップチップの描写開始位置をずらさなければいけません。

そしてそのままずらしてしまうと今度はステータス画面の表示する場所が狭くなってしまいます。

なので次回はマップチップを描写する数を減らしてUIの部分の隙間を確保していきます。

最後に今回のソースコード完全版を載せておきます。

それでは。

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> = [];

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

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

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

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

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

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

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

        // オブジェクトをステージに追加
        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));
    }

    // メインループ
    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();
        }
    }

    // 初回ステージ追加
    private addStageInit() {
        // マップチップ配列をステージに追加
        this.MapChips.map( (item) => {
            this.App.stage.addChild(item);
        });
    }
}

export default PIXI_MainProcess;

const GameFrame:PIXI_MainProcess = new PIXI_MainProcess;