[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();
});

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

テキスト表示&文字送りのシステムについては、本当にほとんど変えずに、simiraaaa氏のLabelAreaサンプルを使わせて(パクらせて)いただいたので、ここで深く感謝(と謝罪)を述べさせていただきます。本当にありがとうございましたm(_ _)m

表示テキスト

(LabelAreaサンプルの形式がそうだったのもあり)私の場合、表示させたい文章を、以下のように区切って、 配列として最初にまとめて作っておく方式をとりました。最後、””を作っているのは、次のシーンへ遷移させる為に余分に一つに作っておいているのです。

※ノベルゲーム『櫻の樹の下には』、及び、ここで使っているテキストのデータは、青空文庫様からお借りしました。
https://www.aozora.gr.jp/cards/000074/card427.html

const MAINNTEXTS = [
    "桜の樹の下には屍体《したい》が\n埋まっている!",
    "これは信じていいことなんだよ。",
    "何故《なぜ》って、\n桜の花があんなにも見事に咲くなんて\n信じられないことじゃないか。",
    "俺はあの美しさが信じられないので、\nこの二三日不安だった。",
    "しかしいま、やっとわかるときが来た。",
    "桜の樹の下には屍体が埋まっている。",
    "これは信じていいことだ。",
    "どうして俺が毎晩家へ帰って来る道で、\n俺の部屋の数ある道具のうちの、\n選《よ》りに選って\nちっぽけな薄っぺらいもの、\n安全剃刀の刃なんぞが、千里眼のように\n思い浮かんで来るのか",
    "――おまえはそれがわからないと言ったが\n――そして俺にもやはりそれがわからないのだが\n――それもこれもやっぱり同じようなことに\nちがいない。",
    "いったいどんな樹の花でも、\nいわゆる真っ盛りという状態に達すると、\nあたりの空気のなかへ\n一種神秘な雰囲気を撒き散らすものだ。",
    "それは、\nよく廻った独楽《こま》が完全な静止に\n澄むように、",
    "また、\n音楽の上手な演奏が\nきまってなにかの幻覚を伴うように、",
    "灼熱《しゃくねつ》した生殖の幻覚させる\n後光のようなものだ。",
    "それは人の心を撲《う》たずにはおかない、\n不思議な、生き生きとした、美しさだ。",
    "しかし、\n昨日、一昨日、\n俺の心をひどく陰気にしたものもそれなのだ。",
    "俺にはその美しさがなにか\n信じられないもののような気がした。",
    "俺は反対に\n不安になり、\n憂鬱《ゆううつ》になり、\n空虚な気持になった。",
    "しかし、俺はいまやっとわかった。",
    "おまえ、\nこの爛漫《らんまん》と咲き乱れている\n桜の樹の下へ、一つ一つ屍体が埋まっていると\n想像してみるがいい。\n何が俺をそんなに不安にしていたかが\nおまえには納得がいくだろう。",
    "馬のような屍体、犬猫のような屍体、\nそして人間のような屍体、屍体はみな\n腐爛《ふらん》して蛆《うじ》が湧き、\n堪《たま》らなく臭い。",
    "それでいて水晶のような液を\nたらたらとたらしている。",
    "桜の根は貪婪《どんらん》な蛸《たこ》の\nように、それを抱きかかえ、\nいそぎんちゃくの食糸のような\n毛根を聚《あつ》めて、その液体を\n吸っている。",
    "何があんな花弁を作り、\n何があんな蕊《しべ》を作っているのか、",
    "俺は毛根の吸いあげる水晶のような液が、\n静かな行列を作って、維管束のなかを\n夢のようにあがってゆくのが見えるようだ。",
    "――おまえは何をそう苦しそうな顔を\nしているのだ。美しい透視術じゃないか。",
    "俺はいまようやく瞳《ひとみ》を据えて\n桜の花が見られるようになったのだ。\n昨日、一昨日、\n俺を不安がらせた神秘から自由になったのだ。",
    "二三日前、\n俺は、ここの溪《たに》へ下りて、\n石の上を伝い歩きしていた。",
    "水のしぶきのなかからは、\nあちらからもこちらからも、\n薄羽かげろうがアフロディットのように\n生まれて来て、溪の空をめがけて\n舞い上がってゆくのが見えた。",
    "おまえも知っているとおり、\n彼らはそこで美しい結婚をするのだ。",
    "しばらく歩いていると、\n俺は変なものに出喰《でく》わした。",
    "それは溪の水が乾いた磧《かわら》へ、\n小さい水溜を残している、\nその水のなかだった。",
    "思いがけない石油を流したような光彩が、\n一面に浮いているのだ。",
    "おまえはそれを何だったと思う。",
    "それは何万匹とも数の知れない、\n薄羽かげろうの屍体だったのだ。",
    "隙間なく水の面を被っている、\n彼らのかさなりあった翅《はね》が、\n光にちぢれて\n油のような光彩を流しているのだ。",
    "そこが、\n産卵を終わった彼らの墓場だったのだ。",
    "俺はそれを見たとき、\n胸が衝《つ》かれるような気がした。\n墓場を発《あば》いて\n屍体を嗜《この》む変質者のような\n残忍なよろこびを俺は味わった。",
    "この溪間では\nなにも俺をよろこばすものはない。",
    "鶯《うぐいす》や四十雀《しじゅうから》も、\n白い日光をさ青に煙らせている木の若芽も、\nただそれだけでは、\nもうろうとした心象に過ぎない。",
    "俺には惨劇が必要なんだ。\nその平衡があって、\nはじめて俺の心象は明確になって来る。",
    "俺の心は悪鬼のように憂鬱に渇いている。\n俺の心に憂鬱が完成するときにばかり、\n俺の心は和《なご》んでくる。",
    "――おまえは腋《わき》の下を\n拭《ふ》いているね。\n冷汗が出るのか。\nそれは俺も同じことだ。",
    "何もそれを不愉快がることはない。\nべたべたとまるで精液のようだと\n思ってごらん。",
    "それで俺達の憂鬱は完成するのだ。",
    "ああ、\n桜の樹の下には屍体が埋まっている!",
    "いったいどこから浮かんで来た空想か\nさっぱり見当のつかない屍体が、\nいまはまるで桜の樹と一つになって、\nどんなに頭を振っても離れてゆこうとは\nしない。",
    "今こそ俺は、\nあの桜の樹の下で\n酒宴をひらいている村人たちと同じ権利で、\n花見の酒が呑《の》めそうな気がする。",
    ""
];

テキスト表示&文字送り

まずは先述の通り、主軸となる文字送りシステムを、simiraaaa氏作成のサンプルから、ほとんど変えずに持ってきます。私の場合は、テキストの背景となるRectangleShapeにまとめて、一つのオブジェクトとしてしまいました。

/***************************************
 * テキスト表示&文字送り
 */
phina.define("LabelRect", {
    superClass: "RectangleShape",
    init: function () {
        this.superInit({
            cornerRadius: 5,
            width: 630,
            height: 280,
            stroke: "white",
            fill: "#eee"
        });
        this.alpha = 0.8;

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

        this.texts = [];
        this.textIndex = 0;
        this.charIndex = 0;

        // 次のテキスト合図の▽
        this.nextTriangle = TriangleShape({
            fill: "black",
            stroke: false,
            radius: FONT_SIZE / 2
        }).addChildTo(this)
            .setPosition(280, 100);
        this.nextTriangle.rotation = 180;
        this.nextTriangle.hide();

    },
    showAllText: function () {
        let text = this.texts[this.textIndex];
            this.labelArea.text = text;
            this.textAll = true;
            this.charIndex = text.length;
        },

    clearText: function () {
        this.labelArea.text = "";
    },

    nextText: function () {
        this.clearText();
        ++this.textIndex
        this.charIndex = 0;
        this.addChar();
    },
    addChar: function () {
        this.labelArea.text += this.getChar();
    },
    getChar: function () {
       let text = this.texts[this.textIndex];
       if (text.length <= this.charIndex) {
           this.textAll = true;
           return "";
       } else {
           this.textAll = false;
           return text[this.charIndex++];
       }
    }
});

これをメインシーンで生成し、 このRabelRectクラスのthis.textsプロパティに、前項のMAINNTEXTSを格納して使うわけです。

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

        // 背景色
        this.backgroundColor = "black";

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

        // テキストエリアの矩形
        this.labelRect = LabelRect().addChildTo(this)
            .setPosition(this.gridX.center(), this.gridY.center(5.5));

        this.labelRect.texts = MAINNTEXTS;
        this.labelRect.textIndex = 0;
        this.labelRect.charIndex = 0;

        this.setPhase();
    },
/*
 * 省略 
 */

そして、メインシーンのupdateのところで、テキストの文字送り、全部のテキストを表示、次のテキストへの切り替えを管理します。

/*
*省略
*/
// 更新
update: function (app) {
    //クリックかEnterキーの入力があった場合
    if (app.pointer.getPointingStart() || app.keyboard.getKeyDown("enter")) {
        if (this.labelRect.textAll) {//テキスト全部表示済み
            this.labelRect.nextText();
            // 次の背景に切替
            this.setPhase();
        } else {
            this.labelRect.showAllText();
        }
    } else {
        this.labelRect.addChar();
    }

    if (this.labelRect.textAll) {
        this.labelRect.nextTriangle.show();
    } else {
        this.labelRect.nextTriangle.hide();
    }
},
/*
*省略
*/

背景画像・音楽素材・場面を管理

前項のinit内の最後のところやupdate内にある、this.setPhase()という、メインシーンに紐付けされているメソッドを使って、背景画像・音楽素材・場面を管理します。RabelRectオブジェクトのtextIndexプロパティ(何番目の文章かのカウント)の数値で、背景や音の処理を進行させていく方式をとりました。
因みに、この時、背景画像をほとんど640×960の大きさに統一して、引数にアセット名を入れたら、それだけで画面中央に表示されるようにするメソッド、setBackImg()を作って管理しました。

前項のupdateの中で、このsetPhase()メソッドを呼び出すようにしています。具体的に説明すると、その時の分の全部の文章が読み込まれた後に、クリックかEnterキーが押された場合、文章が進みます。その毎に、一緒に画像・音楽の切り替えを行っています。

画像の操作についてや、tweenerクラスによるアニメーション設定は、alkn203氏の記事に解説を丸投げさせていただきます。

ちなみに、サウンドの操作については、過去に書いた記事がありますので、わからなければご参考にどうぞ。

/*
*省略
*/
setPhase: function () {
    switch (this.labelRect.textIndex) {
        case 0:
            //メインBGM再生
            SoundManager.playMusic("piano23_bgm");
            //桜木・桜の絨毯画像フェードイン
            this.cherryCarpet2 = this.setBackImg("cherry_carpet2");
            this.cherryCarpet2.alpha = 0;
            this.cherryCarpet2.tweener.clear().to({ alpha: 1 }, 200).play();
            //抽象的な歪みの画像フェードイン
            this.dandan = this.setBackImg("dandan");
            this.dandan.alpha = 0;
            this.dandan.tweener.clear().to({ alpha: 0.3 }, 200).play();
            break;
        case 7:
            //桜木・桜の絨毯画像フェードアウト
            this.cherryCarpet2.tweener.clear().to({ alpha: 0 }, 1000).play();
            //抽象的な歪みの画像フェードアウト
            this.dandan.tweener.clear().to({ alpha: 0 }, 1000).play();
            // カミソリ画像フェードイン
            this.razorBlade = this.setBackImg("razor_blade");
            this.razorBlade.alpha = 0;
            this.razorBlade.tweener.clear().to({ alpha: 1 }, 1000).play();
            break;
        case 9:
            //カミソリ画像フェードアウト
            this.razorBlade.tweener.clear().to({ alpha: 0 }, 1000).play();

            // 桜画像フェードイン
            this.cherry = this.setBackImg("cherry_blossoms");
            this.cherry.alpha = 0;
            this.cherry.tweener.clear().to({ alpha: 1 }, 1000).play();
            break;
        case 10:
            //桜画像フェードアウト
            this.cherry.tweener.clear().to({ alpha: 0 }, 1000).play();

            //桜木・桜の絨毯画像削除
            this.cherryCarpet2.remove();
            //抽象的な歪みの画像削除
            this.dandan.remove();
            //カラフルライン生成・フェードイン
            this.colorfulLines = ColorfulLines().addChildTo(this.backImageGroup)
                .setPosition(this.gridX.center(), this.gridY.center());
            this.colorfulLines.alpha = 0;
            this.colorfulLines.tweener.clear().to({ alpha: 1 }, 1000).play();
            //エフェクト01SE再生
            SoundManager.play("effect01_se");
            break;

        case 14:
            //カラフルラインフェードアウト
            this.colorfulLines.tweener.clear().to({ alpha: 0 }, 1000).play();

            //抽象的な歪みの画像フェードイン
            this.dandan = this.setBackImg("dandan");
            this.dandan.alpha = 0;
            this.dandan.tweener.clear().to({ alpha: 1 }, 1000).play();
            break;

        case 15:
            //カミソリ画像削除
            this.razorBlade.remove();
            //カラフルライン削除
            this.colorfulLines.remove();
            //桜画像フェードイン
            this.cherry.tweener.clear().to({ alpha: 1 }, 1000).play();
            //抽象的な歪みの画像半透明化
            this.dandan.tweener.clear().to({ alpha: 0.4 }, 1000).play();

            break;
        case 18:
            //桜画像フェードアウト
            this.cherry.tweener.clear().to({ alpha: 0 }, 1000).play();
            //抽象的な歪みの画像不透明化
            this.dandan.tweener.clear().to({ alpha: 1 }, 1000).play();
            //桜の絨毯画像フェードイン
            this.cherryCarpet = this.setBackImg("cherry_carpet");
            this.cherryCarpet.alpha = 0;
            this.cherryCarpet.tweener.clear().to({ alpha: 0.6 }, 1000).play();
            break;
        case 21:
            //マンデルブロ集合体フェードイン
            this.mandelbrot = this.setBackImg("mandelbrot");
            this.mandelbrot.setPosition(-50, 500);
            this.mandelbrot.alpha = 0;
            this.mandelbrot.tweener.clear().to({ alpha: 0.5 }, 1000)
                .to({ x: 0, y: 700, scaleX: 1.6, scaleY: 1.6 }, 4000).play();
            break;
        case 22:
            //マンデルブロ集合体フェードアウト
            this.mandelbrot.tweener.to({ alpha: 0 }, 1000).play();
            //スパイラルライン
            this.spiral = SpiralLine().addChildTo(this.backImageGroup).setPosition(this.gridX.center(), this.gridY.center());
            this.spiral.alpha = 0;
            this.spiral.tweener.clear().to({ alpha: 0.6 }, 4000).play();
            break;
        case 24:
            // 抽象的な歪画像フェードアウト
            this.dandan.tweener.clear().to({ alpha: 0 }, 3000).play();
            //桜の絨毯画像フェードアウト
            this.cherryCarpet.tweener.clear().to({ alpha: 0 }, 3000).play();
            //スパイラルラインフェードアウト
            this.spiral.tweener.clear().to({ alpha: 0 }, 3000).play();
            break;
        case 26:
            //メインBGMフェードアウト
            SoundManager.stopMusic(1000);
            //雫画像
            this.drop = this.setBackImg("drop");
            this.drop.alpha = 0;
            this.drop.tweener.clear().to({ alpha: 1 }, 1000).play();

            //川BGMフェードイン
            this.river = SoundManager.playMusic("river_bgm", 700);
            break;
        case 27:
            //マンデルブロ集合体・スパイラルライン削除
            this.mandelbrot.remove();
            this.spiral.remove();
            //水滴SE再生
            SoundManager.play("drop_se");
            break;
        case 28:
            //雫画像フェードアウト
            this.drop.tweener.to({ alpha: 0 }, 2400).play();
            break;
        case 31:
            //川BGMフェードアウト
            SoundManager.stopMusic(2000);
            //油画像
            this.oil = this.setBackImg("oil");
            this.oil.blendMode = "lighter";
            this.oil.alpha = 0;
            this.oil.tweener.clear().to({ alpha: 1 }, 6000).play();
            break;
        case 36:
            //BGM再生していたら、ストップ
            SoundManager.currentMusic = null;
            //心臓音SE再生
            SoundManager.play("heart_beat05_se");
            //パーリンノイズ画像表示
            this.perlin = this.setBackImg("perlin").addChildTo(this);
            this.perlin.alpha = 0.3;
            this.perlin.blendMode = "lighter";
            //抽象的な歪みの画像
            this.dandan = this.setBackImg("dandan");
            this.dandan.blendMode = "lighter";
            this.dandan.alpha = 0;
            this.dandan.tweener.clear().to({ alpha: 0.5, scaleX: 2, scaleY: 2 }, 600)
                .to({ alpha: 0, scaleX: 1, scaleY: 1 }, 1200).play();
            break;
        case 37:
            //エンディングBGMフェードイン
            SoundManager.playMusic("sinfonia01_bgm", 10000);
            break;
        case 41:
            this.oil.tweener.clear().to({ alpha: 0 }, 6000).play();
            break;
        case 44:
            //桜画像フェードイン
            this.cherry.tweener.clear().to({ alpha: 1 }, 2500).play();
            this.oil.tweener.clear().to({ alpha: 1 }, 1000)
                .to({ alpha: 0 }, 1500).play();
            this.dandan.tweener.clear().to({ alpha: 1 }, 1000)
                .to({ alpha: 0 }, 1500).play();
            break;
        case 47:
            this.exit();
            break;
    }
},
// 背景画像セット
setBackImg: function (imgName) {
    let img = Sprite(imgName).addChildTo(this.backImageGroup)
        .setPosition(this.gridX.center(), this.gridY.center());
    return img;
}
/*
*省略
*/

メインシーンのソースコード全文

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

        // 背景色
        this.backgroundColor = "black";

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

        // テキストエリアの矩形
        this.labelRect = LabelRect().addChildTo(this)
            .setPosition(this.gridX.center(), this.gridY.center(5.5));

        this.labelRect.texts = MAINNTEXTS;
        this.labelRect.textIndex = 0;
        this.labelRect.charIndex = 0;

        this.setPhase();
    },
    update: function (app) {
        //クリックかEnterキーの入力があった場合
        if (app.pointer.getPointingStart() || app.keyboard.getKeyDown("enter")) {
            if (this.labelRect.textAll) {//テキスト全部表示済み
                this.labelRect.nextText();
                // 次の背景に切替
                this.setPhase();
            } else {
                this.labelRect.showAllText();
            }
        } else {
            this.labelRect.addChar();
        }

        if (this.labelRect.textAll) {
            this.labelRect.nextTriangle.show();
        } else {
            this.labelRect.nextTriangle.hide();
        }
    },

    // 段階(場面)の管理 サウンドや画像の切り替え
    setPhase: function () {
        switch (this.labelRect.textIndex) {
            case 0:
                //メインBGM再生
                SoundManager.playMusic("piano23_bgm");
                //桜木・桜の絨毯画像フェードイン
                this.cherryCarpet2 = this.setBackImg("cherry_carpet2");
                this.cherryCarpet2.alpha = 0;
                this.cherryCarpet2.tweener.clear().to({ alpha: 1 }, 200).play();
                //抽象的な歪みの画像フェードイン
                this.dandan = this.setBackImg("dandan");
                this.dandan.alpha = 0;
                this.dandan.tweener.clear().to({ alpha: 0.3 }, 200).play();
                break;
            case 7:
                //桜木・桜の絨毯画像フェードアウト
                this.cherryCarpet2.tweener.clear().to({ alpha: 0 }, 1000).play();
                //抽象的な歪みの画像フェードアウト
                this.dandan.tweener.clear().to({ alpha: 0 }, 1000).play();
                // カミソリ画像フェードイン
                this.razorBlade = this.setBackImg("razor_blade");
                this.razorBlade.alpha = 0;
                this.razorBlade.tweener.clear().to({ alpha: 1 }, 1000).play();
                break;
            case 9:
                //カミソリ画像フェードアウト
                this.razorBlade.tweener.clear().to({ alpha: 0 }, 1000).play();

                // 桜画像フェードイン
                this.cherry = this.setBackImg("cherry_blossoms");
                this.cherry.alpha = 0;
                this.cherry.tweener.clear().to({ alpha: 1 }, 1000).play();
                break;
            case 10:
                //桜画像フェードアウト
                this.cherry.tweener.clear().to({ alpha: 0 }, 1000).play();

                //桜木・桜の絨毯画像削除
                this.cherryCarpet2.remove();
                //抽象的な歪みの画像削除
                this.dandan.remove();
                //カラフルライン生成・フェードイン
                this.colorfulLines = ColorfulLines().addChildTo(this.backImageGroup)
                    .setPosition(this.gridX.center(), this.gridY.center());
                this.colorfulLines.alpha = 0;
                this.colorfulLines.tweener.clear().to({ alpha: 1 }, 1000).play();
                //エフェクト01SE再生
                SoundManager.play("effect01_se");
                break;

            case 14:
                //カラフルラインフェードアウト
                this.colorfulLines.tweener.clear().to({ alpha: 0 }, 1000).play();

                //抽象的な歪みの画像フェードイン
                this.dandan = this.setBackImg("dandan");
                this.dandan.alpha = 0;
                this.dandan.tweener.clear().to({ alpha: 1 }, 1000).play();
                break;

            case 15:
                //カミソリ画像削除
                this.razorBlade.remove();
                //カラフルライン削除
                this.colorfulLines.remove();
                //桜画像フェードイン
                this.cherry.tweener.clear().to({ alpha: 1 }, 1000).play();
                //抽象的な歪みの画像半透明化
                this.dandan.tweener.clear().to({ alpha: 0.4 }, 1000).play();

                break;
            case 18:
                //桜画像フェードアウト
                this.cherry.tweener.clear().to({ alpha: 0 }, 1000).play();
                //抽象的な歪みの画像不透明化
                this.dandan.tweener.clear().to({ alpha: 1 }, 1000).play();
                //桜の絨毯画像フェードイン
                this.cherryCarpet = this.setBackImg("cherry_carpet");
                this.cherryCarpet.alpha = 0;
                this.cherryCarpet.tweener.clear().to({ alpha: 0.6 }, 1000).play();
                break;
            case 21:
                //マンデルブロ集合体フェードイン
                this.mandelbrot = this.setBackImg("mandelbrot");
                this.mandelbrot.setPosition(-50, 500);
                this.mandelbrot.alpha = 0;
                this.mandelbrot.tweener.clear().to({ alpha: 0.5 }, 1000)
                    .to({ x: 0, y: 700, scaleX: 1.6, scaleY: 1.6 }, 4000).play();
                break;
            case 22:
                //マンデルブロ集合体フェードアウト
                this.mandelbrot.tweener.to({ alpha: 0 }, 1000).play();
                //スパイラルライン
                this.spiral = SpiralLine().addChildTo(this.backImageGroup).setPosition(this.gridX.center(), this.gridY.center());
                this.spiral.alpha = 0;
                this.spiral.tweener.clear().to({ alpha: 0.6 }, 4000).play();
                break;
            case 24:
                // 抽象的な歪画像フェードアウト
                this.dandan.tweener.clear().to({ alpha: 0 }, 3000).play();
                //桜の絨毯画像フェードアウト
                this.cherryCarpet.tweener.clear().to({ alpha: 0 }, 3000).play();
                //スパイラルラインフェードアウト
                this.spiral.tweener.clear().to({ alpha: 0 }, 3000).play();
                break;
            case 26:
                //メインBGMフェードアウト
                SoundManager.stopMusic(1000);
                //雫画像
                this.drop = this.setBackImg("drop");
                this.drop.alpha = 0;
                this.drop.tweener.clear().to({ alpha: 1 }, 1000).play();

                //川BGMフェードイン
                this.river = SoundManager.playMusic("river_bgm", 700);
                break;
            case 27:
                //マンデルブロ集合体・スパイラルライン削除
                this.mandelbrot.remove();
                this.spiral.remove();
                //水滴SE再生
                SoundManager.play("drop_se");
                break;
            case 28:
                //雫画像フェードアウト
                this.drop.tweener.to({ alpha: 0 }, 2400).play();
                break;
            case 31:
                //川BGMフェードアウト
                SoundManager.stopMusic(2000);
                //油画像
                this.oil = this.setBackImg("oil");
                this.oil.blendMode = "lighter";
                this.oil.alpha = 0;
                this.oil.tweener.clear().to({ alpha: 1 }, 6000).play();
                break;
            case 36:
                //BGM再生していたら、ストップ
                SoundManager.currentMusic = null;
                //心臓音SE再生
                SoundManager.play("heart_beat05_se");
                //パーリンノイズ画像表示
                this.perlin = this.setBackImg("perlin").addChildTo(this);
                this.perlin.alpha = 0.3;
                this.perlin.blendMode = "lighter";
                //抽象的な歪みの画像
                this.dandan = this.setBackImg("dandan");
                this.dandan.blendMode = "lighter";
                this.dandan.alpha = 0;
                this.dandan.tweener.clear().to({ alpha: 0.5, scaleX: 2, scaleY: 2 }, 600)
                    .to({ alpha: 0, scaleX: 1, scaleY: 1 }, 1200).play();
                break;
            case 37:
                //エンディングBGMフェードイン
                SoundManager.playMusic("sinfonia01_bgm", 10000);
                break;
            case 41:
                this.oil.tweener.clear().to({ alpha: 0 }, 6000).play();
                break;
            case 44:
                //桜画像フェードイン
                this.cherry.tweener.clear().to({ alpha: 1 }, 2500).play();
                this.oil.tweener.clear().to({ alpha: 1 }, 1000)
                    .to({ alpha: 0 }, 1500).play();
                this.dandan.tweener.clear().to({ alpha: 1 }, 1000)
                    .to({ alpha: 0 }, 1500).play();
                break;
            case 47:
                this.exit();
                break;
        }
    },
    // 背景画像セット
    setBackImg: function (imgName) {
        let img = Sprite(imgName).addChildTo(this.backImageGroup)
            .setPosition(this.gridX.center(), this.gridY.center());
        return img;
    }
});

おまけ – 画像以外で使ったオブジェクト

実際に”【ノベルゲーム】櫻の樹の下には”で使用した、画像以外のオブジェクトのご紹介。

  • ColorfulLines()
  • SpiralLine()

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

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

Pocket

12件のコメント

  1. はじめまして、コメント失礼いたします。

    学校の課題で、大変参考になりました。
    ありがとうごうざいます。

    質問よろしいでしょうか?

    ストーリーが進む中で、
    画面にボタンやテキストを表示し、その押された処理によって、
    条件分岐を行いたいのですが
    今のコードだと、画面全体にタッチまたはクリックが認識されてしまいます。
    なにか助言いただけると幸いです。

    駆け出しプログラマ
    1. 駆け出しプログラマさん
      初めまして、コメントありがとうございます。

      お役に立てたのでしたら大変嬉しく思います。
      そうですか、学校の課題では、そういったものが出されるんですね。
      こちらも勉強になります。

      課題とのことで、
      どこまで具体的に申し上げてよいのかわからないのですが、
      とりあえず、コードは書かずに、私が考え付いた方法だけお伝えします。

      ●シーンを遷移してしまう方法
      思い切ってシーンを移して選択肢シーンのようなものを作ってしまう方法です。
      シーン遷移に関しましては、こちらの記事で書いていますのでご参考までに。
      https://horohorori.com/memo_phina_js/about_frame_and_time_and_scene/

      ●テキストを進ませるきっかけを、シーンによるクリック検出ではなく、
      画面かLabelRectに対するクリックイベントで起こす方法

      記事では、シーンのupdateのところで、
      シーンの中でのクリック検出をすることでテキストを進行させていますが、
      画面かLabelRectオブジェクト(this or this.labelRect)に対してのクリックイベントに
      テキストを進ませる処理を記述してしまえば、そういったことが可能かと思います。

      画面に対してのクリックイベントで進行させる場合、
      ここでも画面上の他のボタンかテキストに対してのクリックイベントと一緒に反応してしまうので、
      ボタンかテキストを生成するところで、
      一時的に画面(この場合、this)のinteractiveプロパティの値をfalseにして、
      クリックイベントを受け付けなくすることで可能かと思います。

      ボタンかテキストのクリック後、画面のinteractiveプロパティをtrueにすれば、
      再度クリックイベントを受け付けるようになります。

      また、文字を一文字ずつ表示させる処理は、
      シーンとは別にオブジェクト自体にupdateを設定することができるので、
      this.labelRectのupdateに設定してしまう方法が良いかと思います。

      オブジェクト単体へのクリックイベントの設定は、下の記事で書いているので、よろしければ参考にどうぞ。
      https://horohorori.com/memo_phina_js/about_input_opetate/#click_touch1

      もっと詳しい説明のご要望や疑問点などございましたら、またお手数ですが再度ご質問ください。

      1. ご返信ありがとうございます。
        とても参考になります。

        ほろほろり様の一つ目の提案、画面に対してのクリックイベントで進行させる手法を
        実装しようとしました。
        setPhase関数のcase文の中で、あるインデックス番号にきたらthis.setInteractive(false)の処理を追加しました。
        しかし、タッチ、クリックを受け付けてしまいます。

        シーンが問題なのかなぁ・・・
        私が開発している物は、「タイトルシーン」➡︎「メインシーン」
        と画面遷移をしているのですが、タイトルシーンのinit関数内でthis.setInteractive(false) を追加したら、問題なく動作しました。
        しかし、メインシーンの様々な場所(init関数、update関数)でthis.setInteractive(false)を追加しても、クリックを受け付けてしまいます。
        setPhase関数内でinteractiveプロパティのフラグが変わる挙動はない気がしますし・・・

        無知ですいません。
        何卒、助言をいただけると幸いです。

        (プログラミング難しいぃぃぃいい!!!)

        駆け出しプログラマ
        1. 再訪問・再コメントいただき、ありがとうございます!
          最初は誰だって無知ですので、大丈夫ですよ。焦らず行きましょう。

          ソースコードを拝見していないので推測ですが、
          シーンのupdate内で、タッチ・クリックの判定をして、
          テキストを進行させていませんか?

          言葉が足りなかったかもしれませんので、
          わかりずらかったかもしれませんが、
          メインシーンのupdate内は空にします。

          なので、少し書き直すところが出てくるかと思います。

          記事のメインシーンのupdate内では、app.pointerで、
          appから、
          常にマウス(あるいはタッチ)の状態を取得している感じで、
          クリックの判定をしているのです。

          画面に対してのクリックイベントでテキストを進行させる方法は、
          クリックイベントを
          this.onpointstart = () => {
          // クリックイベント発生時の処理
          };
          などで、thisを使っているので、
          this.setInteractive(false);
          でクリック無効化できるのですが、
          appとthisは別物なので、
          記事内でやっている
          app.pointer.getPointingStart()

          this.setInteractive(false);
          では制御できません。

          といったところでいかがでしょうか?

          LabelRect()関数でinteractiveプロパティの挙動が~
          というのは、どうやろうとしているのか、
          もうちょっと詳しくお聞かせ願えませんでしょうか。

          1. こんな初心者に付き合ってくれて本当に感謝です。
            (焦りたくはないんですが、課題の締め切りは近づいてくるんすよね・・・笑)

            なるほど!
            メインシーン内updateを空にし、LabelRect()関数内のupdate関数か!

            ・・・
            実装してみたら、やはりエラーが・・・(泣)
            LabelRect内ではthis.setPhaseを呼び出すことは不可能なのかな・・・?
            LabelRect内でsetPhaseのようなものを組む必要が?
            でも、分別してしまうと背景などの扱いが複雑化してしまいますよね。
            この辺、クラスの概念とかがまだあやふやだ・・・
            Phinaは若干、癖がある気がします。。。

            すいません、LabelRect()関数でinteractiveプロパティの挙動が~
            というのは私のただの勘違いでした。申し訳ありません。

            (今、githubのリポジトリを挙げようと試していたら、久しぶりすぎて
            sshハマってしまいました泣 できたら、載せさせてもらいます)

            駆け出しプログラマ
    1. いえいえ、大丈夫ですよ!
      なるほど、締め切りは確かに焦らなきゃですね…
      私に出来る範囲ではお手をお貸ししますので、がんばってください!

      ああ!なるほど!!
      ソースコード拝見したんですが、
      えーとですね、
      まず、LabelRect()のupdate()の設定なのですが、
      それを、MainScene()内で設定する感じで考えてたんですよ。
      つまり、MainScene()のinit()内で
      子オブジェクトとして生成した状態のthis.labelRectに設定する感じで、
      this.labelRect.update=()=>{
      //一文字一文字追加で表示する処理
      };
      こうすれば、
      同じシーン内なので、this.setPhase()も参照できます。

      次に、テキスト進行の全ての処理をthis.labelRectのupdateで、とすると、
      画面全体に対するクリック判定でのテキスト進行の処理を行うのは、
      多分無理です(もしかしたら方法あるかもしれませんが、私は存じ上げませんので)

      ですので、一文字一文字追加で表示する処理だけを
      this.labelRect.updateに任せて、
      他のテキストの進行の処理は、
      基本的にthis.onpointstartでの画面全体に対してのクリックイベントに設定、
      という方法でいかがでしょうか?

      それから、これがたぶん一番重要なとこなのですが、
      this.onpointstartの使い方が違います。
      onpointstartには、クリックの有無が真偽値で入るのではなく、
      クリック時に発火させるイベントの関数を直接セットする感じなんですよ。

      ここが確かにちょっとややこしいところですね。
      具体的には、
      app.pointer.getPointingStart()
      ・・・クリックされているかどうかの真偽値が入っている
      this.onpointstart
      ・・・画面がクリックされたときに起こしたいイベントを関数で入れる
      といった感じ。

      なので、
      使用するときに、
      /** update(app)の中に記述 ***********************/
      // 更新毎にチェックし、クリックがされた場合、値がtrueになるので、
      if(app.pointer.getPointingStart()){
      // if文の中に記述した起こしたいイベントが発火する
      }

      /** init()等で記述しても問題ない *****/
      // あらかじめ、対象オブジェクトにクリック時に発火させたい処理(関数)を設定しておく
      this.onpointstart = () =>{
      // クリック時にここに記述しておいた処理が呼び出される
      };
      といった使い方の違いがあります。

      何分独学なもので、私の説明が下手なのかもしれませんね(^_^;)
      何度も質問のお手間取らせてしまってすみません(>人<) でも、課題、応援してます!

  2. 毎度、ご返信ありがとうございます!!!
    いやいや!何をおっしゃるんですか・・・
    こんな初学者に対して、ものすごく親切にして、本当に感謝ですよ・・・!

    なるほど!
    this.labelRect.textIndexの分岐は良い方法ですね!

    再び質問よろしいでしょうか?泣
    app.pointer.getPointingStart()のような感じで、真偽値を返してくれるものが、
    this.labelRectやspriteにも適用できる関数はあるのですか?
    「クリックされた時の処理」を行おうとして
    記事のcase文内で、textIndexがある程度進んだら、画面にタッチ有効な画像(Sprite)を複数設置しました。
    どの画像を押されたにより分岐を行いたいのですがここで問題が・・・

    this.〇〇(生成したsprite).onclick = function () {
    //ここにやりたい処理を書くのですよね?
      //しかし、ここで書くthisの意味合いがこの関数外とでは違う!
      //this.labelRect.〇〇と書いても、「そんなの定義してないよぉ〜」とエラーが
    }
    このように、別のクラス?関数に潜ったら、thisの意味合いが違くなり
    参照したい場所へのアクセスができなくなります・・・泣

    解決策としては
    1. 参照の仕方がある
    2. app.pointer.getPointingStart()のような感じで、真偽値を返してくれるものでif文で行う

    と思っているのですが、なかなか調べてできなくて・・・

    本当に毎回毎回すいません・・・

    駆け出しプログラマ
    1. いや、すみません、気を使わせてしまいましたね(^∀^;)
      (今コメント見返すと、
      ほんと何言ってんだって感じでびっくりしました…
      夜は寝なきゃダメですねww)

      ちょっと夜、沈んでおりました時に書きこんでたみたいで、
      寝たらテンションがまともになりましたのでご安心を!
      いやはやお恥ずかしいw忘れて下さるとありがたいですww

      お役に立てているなら、こちらとしましても大変嬉しいです!!!

      ご質問の、
      まずは
      /******************************************************************
      ●app.pointer.getPointingStart()のような感じで、真偽値を返してくれるものが、
      this.labelRectやspriteにも適用できる関数はあるのですか?
      ***********************************************************/
      とのことですが、とりあえずonpointstartで、オブジェクト側から、
      例えば、

      this.labelRect.onpointstart = (e) =>{
      console.log(e.pointer.getPointingStart()); // クリックされた時コンソールに”true”
      };

      と言ったやり方ならあるのですが、
      これはいかがでしょうか?

      もう一方の、
      /*********************************************************
      ●this.〇〇(生成したsprite).onclick = function () {
      //ここにやりたい処理を書くのですよね?
        //しかし、ここで書くthisの意味合いがこの関数外とでは違う!
        //this.labelRect.〇〇と書いても、「そんなの定義してないよぉ〜」とエラーが
      }
      このように、別のクラス?関数に潜ったら、thisの意味合いが違くなり
      参照したい場所へのアクセスができなくなります・・・泣
      /************************************************************/
      の方は、アロー演算子「=()=>」を使ってしまえば、
      外のthisも内側から同様に参照できますが、
      function(){}を使う場合は、

      let self = this;
      this.○○.onclick = function(){
      self.labelRect.○○
      };

      といったやり方で出来るかと思われます。

      またわからないことがあれば、ご質問をお気軽にお寄せください!
      課題、頑張ってくださいね!

      1. 夜にご返信いただいていたのですね。。。
        なんかすいません。ありがとうございます!!!

        なるほどー。
        やはりまだまだですねぇ。アロー演算子とかの存在は知っていたものの、
        「こんなのいつ使うんや!!!」と思って、忘れてましたw

        ほろほろりさんのコメントを読んで、試行錯誤した結果、
        なんとか形にでき、期限内に提出することができました。
        本当に、ありがとうございました!!!
        めっちゃ助かりました!(レスポンスも早くて、感謝です)

        また、お世話になるかもしれませんが、
        そのときはまた、温かい目でみてください!笑

        駆け出しプログラマ
        1. いえいえ!
          もともと別の事をしていた時に、
          ふと思いついて返信させて頂いただけですので、
          お気になさらず!

          知識として知ってても、実際に使う段階になって、
          やっとそれの真価がわかるってよくありますよねw

          おお!!!それは良かった!
          いえいえ、私はただほんのちょっとだけお手伝いをさせていただいただけで、
          アイディアも、そのアイディアを形にして完成させたのも、
          駆け出しプログラマさんですよ!

          はい!また何時でも、ご遠慮なくいらしてくださいませ!
          駆け出しプログラマさんの、
          更なる飛躍をお祈りいたしております!

コメントを残す

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

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