Skip to content

オープニングとエンディング

本章では、オープニング演出とエンディング演出の追加について説明します。

オープニング演出の作成

オープニングはゲームの概要を説明する重要な部分です。 とくにランキング形式のニコ生ゲームにおいては、ゲームの起動時間に制約があります。 この時間の中で、簡潔かつわかりやすくゲームのルールを伝えられることが重要になります。

今回は以下のタイトルロゴと説明を順番に表示するようなオープニングを実装してみます。 以下の画像をそれぞれダウンロードして assets/opening/ ディレクトリに保存しておきます。

タイトルロゴ

説明

配置後、忘れずに akashic scan を実行します。

sh
akashic scan asset
akashic scan asset

それではオープニング用のシーン openingScene を新規に作成します。

javascript
exports.main = (param) => {
    const game = g.game; // よくアクセスするため変数に保持しておく

    // オープニング用シーンの作成
    const openingScene = new g.Scene({
        game,
        assetPaths: [
            "/assets/opening/*"
        ],
    });
    openingScene.onLoad.addOnce(() => {
        const titleLogo = new g.Sprite({
            scene: openingScene,
            src: openingScene.asset.getImage("/assets/opening/breakout_title.png"),
            x: game.width / 2,
            y: game.height / 2,
            anchorX: 0.5,
            anchorY: 0.5,
        });
        openingScene.append(titleLogo);
 
        const descriptionLogo = new g.Sprite({
            scene: openingScene,
            src: openingScene.asset.getImage("/assets/opening/breakout_description.png"),
            x: game.width / 2,
            y: game.height / 2,
            anchorX: 0.5,
            anchorY: 0.5,
        });
 
        openingScene.setTimeout(() => {
            titleLogo.destroy();
            openingScene.append(descriptionLogo);
        }, 2000);
 
        openingScene.setTimeout(() => {
            game.replaceScene(scene);
        }, 6000);
    });
    game.pushScene(openingScene);

    const scene = new g.Scene({
        game,
        assetPaths: [
            "/assets/images/*",
            "/assets/se/*",
            "/assets/fonts/*",
        ],
    });
exports.main = (param) => {
    const game = g.game; // よくアクセスするため変数に保持しておく

    // オープニング用シーンの作成
    const openingScene = new g.Scene({
        game,
        assetPaths: [
            "/assets/opening/*"
        ],
    });
    openingScene.onLoad.addOnce(() => {
        const titleLogo = new g.Sprite({
            scene: openingScene,
            src: openingScene.asset.getImage("/assets/opening/breakout_title.png"),
            x: game.width / 2,
            y: game.height / 2,
            anchorX: 0.5,
            anchorY: 0.5,
        });
        openingScene.append(titleLogo);
 
        const descriptionLogo = new g.Sprite({
            scene: openingScene,
            src: openingScene.asset.getImage("/assets/opening/breakout_description.png"),
            x: game.width / 2,
            y: game.height / 2,
            anchorX: 0.5,
            anchorY: 0.5,
        });
 
        openingScene.setTimeout(() => {
            titleLogo.destroy();
            openingScene.append(descriptionLogo);
        }, 2000);
 
        openingScene.setTimeout(() => {
            game.replaceScene(scene);
        }, 6000);
    });
    game.pushScene(openingScene);

    const scene = new g.Scene({
        game,
        assetPaths: [
            "/assets/images/*",
            "/assets/se/*",
            "/assets/fonts/*",
        ],
    });

openingScene では、最初にゲームのロゴを2秒間表示し、その後ゲームの説明を4秒間 (ゲーム開始から6秒後まで) 表示します。 scene.setTimeout()scene.setInterval() に似ていますが、繰り返しではなく一度のみ処理が実行されます。 scene の部分には openingScene を指定することに注意してください。

ゲームのシーンへと遷移には game.replaceScene() を利用しています。

TIP

game.pushScene() は現在のシーンスタックに対して遷移先のシーンを追加するため、遷移元のシーンの状態を保存します。 一方 game.replaceScene() は遷移元のシーンを遷移先のシーンに置き換えます。 最初に追加するシーン以外で、戻る必要のないシーン遷移では game.replaceScene() を利用すべきでしょう。

最後にコード末尾にある game.pushScene(scene) を削除します。

javascript
    scene.onLoad.add(() => {
        ...

        function pointInRect(x, y, e) {
            ...
        }
    });
    game.pushScene(scene);
};
    scene.onLoad.add(() => {
        ...

        function pointInRect(x, y, e) {
            ...
        }
    });
    game.pushScene(scene);
};

ゲームを実行してみましょう。 ゲームロゴの後に説明が表示され、その後ゲーム画面へと遷移します。

エンディング演出の作成

エンディング画面では、ゲームの終了通知と獲得したスコアの2つの要素を表示します。

終了ロゴ

まずは終了を通知するロゴをゲームに組み込みます。 以下の画像をダウンロードして assets/images へ配置してください。

ゲーム終了時に終了ロゴを表示するように修正します。 前に残しておいた // TODO: 終了処理 というコメントの部分に移動して、終了ロゴを表示させるコードを追加します。

javascript
        // 残り時間の更新
        const timer = scene.setInterval(() => {
            remainingTime--;
            if (remainingTime === 0) {
                scene.clearInterval(timer);
                vx = 0;
                vy = 0;
                // TODO: 終了処理
                // 終了ロゴを表示
                const finishLogo = new g.Sprite({
                    scene,
                    src: scene.asset.getImage("/assets/images/breakout_finish.png"),
                    x: game.width / 2,
                    y: game.height / 2,
                    anchorX: 0.5,
                    anchorY: 0.5,
                });
                scene.append(finishLogo);
            }
            updateTimer();
        }, 1000);
        // 残り時間の更新
        const timer = scene.setInterval(() => {
            remainingTime--;
            if (remainingTime === 0) {
                scene.clearInterval(timer);
                vx = 0;
                vy = 0;
                // TODO: 終了処理
                // 終了ロゴを表示
                const finishLogo = new g.Sprite({
                    scene,
                    src: scene.asset.getImage("/assets/images/breakout_finish.png"),
                    x: game.width / 2,
                    y: game.height / 2,
                    anchorX: 0.5,
                    anchorY: 0.5,
                });
                scene.append(finishLogo);
            }
            updateTimer();
        }, 1000);

終了時に効果音を鳴らします。 scene.onLoad の直後でオーディオ再生コンテキストを生成します。

javascript
    scene.onLoad.add(() => {
        const sePaddleAsset = scene.asset.getAudio("/assets/se/se_paddle");
        const sePaddle = game.audio.create(sePaddleAsset);
        const seBlockAsset = scene.asset.getAudio("/assets/se/se_block");
        const seBlock = game.audio.create(seBlockAsset);
        const seMissAsset = scene.asset.getAudio("/assets/se/se_miss");
        const seMiss = game.audio.create(seMissAsset);
        const seFinishAsset = scene.asset.getAudio("/assets/se/se_finish");
        const seFinish = game.audio.create(seFinishAsset);

        ...
    });
    scene.onLoad.add(() => {
        const sePaddleAsset = scene.asset.getAudio("/assets/se/se_paddle");
        const sePaddle = game.audio.create(sePaddleAsset);
        const seBlockAsset = scene.asset.getAudio("/assets/se/se_block");
        const seBlock = game.audio.create(seBlockAsset);
        const seMissAsset = scene.asset.getAudio("/assets/se/se_miss");
        const seMiss = game.audio.create(seMissAsset);
        const seFinishAsset = scene.asset.getAudio("/assets/se/se_finish");
        const seFinish = game.audio.create(seFinishAsset);

        ...
    });

ロゴ表示部分で効果音を再生します。

javascript
        // 残り時間の更新
        const timer = scene.setInterval(() => {
            remainingTime--;
            if (remainingTime === 0) {
                scene.clearInterval(timer);
                vx = 0;
                vy = 0;
                // 終了ロゴを表示
                const finishLogo = new g.Sprite({
                    ...
                });
                scene.append(finishLogo);
                seFinish.play();
            }
            updateTimer();
        }, 1000);
        // 残り時間の更新
        const timer = scene.setInterval(() => {
            remainingTime--;
            if (remainingTime === 0) {
                scene.clearInterval(timer);
                vx = 0;
                vy = 0;
                // 終了ロゴを表示
                const finishLogo = new g.Sprite({
                    ...
                });
                scene.append(finishLogo);
                seFinish.play();
            }
            updateTimer();
        }, 1000);

スコア結果表示

最後に、プレイヤーが獲得したスコアを表示するための結果画面を作成しましょう。 以下の画像をダウンロードして assets/ending ディレクトリへと配置してください。

結果画面では、スコア表示をわかりやすく目立つように、大きめのサイズのビットマップフォントを新たに作成します。 以下のコマンドを実行して font-number-large を作成します。

sh
bmpfont-generator rounded-mplus-2p-black.ttf -F "#fff" -S "#000" --stroke-width 4 -c "0123456789+-" -H 80 --margin 3 assets/fonts/font-number-large.png
bmpfont-generator rounded-mplus-2p-black.ttf -F "#fff" -S "#000" --stroke-width 4 -c "0123456789+-" -H 80 --margin 3 assets/fonts/font-number-large.png

akashic scan コマンドで変更を適用します。

sh
akashic scan asset
akashic scan asset

結果画面を表示するシーン endingScene を作成します。 このシーンでは、スコアと表示されているパネル上に先ほど作成したフォント font-number-large でプレイヤーのスコアを表示します。 scene.onLoad の後ろ (コードの最終行付近) に以下のコードを追加します。

javascript
    scene.onLoad.add(() => {
        ...
    });

    const endingScene = new g.Scene({
        game,
        assetPaths: [
            "/assets/fonts/*",
            "/assets/ending/*",
        ],
    });
    endingScene.onLoad.addOnce(() => {
        // スコア用フォントの作成
        const font = new g.BitmapFont({
            scene: endingScene,
            src: endingScene.asset.getImage("/assets/fonts/font-number-large.png"),
            glyphInfo: endingScene.asset.getJSONContent("/assets/fonts/font-number-large_glyphs.json"),
        });
 
        // スコア表示用のパネル表示
        const resultPanel = new g.Sprite({
            scene: endingScene,
            src: endingScene.asset.getImage("/assets/ending/breakout_result.png"),
            x: game.width / 2,
            y: game.height / 2,
            anchorX: 0.5,
            anchorY: 0.5,
        });
        endingScene.append(resultPanel);
 
        // スコア結果表示エンティティの作成
        const resultScoreLabel = new g.Label({
            scene: endingScene,
            font,
            fontSize: font.size,
            text: `${game.vars.gameState.score}`,
            x: resultPanel.x,
            y: resultPanel.y + 60,
            anchorX: 0.5,
            anchorY: 0.5,
        });
        endingScene.append(resultScoreLabel);
    });
};
    scene.onLoad.add(() => {
        ...
    });

    const endingScene = new g.Scene({
        game,
        assetPaths: [
            "/assets/fonts/*",
            "/assets/ending/*",
        ],
    });
    endingScene.onLoad.addOnce(() => {
        // スコア用フォントの作成
        const font = new g.BitmapFont({
            scene: endingScene,
            src: endingScene.asset.getImage("/assets/fonts/font-number-large.png"),
            glyphInfo: endingScene.asset.getJSONContent("/assets/fonts/font-number-large_glyphs.json"),
        });
 
        // スコア表示用のパネル表示
        const resultPanel = new g.Sprite({
            scene: endingScene,
            src: endingScene.asset.getImage("/assets/ending/breakout_result.png"),
            x: game.width / 2,
            y: game.height / 2,
            anchorX: 0.5,
            anchorY: 0.5,
        });
        endingScene.append(resultPanel);
 
        // スコア結果表示エンティティの作成
        const resultScoreLabel = new g.Label({
            scene: endingScene,
            font,
            fontSize: font.size,
            text: `${game.vars.gameState.score}`,
            x: resultPanel.x,
            y: resultPanel.y + 60,
            anchorX: 0.5,
            anchorY: 0.5,
        });
        endingScene.append(resultScoreLabel);
    });
};

ゲームの終了後に endingScene へと遷移するようにコードを追加します。

javascript
        // 残り時間の更新
        const timer = scene.setInterval(() => {
            remainingTime--;
            if (remainingTime === 0) {
                scene.clearInterval(timer);
                vx = 0;
                vy = 0;

                // 終了ロゴを表示
                const finishLogo = new g.Sprite({
                    ...
                });
                scene.append(finishLogo);
                seFinish.play();

                // エンディングシーンへと遷移
                scene.setTimeout(() => {
                    game.replaceScene(endingScene);
                }, 3000);
            }
            updateTimer();
        }, 1000);
        // 残り時間の更新
        const timer = scene.setInterval(() => {
            remainingTime--;
            if (remainingTime === 0) {
                scene.clearInterval(timer);
                vx = 0;
                vy = 0;

                // 終了ロゴを表示
                const finishLogo = new g.Sprite({
                    ...
                });
                scene.append(finishLogo);
                seFinish.play();

                // エンディングシーンへと遷移
                scene.setTimeout(() => {
                    game.replaceScene(endingScene);
                }, 3000);
            }
            updateTimer();
        }, 1000);

ゲームを実行してみましょう。 左上の残り時間が 0 になったタイミングで「Finish!」というロゴが表示され、その後獲得スコアが表示されれば成功です。

番外編: トゥイーンアニメーションを使った演出

トゥイーンアニメーション(Tween Animation)はアニメーションを作成する手法の一つで、物体やオブジェクトがある状態から別の状態に滑らかに変化するような動きを実現できます。 ゲーム制作だけではなく Web サイトのアニメーション演出など広く使われています。

Akashic Engine では、トゥイーンアニメーションを手軽に利用できる拡張ライブラリ akashic-timeline を提供しています。 以下をクリックすることでデモを動作させることができます。

今回は、実際に akashic-timeline を使って、残り時間が少なくなったときに画面全体を赤く明滅させるような警告表示を導入してみます。

akashic-timeline のインストール

akashic-timeline は akashic install コマンドを使ってインストールできます。

sh
akashic install @akashic-extension/akashic-timeline
akashic install @akashic-extension/akashic-timeline

以下のように出力されていることを確認します。

INFO: Installing @akashic-extension/akashic-timeline...
INFO: Adding file paths to globalScripts...
INFO: Adding file paths to moduleMainScripts...
INFO: Done!
INFO: Installing @akashic-extension/akashic-timeline...
INFO: Adding file paths to globalScripts...
INFO: Adding file paths to moduleMainScripts...
INFO: Done!

akashic-timeline の利用

コードの冒頭で require() を使用し、以降は tl 変数を介して akashic-timeline を利用します。

javascript
const tl = require("@akashic-extension/akashic-timeline"); 

exports.main = (param) => {
    ...
const tl = require("@akashic-extension/akashic-timeline"); 

exports.main = (param) => {
    ...

トゥイーンの作成には、まずはシーンに紐づいた Timeline を生成する必要があります。 scene の生成直後で Timeline を生成します。

javascript
    const scene = new g.Scene({
        game,
        assetPaths: [
            "/assets/images/*",
            "/assets/se/*",
            "/assets/fonts/*",
        ],
    });
    const timeline = new tl.Timeline(scene); 
    const scene = new g.Scene({
        game,
        assetPaths: [
            "/assets/images/*",
            "/assets/se/*",
            "/assets/fonts/*",
        ],
    });
    const timeline = new tl.Timeline(scene); 

トゥイーンを適用する警告用の赤い背景 warning は、blockContainer が生成された直後に作成します。

javascript
        const blockContainer = new g.E({
            scene,
        });
        scene.append(blockContainer);

        // 残り時間が少ないときの警告表示
        const warning = new g.FilledRect({
            scene,
            cssColor: "#f00",
            x: 0,
            y: 0,
            width: game.width,
            height: game.height,
            opacity: 0, // 初期状態は透過
        });
        scene.append(warning);
        const blockContainer = new g.E({
            scene,
        });
        scene.append(blockContainer);

        // 残り時間が少ないときの警告表示
        const warning = new g.FilledRect({
            scene,
            cssColor: "#f00",
            x: 0,
            y: 0,
            width: game.width,
            height: game.height,
            opacity: 0, // 初期状態は透過
        });
        scene.append(warning);

opacity0 を設定することで、エンティティを透過状態にしています。

残り時間の更新処理で、トゥイーンを使って警告用の背景を明滅させます。

javascript
        // 残り時間の更新
        const timer = scene.setInterval(() => {
            remainingTime--;
            if (remainingTime === 0) {
                ...
            } else if (remainingTime === 10) {
                const tween = timeline.create(warning, {
                    loop: true, // ループを有効にする
                });
                // 警告用の明滅トゥイーンの作成
                tween
                    .to({ opacity: 0.2 }, 300) // 300 ミリ秒で透過度 20%
                    .to({ opacity: 0 }, 300) // 300 ミリ秒で透過度を 0% に
                    .wait(400)
                    .call(() => {
                        if (remainingTime === 0) tween.cancel(); // 終了したらトゥイーンを終了
                    });
            }
            updateTimer();
        }, 1000);
        // 残り時間の更新
        const timer = scene.setInterval(() => {
            remainingTime--;
            if (remainingTime === 0) {
                ...
            } else if (remainingTime === 10) {
                const tween = timeline.create(warning, {
                    loop: true, // ループを有効にする
                });
                // 警告用の明滅トゥイーンの作成
                tween
                    .to({ opacity: 0.2 }, 300) // 300 ミリ秒で透過度 20%
                    .to({ opacity: 0 }, 300) // 300 ミリ秒で透過度を 0% に
                    .wait(400)
                    .call(() => {
                        if (remainingTime === 0) tween.cancel(); // 終了したらトゥイーンを終了
                    });
            }
            updateTimer();
        }, 1000);

remainingTime10 になったときに警告の表示を開始します。

トゥイーンの生成には timeline.create() を用います。 第1引数には対象のエンティティを、第2引数にはオプションを指定します。 オプションで looptrue にしているため、トゥイーンアニメーションが繰り返し再生されます。

戻り値である変数 tween に、実際のトゥイーンの変化をメソッドチェーン形式で定義します。

to() メソッドはエンティティの指定されたプロパティを変化させるためのもので、ここでは opacity を指定しています。 第2引数には変化にかかる時間をミリ秒単位で指定します。

wait() メソッドは指定のミリ秒だけトゥイーンを一時停止します。

call() メソッドは任意の関数を呼び出すもので、残り時間 remainingTime0 になったとき、トゥイーンをキャンセルして警告用の表示を停止します。

以上から、このトゥイーンは以下のような挙動を示します。

  1. warning.opacity300 ミリ秒かけて 0.2 に変化させる
  2. warning.opacity300 ミリ秒かけて 0 に変化させる
  3. 400 ミリ秒待機する
  4. remainingTime0 であればトゥイーンをキャンセル、そうでなければ 1. へループ

これで警告表示を実現できました。

akashic-timeline には他にも様々な機能が備わっています。 詳細については ガイド文書API リファレンス を参照してください。

TIP

Akashic Engine が提供する拡張ライブラリとして、二次元の物理エンジンをサポートする akashic-box2d、複数行の文字表示をサポートする akashic-label など他にも多くのものがあります。

また、もちろん開発者自身で拡張ライブラリを作成することもできます。 詳細については 拡張ライブラリを使う も参照してください。

完成したゲームとソースコード

Playground で実行

おわりに

おつかれさまです! 以上で「ブロックくずしをつくろう」のチュートリアルは終了となります。 本チュートリアルを通してゲーム開発に興味が湧いたのなら、ぜひ Akashic Engine を使って自身のゲームを作成してみましょう。

冒頭でも説明した通り、作成したゲームはニコニコ生放送上で遊ぶことができます。 ニコ生ゲームを投稿しよう から、実際に作成したゲームをニコニコ生放送上で遊ぶための手順を確認することができます。

今回作成したゲームは一人プレイのものですが、Akashic Engine を使うと放送者と視聴者が参加できるマルチプレイのゲームも簡単に作成することができます。 マルチプレイのゲームを作成するためのガイドについては ニコ生ゲームを作ろう » マルチプレイゲーム を参照してください。

その他ニコ生ゲームを作る際の情報については ニコ生ゲームを作ろう にまとまっています。 ニコ生ゲームを作成する際には、これらのページも併せて参照してください。