ジャンル雑多なゲーム・ゲーム制作関連の色々な情報を取り扱っているブログ。最近はBlenderについてが中心。
[phina.js] 探索・脱出ゲームの作り方

[phina.js] 探索・脱出ゲームの作り方

[phina.js] 探索・脱出ゲームの作り方

今回の記事は、”【ノベルゲーム】ガラス細工のクリスマス” で使った探索パートのソースコードを一部晒しつつ、phina.jsを使った探索・脱出ゲームの作り方について書いていこうと思います。※ただし、トリックなどのアイディアはこちらではご用意できませんので、ご自分でご用意ください。

後日追記:ノベルゲームの作り方編の記事も書きましたので良よしければ合わせてご覧ください。

準備

今回の記事では、ローカルサーバー上で動作させる事を前提に進めさせていただきます。いくつか背景用の画像をご用意いただき、assetsに読み込ませてください。もしなければ、Code and Materialで公開しているイラストをご利用ください。

ソースコードの雛型として置いておきます。

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

// アセット
const ASSETS = {
    // 画像
    image: {
        "img":"背景画像のパス",
    }
};

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

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

制作方法においての4つのポイント

  • テキスト表示をするLabelAreaオブジェクト
  • クリックポイント(setInteractiveをtrueにしたオブジェクト)
  • ゲームのフラグの状態を格納するグローバル変数
  • 部屋移動時の背景画像・クリックポイントの切り替え

テキスト表示

ということで、まずはLabelAreaオブジェクトを用意しますが、その前に、そのLabelAreaオブジェクトの枠(背景?)として、一つ、RectangleShapeオブジェクトをご用意下さい。そのRectangleShapeオブジェクトの子オブジェクトとして、LabelAreaオブジェクトをセット。これでひとまず、テキスト表示の方は準備完了です。

// テキストエリアの矩形
this.labelRect = RectangleShape({
    cornerRadius: 5,
    width: 630,
    height: 280,
    stroke: "white",
    fill: "#eee"
}).addChildTo(this)
    .setPosition(this.gridX.center(), this.gridY.center(5.5));
this.labelRect.alpha = 0.8;
this.labelRect.hide();

// テキスト表示用
this.labelArea = LabelArea({
    text: "",
    width: 600,
    height: 240,
    fontSize: FONT_SIZE
}).addChildTo(this.labelRect).setPosition(0, 0);

クリックポイント

まずクリックポイントを格納するクリックポイントグループを作成し、クリックポイントとなるオブジェクト(RectangleShapeやCircleShape)を設置します。そして、イベント(例えばonpointstart)に先ほど作ったテキスト表示用のLabelAreaのtextプロパティを呼び出し、何かしらの文章をセットします。位置確認した設置後はそのオブジェクトをhide()するか、alphaの値を0にするなどで隠してください。こうすれば、クリックポイントがクリックされた時に、テキストの文章が変わります。

// クリックポイントグループ
this.clickPointsGroup = DisplayElement().addChildTo(this);
/*
 * 省略
 */
// クリスマスツリークリックポイントの生成
let tree = RectangleShape({
    width: 40,
    height: 360
}).addChildTo(this.clickPointsGroup).setPosition(620, 650);
// 非表示
tree.hide();
// クリック有効化
tree.setInteractive(true);
// クリックされた時、テキスト表示
tree.onpointstart = () => {
    // labelAreaに表示したい文章をセット
    this.labelArea.text = "クリスマスツリーだ。";
    // 非表示状態だったlabelRectを表示
    this.labelRect.show();
};

上でやったような仕組みを、「どうせ隠しちゃうんだし…」と、個々で位置や大きさの指定だけしてクリックポイントを生成するメソッドと、クリックされた時のテキスト表示についての設定をするメソッドを、独自に作っちゃいました。

// セットするクリックポイントの生成-〇の場合-
createClickCircleObj: function (rad, x, y) {
    // オブジェクトの生成
    let circle = CircleShape({
        radius: rad
    }).addChildTo(this.clickPointsGroup).setPosition(x, y);
    // オブジェクト非表示
    circle.hide();
    // クリック有効化
    circle.setInteractive(true);

    return circle;
},
// セットするクリックポイントの生成-□の場合-
ceateClickRectObj: function (w, h, x, y) {
    // オブジェクトの生成
    let rect = RectangleShape({
        width: w,
        height: h
    }).addChildTo(this.clickPointsGroup).setPosition(x, y);
    //オブジェクト非表示
    rect.hide();
    // クリック有効化
    rect.setInteractive(true);

    return rect;
},
// クリックポイントをクリックした時のテキスト表示
setLabelRectText: function (text) {
    // labelAreaに表示したい文章をセット
    this.labelArea.text = text;
    // 非表示状態だったlabelRectを表示
    this.labelRect.show();
}

これを使って、先ほどのクリックポイントの記述を直したのが下のものになります。

// クリスマスツリークリックポイントの生成
let tree = this.createClickRectObj(40, 360, 620, 650);
tree.onpointstart = () => {
    // labelAreaに表示したい文章をセット
    this.setLabelRectText("クリスマスツリーだ。");
};

また、何かの特殊イベントやエンディングなどは、私の場合ほとんど、何かのオブジェクトのonpointstart内に条件分岐を記述し、管理が楽(面倒)だったので条件が合致した場合にpopScene()かreplaceScene()で、そのままシーン遷移するようにしてしまいました。

tree.onpointstart = () => {
    // クリスマスツリーイベントがまだ発生していなかった場合、イベント発生
    if (!TREE_FLAG) {
        TREE_FLAG = true;
        this.app.pushScene(TreeEventScene());
    } else {
        this.setLabelRectText("クリスマスツリーだ。");
    }
};

因みにですが、私の場合、まず全体絵を描いて、こういう方法を取ったのですが、それぞれのクリックポイントとなるアイテムの写真やら画像をそのままクリックポイントにする方法もあるかと思います。アイテム回収するような脱出ゲームを作ろうとしている場合は、そちらの方が効率的だと思います。

フラグ

ゲームを進めるために必須となるであろう、フラグです。私の場合は、あらかじめグローバル変数として真偽値を格納しておき、クリックポイントのonpointstart内で条件が合致した場合に呼び出してtrue/falseを反転させ、ゲームを進行させる仕様にしました。

// フラグ
let TREE_FLAG = false;

/*
 * 略
 */
tree.onpointstart = () => {
    // クリスマスツリーイベントがまだ発生していなかった場合、イベント発生
    if (!TREE_FLAG) {
        TREE_FLAG = true;
        this.app.pushScene(TreeEventScene());
    } else {
        // labelAreaに表示したい文章をセット
        this.labelArea.text = "クリスマスツリーだ。";
        // 非表示状態だったlabelRectを表示
        this.labelRect.show();
    }
};

部屋移動

部屋移動については、まず、グローバル変数としてBASYOを用意して、一番最初に”ribingu”を格納しておきます。そして、探索シーンで、this.basyoというプロパティを用意します。こちらも初期値は”ribingu”を格納しておきます。そして、探索シーンの隅っこに移動ボタンを設置し、移動ボタンが押された時にpopScene()して背景が半透明な一時的シーンを作るように設定しておきます。そして、その一時的なシーンで、間取りを表示し、そのそれぞれの部屋をこれまたクリック有効化して、どれかがクリックされたら、その一時的シーンを消すと同時に、そのクリックされた部屋に対応した場所の名前をグローバル変数BASYOに格納し、通常の探索シーンに戻ります。

// 場所
let BASYO = "ribingu";
/*
 * 略
 */
// 現在位置の場所
this.basyo = "ribingu";
this.heyaIdou("ribingu");

// 部屋移動ボタン
this.idoBtn = Button({
    text: "移動",
    fontSize: 25,
    fontColor: "black",
    fill: "white",
    width: 100,
    height: 50
}).addChildTo(this).setPosition(580, 900)
    .onpush = () => {
        this.app.pushScene(MyIdoScene());
    };
/*
 * 略
 */
/*************************
* 部屋移動画面
*/
phina.define("MyIdoScene", {
    superClass: "DisplayScene",
    init: function () {
        this.superInit();
        // 半透明な背景色
        this.backgroundColor = "hsla(0,100%,0%,0.6)";

        // リビング
        this.setBasyoRect(250, 300, 430, 230, "ribingu", "リビング");
        // 玄関
        this.setBasyoRect(150, 100, 170, 660, "genkan", "玄関");
        // 寝室
        this.setBasyoRect(250, 200, 430, 580, "sinsitu", "寝室");
    },
    // 更新
    update: function (app) {
        if (app.frame % 10 === 0) {
            // 現在地を示すの赤●の輪郭の明滅
            let c = CircleShape({
                fill: null,
                stroke: "red",
                strokeWidth: 10
            }).addChildTo(this.circle);
            c.update = () => {
                c.radius++;
                c.alpha -= 0.1;
                if (c.alpha <= 0) {
                    c.remove();
                }
            };
        }
    },
    // 場所の設置
    setBasyoRect: function (w, h, x, y, basyo, text) {
        // 場所を表す矩形
        let rect = RectangleShape({
            width: w,
            height: h,
            fill: "white",
            stroke: null
         }).addChildTo(this).setPosition(x, y);
         //半透明にする
         rect.alpha = 0.7;
         // 場所名を示すラベル
         Label({
             text: text,
             fill: "black",
             fontSize: 32
         }).addChildTo(rect).setPosition(0, 0);

         // クリック有効化
         rect.setInteractive(true);
         // クリックされた場合
         rect.onpointend = () => {
             // グローバル変数の値を、クリックされた場所に変更
             BASYO = basyo;
             // シーン遷移
             this.exit();
         };

         // 現在地を示す赤●
         if (basyo === BASYO) {
             this.circle = CircleShape({
                 stroke: null
             }).addChildTo(rect);
         }
     }
 });

その上で、もし、通常探索シーンのthis.basyoと、BASYOに格納されている値が違った場合、部屋移動の独自メソッドを呼び出し、画面に表示されている画像や、クリックポイントを切り替え、といった具合に仕上げました。背景画像用グループを作っているのは、phina.jsではオブジェクトの表示順を途中で変更できないのでその対策です。この事については、alkn203氏の[phina.js]オブジェクトの表示順について – Qiitaという記事がとても分かりやすいかと思いますので、詳しく知りたい方はご覧ください。あと、もしも、ルートやフラグによって背景画像を変えたい場合は、heyaIdou()メソッド内でbackImageNameに入れる名前を条件によって変更するか、シーンごと変更するという方法があるかと思います。

/**********************
 * メインシーン
 */
phina.define("MainScene", {
    superClass: "DisplayScene",
    init: function () {
        this.superInit();

        // 背景画像用グループ
        this.backImageGroup = DisplayElement().addChildTo(this);

        // クリックポイントグループ
        this.clickPointsGroup = DisplayElement().addChildTo(this);

        // フラグリセット
        TREE_FLAG = false;
        BAD_ROUTE = false;

        // 現在位置の場所
        this.basyo = "ribingu";
        this.heyaIdou("ribingu");

        // 部屋移動ボタン
        this.idoBtn = Button({
            text: "移動",
            fontSize: 25,
            fontColor: "black",
            fill: "white",
            width: 100,
            height: 50
        }).addChildTo(this).setPosition(580, 900)
            .onpush = () => {
                this.app.pushScene(MyIdoScene());
            };
        /*
         *略
         */
    },
    // 更新
    update: function (app) {
        // 部屋が移動した場合
        if (this.basyo !== BASYO) {
            this.basyo = BASYO;
            this.heyaIdou(this.basyo);
        }
        // クリックポイント以外がクリックされた場合、テキストを一度非表示にする
        if (app.pointer.getPointingStart()) {
            if (this.labelRect.visible) {
                this.labelArea.text = "";
                this.labelRect.hide();
            }
        }

        // 玄関に居た上で、BAD_ROUTEフラグが立っていたら、バッドエンドシーンへ遷移
        if (this.basyo === "genkan" && BAD_ROUTE) {
            this.backImageGroup.children.clear();
            app.replaceScene(BadEndScene());
        }
    },
    //部屋移動 *******************
    heyaIdou: function (basyo) {
        //背景画像の削除
        this.backImageGroup.children.clear();
        // 背景画像の名前格納用変数
        let backImageName;
        switch (basyo) {
            // リビング
            case "ribingu":
                backImageName = "ribingu";
                break;
            // 玄関
            case "genkan":
                backImageName = "genkan";
                break;
            // 寝室
            case "sinsitu":
                backImageName = "sinsitu";
                break;
        }
        // 背景画像を表示
        let idouBackImage = Sprite(backImageName).addChildTo(this.backImageGroup)
            .setPosition(this.gridX.center(), this.gridY.center());
        // 背景画像切り替えの際の違和感をごまかす為のアニメーション
        idouBackImage.tweener.clear().to({ alpha: 0 }, 1).to({ alpha: 1 }, 100).play();
        // クリックポイントのセット
        this.setClickPoints(basyo);

    },
    // クリックポイントセット **************
    setClickPoints(basyo) {
        // クリックポイントグループの削除
        this.clickPointsGroup.children.clear();

        switch (basyo) {
            // リビング/////////////////////////////
            case "ribingu":
                /*
                 *略
                 */
                break;

            // 玄関 /////////////////////////////////
            case "genkan":
                /*
                 *略
                 */
                break;

            // 寝室////////////////////////////////
            case "sinsitu":
                /*
                 *略
                 */
                break;
        }
    },
    /*
     *略
     */
});

この方法以外にも、左右や上下に矢印ボタンを設置して、それが押されたらそれぞれに対応した場所へ移動する、ってやり方でもいいと思います。

サンプル

ソースコード

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

    // 定数
    const W = 640;
    const H = 960;
    const FONT_SIZE = 28;

    // 場所
    let BASYO = "ribingu";

    // フラグ
    let TREE_FLAG = false;
    let BAD_ROUTE = false;

    // アセット
    const ASSETS = {
        image: {
            "ribingu": "リビングの画像のパス", // リビングの画像
            "genkan": "玄関の画像のパス", // 玄関の画像
            "sinsitu": "寝室の画像のパス" // 寝室の画像
        }
    };
 
    /********************************************
     * メインシーン
     */
    phina.define("MainScene", {
        superClass: "DisplayScene",
        init: function () {
            this.superInit();

            // 背景画像用グループ
            this.backImageGroup = DisplayElement().addChildTo(this);

            // クリックポイントグループ
            this.clickPointsGroup = DisplayElement().addChildTo(this);

            // フラグリセット
            TREE_FLAG = false;
            BAD_ROUTE = false;

            // 現在位置の場所
            this.basyo = "ribingu";
            this.heyaIdou("ribingu");

            // 部屋移動ボタン
            this.idoBtn = Button({
                text: "移動",
                fontSize: 25,
                fontColor: "black",
                fill: "white",
                width: 100,
                height: 50
            }).addChildTo(this).setPosition(580, 900)
                .onpush = () => {
                    this.app.pushScene(MyIdoScene());
                };

            // テキストエリアの矩形
            this.labelRect = RectangleShape({
                cornerRadius: 5,
                width: 630,
                height: 280,
                stroke: "white",
                fill: "#eee"
            }).addChildTo(this)
                .setPosition(this.gridX.center(), this.gridY.center(5.5));
            this.labelRect.alpha = 0.8;
            this.labelRect.hide();

            // テキスト表示用
            this.labelArea = LabelArea({
                text: "",
                width: 600,
                height: 240,
                fontSize: FONT_SIZE
            }).addChildTo(this.labelRect).setPosition(0, 0);
        },
        // 更新
        update: function (app) {
            // 部屋が移動した場合
            if (this.basyo !== BASYO) {
                this.basyo = BASYO;
                this.heyaIdou(this.basyo);
            }
            // クリックポイント以外がクリックされた場合、テキストを一度非表示にする
            if (app.pointer.getPointingStart()) {
                if (this.labelRect.visible) {
                    this.labelArea.text = "";
                    this.labelRect.hide();
                }
            }

            // 玄関に居た上で、BAD_ROUTEフラグが立っていたら、バッドエンドシーンへ遷移
            if (this.basyo === "genkan" && BAD_ROUTE) {
                this.backImageGroup.children.clear();
                app.replaceScene(BadEndScene());
            }
        },
        //部屋移動
        heyaIdou: function (basyo) {
            //背景画像の削除
            this.backImageGroup.children.clear();
            // 背景画像の名前格納用変数
            let backImageName;
            switch (basyo) {
                // リビング
                case "ribingu":
                    backImageName = "ribingu";
                    break;
                // 玄関
                case "genkan":
                    backImageName = "genkan";
                    break;
                // 寝室
                case "sinsitu":
                    backImageName = "sinsitu";
                    break;
            }
            // 背景画像を表示
            let idouBackImage = Sprite(backImageName).addChildTo(this.backImageGroup)
                .setPosition(this.gridX.center(), this.gridY.center());
            // 背景画像切り替えの際の違和感をごまかす為のアニメーション
            idouBackImage.tweener.clear().to({ alpha: 0 }, 1).to({ alpha: 1 }, 100).play();
            // クリックポイントのセット
            this.setClickPoints(basyo);

        },
        // クリックポイントセット **************
        setClickPoints(basyo) {
            // クリックポイントグループの削除
            this.clickPointsGroup.children.clear();

            switch (basyo) {
                // リビング/////////////////////////////
                case "ribingu":
                    // 窓
                    let mado = this.createClickRectObj(400, 300, 440, 490);
                    mado.onpointstart = () => {
                        this.setLabelRectText("窓だ。");
                    };

                    // 本棚
                    let hondana = this.createClickRectObj(300, 460, 80, 610);
                    hondana.onpointstart = () => {
                        this.setLabelRectText("本棚だ。");
                    };

                    // 椅子
                    let isu = this.createClickRectObj(160, 400, 240, 770);
                    let isu2 = this.createClickRectObj(160, 320, 490, 715);
                    isu.onpointstart = isu2.onpointstart = () => {
                        this.setLabelRectText("椅子だ。");
                    };

                    //テーブル
                    let table = this.createClickRectObj(260, 160, 370, 670);
                    table.onpointstart = () => {
                        this.setLabelRectText("テーブルだ。");
                    };

                    // 時計
                    let clock = this.createClickCircleObj(50, 350, 245);
                    clock.onpointstart = () => {
                        this.setLabelRectText("時計だ。");
                    };

                    // クリスマスツリー
                    let tree = this.createClickRectObj(40, 360, 620, 650);
                    tree.onpointstart = () => {
                        // クリスマスツリーイベントがまだ発生していなかった場合、イベント発生
                        if (!TREE_FLAG) {
                            TREE_FLAG = true;
                            this.app.pushScene(TreeEventScene());
                        } else {
                            this.setLabelRectText("クリスマスツリーだ。");
                        }
                    };

                    //照明
                    let syoumei = this.createClickRectObj(230, 80, 350, 20);
                    syoumei.onpointstart = () => {
                        this.setLabelRectText("照明だ。");
                    };
                    break;

                // 玄関 //////////////////////////////////
                case "genkan":
                    // 靴箱
                    let kutubako = this.createClickRectObj(120, 360, 60, 580);
                    kutubako.onpointstart = () => {
                        this.setLabelRectText("靴箱だ。");
                    };

                    // ドア
                    let door = this.createClickRectObj(380, 680, 320, 330);
                    door.onpointstart = () => {
                        // 出る・出ない選択肢のシーン
                        this.app.pushScene(DoorChoiceScene());
                    };

                    // 覗き穴
                    let nozokiana = this.createClickCircleObj(10, 315, 60);
                    nozokiana.onpointstart = () => {
                        this.setLabelRectText("覗き穴だ。");
                    };
                    break;

                // 寝室///////////////////////////
                case "sinsitu":
                    // 襖
                    let husuma = this.createClickRectObj(420, 600, 430, 200);
                    husuma.onpointstart = () => {
                        if (!TREE_FLAG) {//通常
                            this.setLabelRectText("襖だ。");
                        } else {//ツリーイベントを起こしていた場合、ノーマルエンドへ
                            this.app.replaceScene(NormalEndScene());
                        }
                    };

                    // 畳
                    let tatami = this.createClickRectObj(W, 460, 320, 740);
                    tatami.onpointstart = (e) => {
                        // 移動ボタンの位置の場合
                        if (e.pointer.x >= 530 && e.pointer.y >= 875) {
                            return;
                        }
                        this.setLabelRectText("畳だ。");
                    };
                    break;
            }
        },
        // セットするクリックポイントの生成-〇の場合-
        createClickCircleObj: function (rad, x, y) {
            // オブジェクトの生成
            let circle = CircleShape({
                radius: rad
            }).addChildTo(this.clickPointsGroup).setPosition(x, y);
            // オブジェクト非表示
            circle.hide();
            // クリック有効化
            circle.setInteractive(true);

            return circle;
        },
        // セットするクリックポイントの生成-□の場合-
        createClickRectObj: function (w, h, x, y) {
            // オブジェクトの生成
            let rect = RectangleShape({
                width: w,
                height: h
            }).addChildTo(this.clickPointsGroup).setPosition(x, y);
            //オブジェクト非表示
            rect.hide();
            // クリック有効化
            rect.setInteractive(true);

            return rect;
        },
        // クリックポイントをクリックした時のテキスト表示
        setLabelRectText: function (text) {
            // labelAreaに表示したい文章をセット
            this.labelArea.text = text;
            // 非表示状態だったlabelRectを表示
            this.labelRect.show();
        }
    });

    /****************************
    * 部屋移動画面
    */
    phina.define("MyIdoScene", {
        superClass: "DisplayScene",
        init: function () {
            this.superInit();
            // 半透明な背景色
            this.backgroundColor = "hsla(0,100%,0%,0.6)";

            // リビング
            this.setBasyoRect(250, 300, 430, 230, "ribingu", "リビング");
            // 玄関
            this.setBasyoRect(150, 100, 170, 660, "genkan", "玄関");
            // 寝室
            this.setBasyoRect(250, 200, 430, 580, "sinsitu", "寝室");
        },
        // 更新
        update: function (app) {
            if (app.frame % 10 === 0) {
                // 現在地を示すの赤●の輪郭の明滅
                let c = CircleShape({
                    fill: null,
                    stroke: "red",
                    strokeWidth: 10
                }).addChildTo(this.circle);
                c.update = () => {
                    c.radius++;
                    c.alpha -= 0.1;
                    if (c.alpha <= 0) {
                        c.remove();
                    }
                };
            }
        },
        // 場所の設置
        setBasyoRect: function (w, h, x, y, basyo, text) {
            // 場所を表す矩形
            let rect = RectangleShape({
                width: w,
                height: h,
                fill: "white",
                stroke: null
            }).addChildTo(this).setPosition(x, y);
            //半透明にする
            rect.alpha = 0.7;
            // 場所名を示すラベル
            Label({
                text: text,
                fill: "black",
                fontSize: 32
            }).addChildTo(rect).setPosition(0, 0);

            // クリック有効化
            rect.setInteractive(true);
            // クリックされた場合
            rect.onpointend = () => {
                // グローバル変数の値を、クリックされた場所に変更
                BASYO = basyo;
                // シーン遷移
                this.exit();
            };

            // 現在地を示す赤●
            if (basyo === BASYO) {
                this.circle = CircleShape({
                    stroke: null
                }).addChildTo(rect);
            }
        }
    });

    /**********************
     * ドアから出る出ない選択肢画面
     */
    phina.define("DoorChoiceScene", {
        superClass: "DisplayScene",
        init: function () {
            this.superInit();
            // 半透明な背景色
            this.backgroundColor = "hsla(0,100%,0%,0.1)";

            // 出るボタン
            Button({
                text: "出る",
                fontSize: 40,
                fontColor: "black",
                fill: "lightgray",
                width: 140,
                height: 60
            }).addChildTo(this).setPosition(this.gridX.center(), this.gridY.center(-1))
                .onpush = () => {
                    // バッドエンドフラグ
                    BAD_ROUTE = true;
                    this.exit();
                };

            // 出ないボタン
            Button({
                text: "出ない",
                fontSize: 40,
                fontColor: "black",
                fill: "lightgray",
                width: 140,
                height: 60
            }).addChildTo(this).setPosition(this.gridX.center(), this.gridY.center(1))
                .onpush = () => {
                    this.exit();
                };

            // テキストエリアの矩形
            this.labelRect = RectangleShape({
                cornerRadius: 5,
                width: 630,
                height: 280,
                stroke: "white",
                fill: "#eee"
            }).addChildTo(this)
                .setPosition(this.gridX.center(), this.gridY.center(5.5));
            this.labelRect.alpha = 0.8;
            this.labelRect.hide();

            // テキスト表示用
            this.labelArea = LabelArea({
                text: "",
                width: 600,
                height: 240,
                fontSize: FONT_SIZE
            }).addChildTo(this.labelRect).setPosition(0, 0);

            this.labelRect.show();
            this.labelArea.text = "ドアだ。ここから外へ出られる。";
        }
    });

    /****************************************
     * ツリーイベント
     */
    phina.define("TreeEventScene", {
        superClass: "DisplayScene",
        init: function () {
            this.superInit();
            // 背景色
            this.backgroundColor = "black";

            Label({
                text: "イベントです。\nクリックで戻ります。",
                fontSize: FONT_SIZE,
                fill: "white",
                stroke: null
            }).addChildTo(this).setPosition(this.gridX.center(), this.gridY.center());

        },
        // 更新
        update: function (app) {
            // クリックされた場合戻る
            if (app.pointer.getPointingStart()) {
                this.exit();
            }
        }
    });
    /***************************
    * Normalエンディングシーン
    */
    phina.define("NormalEndScene", {
        superClass: "DisplayScene",
        init: function () {
            this.superInit();
            // 背景色
            this.backgroundColor = "black";

            Label({
                text: "ノーマルエンドです。\nクリックで戻ります。",
                fontSize: FONT_SIZE,
                fill: "white",
                stroke: null
            }).addChildTo(this).setPosition(this.gridX.center(), this.gridY.center());
        },
        // 更新
        update: function (app) {
            if (app.pointer.getPointingStart()) {
                BASYO = "ribingu";
                app.replaceScene(MainScene());
            }
        }
    });

    /************************************
     * バッドエンドシーン
     */
    phina.define("BadEndScene", {
        superClass: "DisplayScene",
        init: function () {
            this.superInit();
            // 背景色
            this.backgroundColor = "black";

            Label({
                text: "Game Over\nクリックで戻ります。",
                fontSize: FONT_SIZE,
                fill: "white",
                stroke: null
            }).addChildTo(this).setPosition(this.gridX.center(), this.gridY.center());
        },
        // 更新
        update: function (app) {
            if (app.pointer.getPointingStart()) {
                BASYO = "ribingu";
                app.replaceScene(MainScene());
            }
        }
    });

    /***********************
     * メイン処理
     */
    phina.main(function () {
        const app = GameApp({
            startLabel: "main",
            // アセット読み込み
            assets: ASSETS
        });

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

あとがき(唐突な自分語り)

今回解説した方法は作り方の一例です。自分語りになってしまいますが、『ガラス細工のクリスマス 』は、「なんか、探索+ノベルゲーム作りたい!」という私の唐突な思いつきから誕生しました。日頃phina.jsでゲーム制作をしていたため、phina.jsで制作してみようと思い、どなたかそういった制作方法の記事を書いていないかな?と思って探して見たのですが、なかなか見つからず、「じゃあ、やってみるか!」と試験的に作ってみたところもあります。「ゲームとして、一応形にはなったかな?」と思いましたので、他にもphina.jsでこういった方向の事を試そうとされている方、偶然にもこの記事を目にして頂けた方に、「こんな方法もあるんだ」という参考として示せればと思い、この記事を書いてみました。これよりももっと良い方法があるかと思いますので、この方法に固執せず、読んでくださった方の今後のゲーム制作(ゲーム以外の制作も含め)の、一助となれば幸いです。ここまで読んでいただきありがとうございました!宣伝になってしまいますが、ノベルゲームの場面転換などの仕方なども、後日別の記事として上げる予定です。ご意見・ご感想、また、ここ間違ってるよとか、もっといい方法あるよといったご指摘などございましたら、お手数ですがコメント欄やtwitterアカウントほろほろり(@_horo_horori)へお願いしますm(_ _)m

後日追記:ノベルゲームの作り方編の記事も書きましたので良よしければ合わせてご覧ください。

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

Pocket