今回の記事は、phina.jsでのフレーム数や、経過時間計測、シーン生成・消去、それと、dat.GUIを使う方法をについてを書いていきたいと思います。もしphina.jsが分からない方は、[phina.js]基本 — テンプレートについてをご一読いただくことをおすすめいたします。

準備

コーディングは、RunstantRunstant liteを使って試されることをおすすめします。

一応雛型として置いておきます。

<script src='https://cdn.jsdelivr.net/gh/phi-jp/phina.js@0.2.3/build/phina.js'></script>
// グローバル領域に展開
phina.globalize();

/*
 * メインシーン
 */
phina.define("MainScene",{
  // 継承
  superClass:"DisplayScene",
  // コンストラクタ
  init: function(){
    // 親クラスの初期化
    this.superInit();
    // ここに処理を書く
  },
  // 更新
  update: function(){
  
  },
});

/*
 * メイン処理
 */
phina.main(function(){
   // アプリケーションを生成
   const app = GameApp({
     // MainSceneから開始
     startLabel: "main",
   });
   // fps表示
   //app.enableStats();
   // 実行
   app.run();
});

BaseAppクラスについて

phina.jsを扱うにあたってメイン処理のところで、毎回お世話になっているGameAppクラスですが、そのGameAppクラスが継承しているBaseAppクラスを読み解きつつ、フレーム数や、経過時間、シーン生成と消去と、phina.jsでのdat.GUIの使い方などについて書いていきたいと思います。

プロパティ

プロパティ名 説明 デフォルト値
awake updateをしているかが真偽値で入る。していればtrue。 null
fps 1秒間のフレーム数が入る。 null
frame 現在のフレーム数が入る。 null
currentScene 現在のシーンが入る。 this._scenes[this._sceneIndex]
rootScene これに関してはよくわかりませんでした。すみません。 this._scenes[0]
deltaTime 1フレームにかかったミリ秒数が入る。 this.ticker.deltaTime
elapsedTime シーンが始まってからの経過ミリ秒数が入る。 this.ticker.elapsedTime
currentTime 1970年01月01日 00:00:00 UTC からその時点までのミリ秒数(Unixエポック)が入る。 this.ticker.currentTime
startTime シーンが始まった時刻のUnixエポックが入る。 this.ticker.startTime

メソッド

run()
アプリケーションを実行します。
replaceScene(scene)
シーンを変更します。

[引数]
scene: 実行中のシーンから変更したいシーンを指定します。

pushScene(scene)
シーンを新しく生成します。
replaceSceneとの違いは、replaceSceneはシーン遷移で、
pushSceneは現在のシーンを一時停止して、一時的な新しいシーンを生成する、みたいな感じみたいです。
一時停止等で使うのが良いと思います。

[引数]
scene: 新しく生成したいシーンを指定します。

popScene()
現在実行中のシーンを消します。
一時停止やオプション画面などを消すときに使用するのが良いと思います。
start()
シーンのアップデートを実行します。
stop()
シーンのアップデートをストップします。
enableStats()
fpsを表示します。
enableDatGUI(callback)
dat.GUIを表示します。

[引数]
callback: dat.GUIを使って、変更させたいパラメータを指定します。
dat.GUIについて、詳しくはdat.guiをご参照ください。

サンプル

ソースコード

// グローバル領域に展開
phina.globalize();

// 定数
const W = 640;
const H = 960;
const LINECORE_X = 320;
const LINECORE_Y = 580;

/*
 * メインシーン
 */
phina.define('MainScene', {
    superClass: 'DisplayScene',
    // コンストラクタ
    init: function () {
        // 親クラス初期化
        this.superInit();

        // スパイラルラインfill用PathShape
        this.path = PathShape({
            stroke: null,
            fill: "#ffffff"
        }).addChildTo(this);

        // スパイラルラインstroke用PathShape
        this.path2 = PathShape({
            stroke: "#ffffff",
            strokeWidth: 1,
            fill: null
        }).addChildTo(this);

        // タイム用ラベル
        this.label = Label({
            x: this.gridX.center(),
            y: this.gridY.span(2),
            fill: "#000000",
            fontSize: 30
        }).addChildTo(this);

        // 一時停止ボタン
        Button({
            text: "stop",
            width: 100,
            height: 50
        }).addChildTo(this)
            .setPosition(this.gridX.span(14), this.gridY.span(0.5))
            .onpush = () => {
                // ポーズシーン生成
                this.app.pushScene(PauseScene());
            };

        // update中の経過秒数計測用
        this.time = 0;
    },
    // 更新
    update: function (app) {
        this.time += app.deltaTime;
        // 経過秒数を取得してラベルで表示
        this.label.text = "シーン開始からの経過秒数:" + (app.elapsedTime / 1000).toFixed(3)
            + "\nupdateされている間の経過秒数:" + (this.time / 1000).toFixed(3);

        const gui = app.guiCtrl;

        // 各オブジェクトの色変更
        this.label.fill = gui.fontFill;
        this.backgroundColor = gui.backgroundColor;
        this.path.fill = gui.lineFill;
        this.path2.stroke = gui.lineStroke;

        // スパイラルライン生成
        this.SpiralLine(app.frame % 360, gui);
    },
    // スパイラルライン
    SpiralLine: function (degree, gui) {
        this.path.clear();
        this.path2.clear();

        this.path.addPath(LINECORE_X, LINECORE_Y);

        let x = LINECORE_X, y = LINECORE_Y;
        let i = 0;
        while (0 <= x && x <= W && 0 <= y && y <= H) {
            x = Math.cos(Math.PI * (degree + i) / 180) * (i / 2) + LINECORE_X;
            y = Math.sin(Math.PI * (180 + degree + i) / 180) * (-i / 2) + LINECORE_Y;

            // dat.GUIのfillにチェックが入っていたら、fill用のPathShapeを描画する
            if (gui.fill) {
                this.path.addPath(x, y);
            }
            i++;
        }
        while (i > 0) {
            x = Math.cos(Math.PI * (degree + i) / 180) * (i / 2) + LINECORE_X;
            // dat.GUIのrandomにチェックが入っていたら、線の幅をランダムにバラけさせる
            const yd = (gui.random) ? (-Math.randint(0, 25) - degree - i) : (-15 - degree - i);
            y = Math.sin(Math.PI * yd / 180) * (-i / 2) + LINECORE_Y;

            // dat.GUIのfillにチェックが入っていたら、fill用のPathShapeを描画する
            if (gui.fill) {
                this.path.addPath(x, y);
            }
            // dat.GUIのstrokeにチェックが入っていたら、stroke用のPathShapeを描画する
            if (gui.stroke) {
                this.path2.addPath(x, y);
                this.path2.addPath(LINECORE_X, LINECORE_Y);
            }
            i--;
        }
    }
});

/*
 * ポーズシーン
 */
phina.define("PauseScene", {
    superClass: "DisplayScene",
    init: function () {
        this.superInit();
        this.backgroundColor = "hsla(0, 0%, 0%, 0.8)";

        // スタートボタン
        Button({
            text: "start"
        }).addChildTo(this)
            .setPosition(this.gridX.center(), this.gridY.center())
            .onpush = () => {
                // 自身を削除する
                this.app.popScene();
            };
    }
});

/*
 * メイン処理
 */
phina.main(function () {
    // アプリケーションを生成
    const app = GameApp({
        // MainSceneから開始
        startLabel: "main",
    });
    // fps表示
    app.enableStats();
    // dat.GUI用パラメータ
    const guiCtrl = {
        random: false,
        fill: true,
        stroke: false,
        fontFill: "#000000",
        backgroundColor: "#aaaaaa",
        lineFill: "#ffffff",
        lineStroke: "#ffffff",
        stop: () => {
            app.stop();
        },
        start: () => {
            app.start();
        }
    };
    // dat.GUI表示
    app.enableDatGUI((gui) => {
        gui.add(guiCtrl, "random");
        gui.add(guiCtrl, "fill");
        gui.add(guiCtrl, "stroke");
        const folder1 = gui.addFolder("Color");
        folder1.addColor(guiCtrl, "fontFill");
        folder1.addColor(guiCtrl, "backgroundColor");
        folder1.addColor(guiCtrl, "lineFill");
        folder1.addColor(guiCtrl, "lineStroke");
        folder1.open();
        const folder2 = gui.addFolder("GameApp");
        folder2.add(app, "fps", 1, 60);
        folder2.add(guiCtrl, "stop");
        folder2.add(guiCtrl, "start");
        folder2.open();
    });
    // 参照用
    app.guiCtrl = guiCtrl;

    // 実行
    app.run();
});

解説

dat.GUI

詳しい説明は、本家様や他の解説サイト・ページ様に解説丸投げします。ちなみに、私はdat.GUI入門のページを参考にさせていただきました。

まず、GameAppの直接のプロパティ以外のパラメータやstop、startイベントを設定して、

// dat.GUI用パラメータ
const guiCtrl = {
    random: false,
    fill: true,
    stroke: false,
    fontFill: "#000000",
    backgroundColor: "#aaaaaa",
    lineFill: "#ffffff",
    lineStroke: "#ffffff",
    stop: () => {
        app.stop();
    },
    start: () => {
        app.start();
    }
};

enableDatGUIメソッドからdat.GUIにセットしています。

app.enableDatGUI((gui) => {
    gui.add(guiCtrl, "random");
    gui.add(guiCtrl, "fill");
    gui.add(guiCtrl, "stroke");
    const folder1 = gui.addFolder("Color");
    folder1.addColor(guiCtrl, "fontFill");
    folder1.addColor(guiCtrl, "backgroundColor");
    folder1.addColor(guiCtrl, "lineFill");
    folder1.addColor(guiCtrl, "lineStroke");
    folder1.open();
    const folder2 = gui.addFolder("GameApp");
    folder2.add(app, "fps", 1, 60);
    folder2.add(guiCtrl, "stop");
    folder2.add(guiCtrl, "start");
    folder2.open();
});

そして、メインシーンからGUIのパラメータの値を呼び出すために、メイン処理のところでappに独自のプロパティを作って、

// 参照用
app.guiCtrl = guiCtrl;

メインシーンのアップデートのところで呼び出して、各オブジェクトのプロパティ値を変更しています。

const gui = app.guiCtrl;

// 各オブジェクトの色変更
this.label.fill = gui.fontFill;
this.backgroundColor = gui.backgroundColor;
this.path.fill = gui.lineFill;
this.path2.stroke = gui.lineStroke;

一部、チェックボックスのものは、SpiralLineのところで、真偽値で描画するか等に反映させています。

// スパイラルライン
SpiralLine: function (degree, gui) {
    this.path.clear();
    this.path2.clear();

    this.path.addPath(LINECORE_X, LINECORE_Y);

    let x = LINECORE_X, y = LINECORE_Y;
    let i = 0;
    while (0 <= x && x <= W && 0 <= y && y <= H) {
        x = Math.cos(Math.PI * (degree + i) / 180) * (i / 2) + LINECORE_X;
        y = Math.sin(Math.PI * (180 + degree + i) / 180) * (-i / 2) + LINECORE_Y;

        // dat.GUIのfillにチェックが入っていたら、fill用のPathShapeを描画する
        if (gui.fill) {
            this.path.addPath(x, y);
        }
        i++;
    }
    while (i > 0) {
        x = Math.cos(Math.PI * (degree + i) / 180) * (i / 2) + LINECORE_X;
        // dat.GUIのrandomにチェックが入っていたら、線の幅をランダムにバラけさせる
        const yd = (gui.random) ? (-Math.randint(0, 25) - degree - i) : (-15 - degree - i);
        y = Math.sin(Math.PI * yd / 180) * (-i / 2) + LINECORE_Y;

        // dat.GUIのfillにチェックが入っていたら、fill用のPathShapeを描画する
        if (gui.fill) {
            this.path.addPath(x, y);
        }
        // dat.GUIのstrokeにチェックが入っていたら、stroke用のPathShapeを描画する
        if (gui.stroke) {
            this.path2.addPath(x, y);
            this.path2.addPath(LINECORE_X, LINECORE_Y);
        }
        i--;
    }
}

フレーム数

スパイラルライン生成時、引数に、dat.GUIのパラメータ値を調べるための変数と一緒に、その時点でのフレーム数を360で割った余りをスパイラルライン全体の基準の角度として渡しています。これで1フレーム毎に少しずつ回して変化させています。

// スパイラルライン生成
this.SpiralLine(app.frame % 360, gui);

ちなみに、PathShapeについては、以前書いてみた記事がありますのでよろしければこちらをご覧ください。

経過秒数

経過秒数の表示でシーン開始からの秒数と、update中の秒数を2つあるのは、
一時停止したり、GUIのパネルからの操作でupdateをstopしたりした場合でも、app.elapsedTimeの秒数は増え続けるため、
updateしている間だけの経過秒数を計測するための方法として、app.deltaTimeで取得した1フレーム毎の経過秒数をthis.timeに加算するというやりかたとの比較として並べる為に2つ表示しています。

this.time += app.deltaTime;
// 経過秒数を取得してラベルで表示
this.label.text = "シーン開始からの経過秒数:" + (app.elapsedTime / 1000).toFixed(3)
    + "\nupdateされている間の経過秒数:" + (this.time / 1000).toFixed(3);

シーン生成、消去

また、ポーズシーンによる一時停止は、alkn203氏の[phina.js-Tips] Sceneをプッシュしてポーズ画面を作成するを参考に(ほぼパクリ)させていただきましたので、ここでひっそり感謝(と謝罪)を申し上げさせていただきますm(_ _)m

メインシーンの方で、一時停止ボタンが押された時に、ポーズシーンを生成し、
ポーズシーンでスタートボタンが押された時に、ポーズシーンを消すようにしています。

// 一時停止ボタン
Button({
    text: "stop",
    width: 100,
    height: 50
}).addChildTo(this)
    .setPosition(this.gridX.span(14), this.gridY.span(0.5))
    .onpush = () => {
        // ポーズシーン生成
        this.app.pushScene(PauseScene());
    };
/*****
 * 略
 *****/

/*
 * ポーズシーン
 */
phina.define("PauseScene", {
    superClass: "DisplayScene",
    init: function () {
        this.superInit();
        this.backgroundColor = "hsla(0, 0%, 0%, 0.8)";

        // スタートボタン
        Button({
            text: "start"
        }).addChildTo(this)
            .setPosition(this.gridX.center(), this.gridY.center())
            .onpush = () => {
                // 自身を削除する
                this.app.popScene();
            };
    }
});

ご意見・ご感想、また、ここ間違ってるよとか、もっといい方法あるよといったご指摘などございましたら、お手数ですがコメント欄や当twitterアカウントへお願いしますm(_ _)m

参考にさせていただいたサイト・ページ一覧

phina.js/baseapp.js at develop – phinajs/phina.js – GitHub
[phina.js-Tips-46]ゲームの経過秒数を取得する
[phina.js-Tips] Sceneをプッシュしてポーズ画面を作成する
GitHub – dataarts/dat.gui: dat.gui is a lightweight controller library for JavaScript.
dat.GUI入門

Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください