Skip to content

改造: 残り時間を表示してみる ​

Galaxy Wars の画面右上には、ハイスコア表示があります。 しかしサンプルデモにはスコアを保存する機能がないので、特に意味のない表示になっています。

ハイスコア表示

ここでは、ニコ生ゲームでより意味のある「残り時間」を表示するよう改造し、 関連する Akashic Engine の機能と時間経過の扱いを紹介します。

ハイスコアを表示するコード ​

ハイスコアを残り時間に変えるには、まずハイスコアの表示を行っている箇所を探す必要があります。 ここまで、処理を探す際はファイル名であたりをつけてきました。 しかし今回の場合、"score" を含むようなファイルは見当たりません。

角度を変えて、script/ フォルダ内の全ファイルからテキスト検索してみましょう。 ゲーム画面を見るとハイスコアは HI と表示されているので、その処理の近くには恐らく HI という記述があるはずです。

実際に HI (末尾の空白に注意してください) で検索してみると、 scirpt/ フォルダ内では PlayerStatus.js にしかこの並びがないことが分かります。

詳細: Windows でフォルダ内のテキストを検索する手順

フォルダ内のファイルの内容からテキストを検索する方法は環境によります。

Windows の場合、エクスプローラーの検索機能が使えます。 .zip ファイルを展開してできたフォルダを開き、右上の入力欄に文字を入力すると、そのフォルダを検索できます。

エクスプローラーの検索機能

ただしデフォルトでは、この検索機能はフォルダ内の「ファイル名」しか検索できません。 ここでは「ファイルの内容」を検索したいので、準備が必要です。 ツールバーの ... ボタンを押して、メニューから オプション を選択してください。

エクスプローラーのオプション

フォルダーオプション ダイアログが表示されるので、 検索 タブの ファイル名と内容を常に検索する (C) (数分かかる場合があります) にチェックを入れてください。

フォルダーオプションダイアログの「検索」タブ

OK ボタンでダイアログを閉じて、右上のテキストボックスに "HI " と入力すると、 PlayerStatus.js だけが表示され、このテキストを含むファイルが一つしかないことがわかります。 (残念ながら HI だけでは空白文字を検索できないようなので、ここでは "HI " で検索する必要がありました)

検索した結果

今後ファイルの内容まで検索する動作が不要な場合は、同じ手順で フォルダーオプション ダイアログを開き、ファイル名と内容を常に検索する (C) (数分かかる場合があります) のチェックを外しておきましょう。 OK ボタンを押してダイアログを閉じるのを忘れないようにしてください。

なおここでは、他のツールを導入しないで済むようにエクスプローラーの機能を使いました。 本格的にゲームを開発する場合は、Visual Studio Code のようなエディタの検索機能など、より便利で手軽な (ただし事前準備が必要な) 方法をおすすめします。

PlayerStatus.js の内容を確認すると、 次の部分に HI が見つかります。

js
class PlayerStatus {
    ...
    /**
     * 状態更新
     */
    update() {
        this.scoreLabel.text = "SCORE " + ("00000" + Global_1.Global.gameCore.player.score).slice(-6);
        this.scoreLabel.invalidate();
        this.hiScoreLabel.text = "HI " + ("00000" + Global_1.Global.hiScore).slice(-6);
        this.hiScoreLabel.invalidate();
        const scales = this.hpGauge.children;
        for (let i = 0; i < scales.length; i++) {
            const scale = scales[i];
            scale.cssColor = (i < Global_1.Global.gameCore.player.hp) ? "Yellow" : "Red";
            scale.modified();
        }
        return true;
    }
    ...
}

問題の行を抜き出すとこのような形になっています。= 演算子を使った代入文です。

js
this.hiScoreLabel.text = "HI " + (何か);

"HI " + (何か) という値を、this.hiScoreLabel.text に 代入していることがわかります。 いかにもハイスコアを表示していそうな名前です。 ここに代入した内容が画面右上に反映されるプログラムになっているのでしょう。

INFO

「ハイスコア」は "high score" なので、 "hi score" だと英語的には正しくありません。 しかしソースコード内で一貫してこの表記なので、ここではそのまま hiScore という名前を使っています。

代入されているのは "HI " + (何か) の形なので、二つの値を + した結果です。 その左辺 "HI " は 文字列 です。 ダブルクォート (") に囲まれた記号列は、JavaScript では「その記号列を表す文字列」という意味になります。

TIP 文字列

ここまであまり説明なく使っていますが、 文字列 はプログラミングによく登場する概念で、その名のとおり文字 (記号) の列です。 文字列は、キャラクター名やアイテムの説明文のような「ゲーム画面に表示するテキストデータ」や、 画像や音声ファイルのパス (アセットパス) のような「ゲーム内の処理で使う文字情報」を扱うために利用します。

JavaScript の + 演算子は、対象が文字列の場合 (数値の加算ではなく) 「二つの文字列を連結する」という意味になります。 画面に表示されるものとの見比べると、"HI " + (何か) の (何か) 部分がハイスコアの値に対応しているだろうと想像できます。

TIP 文字列と算術演算子

一般的な数学のイメージとは異なり、JavaScript では + に文字列を与えると、文字列の連結処理になります。

js
const x = 3 + 4; // => 7 (数値の加算)
const message = "Hello " + "Akashic"; // => "Hello Akashic" (文字列の連結)

また + の右辺と左辺どちらかが文字列の場合は、もう一方を文字列に変換して連結します。 (厳密には他にもさまざまな条件を考慮しますが、大雑把には)

たとえば数値は、文字列に変換すると「その値を表す文字列」(e.g. 7 なら "7") になります。 そのため次のようなコードを書くことができます。

js
const x = 3 + 4;
const message = "足したら " + x; // => "足したら 7"

なおこれは + だけの特徴です。 - (減算) や、このあと登場する * (乗算) と / (除算) などの演算子は、文字列に対して使っても特別な効果はありません。

文字列の記法

JavaScript の文字列はダブルクォート (") の他、シングルクォート (') やバッククォート (`) で囲む記法で書かれることもあります (例: 'メニュー', `アイテム`) 。 それぞれ若干異なる部分がありますが、この記事では " しか使わないので詳細は割愛します。 どの記法で書かれたものでも結局は文字列なので、処理上の扱いは同じです。

残り時間表示への変更 ​

ではこのハイスコアを表示するコードを、残り時間の表示処理に変えていきましょう。次のような作業が必要になりそうです。

  • 変数の名前を変える
  • HI の部分を TIME に変える
  • ハイスコアの代わりに残り時間を表示させる

順に見ていきましょう。

変数の名前を変える ​

まず hiScoreLabel という変数名を変更しましょう。 "Label" はともかく "hiScore" の部分は「残り時間」とは関係ないためです。 代わりに timeLabel と呼ぶことにします。

PlayerStatus.js 中の hiScoreLabel をすべて timeLabel に書き換えてください。

TIP 変数名の重複

変数 hiScoreLabel は、本当は他のファイルに登場する可能性もあります。 そのため厳密にはscript/ フォルダの全テキストから検索して hiScoreLabel の利用箇所を探すのが確実です。 また逆に、同じ hiScoreLabel という名前でも、別の箇所で定義された「別の変数」であることもあり得ます。 そのため機械的に変数名を置換すると、本来変えたかったもの以外も巻き込んでしまうことがあります。

このあたりは都度前後のプログラムを読み解いて考える必要があります。 今回のケースでは幸い、 hiScoreLabel はすべて一つの同じ変数で、 PlayerStatus.js の中にしか現れませんでした。

TIP 名前の重要性

厳密にはこの変数名を変える改造は必須ではありません。 変数の名前はあくまでもプログラム上のもので、原則的にゲーム画面に表示されたりすることはないからです。 hiScoreLabel という名前のまま、残り時間を扱うプログラムにすることも、技術的には可能です。

ただしこれは一般的にはおすすめしません。ここまで見てきたとおり、プログラムを理解するには変数名やファイル名が大きな手がかりになります。 常に意味のある、処理の内容を反映した名前をつけることをおすすめします。

固定の文字列を書き換える ​

二つ目の作業もごく単純です。 単に先ほどの "HI " の部分を "TIME " に書き換えるだけです。

js
 update() {
     this.scoreLabel.text = "SCORE " + ("00000" + Global_1.Global.gameCore.player.score).slice(-6);
     this.scoreLabel.invalidate();
     this.timeLabel.text = "HI " + ("00000" + Global_1.Global.hiScore).slice(-6);
     this.timeLabel.text = "TIME " + ("00000" + Global_1.Global.hiScore).slice(-6);
     this.timeLabel.invalidate();
     ...
 }

ハイスコアの代わりに残り時間を表示させる ​

さて三つ目の作業が本題でしょう。 ここまでの変更と合わせると、以下のようなコードになれば良さそうです。

js
this.timeLabel.text = "TIME " + (残り時間);

この (残り時間) の部分に何を書けばいいのか、考える必要があります。

ゲーム画面をよく見ると画面の上側に、時間経過に応じて減っていくゲージがあります。 ということは、このゲーム内のどこかで残り時間は既に求めているはずです。

残り時間ゲージ

この処理を探してみましょう。 ただしハイスコアの HI と異なり、画面上には検索できそうな文字列の手掛かりはありません。 仕方がないので変数名を予想して検索することにします。 「残り時間」は英語で "remaining time" や "time left" なので time と remain 、 あるいは「経過時間」の "elapsed time" から elapse などが候補になりそうです。

time で検索してみると、7 つの .js ファイルが該当しました。

  • bootScene.js
  • GameCore.js
  • gameScene.js
  • GammaEnemy.js
  • Player.js
  • PlayerStatus.js
  • titleScene.js

このうち、PlayerStatus.js は今書き換えた timeLabel が見つかっているだけです。 また titleScene.js はタイトル画面、 bootScene.js の "boot" はきっと起動直後のことでしょうから関係なさそうです。 GammaEnemy.js も "enemy" つまり敵キャラ関係の処理でしょうから、ゲーム全体に関わる残り時間の処理ではなさそうです。

残ったファイルを実際に開いて、 time が現れる箇所を探すと、次のような状況でした。

ファイル名主な "time" の出現箇所
GameCore.jsGameCore.MAX_PLAYTIME = 60;
gameScene.jstimeGauge, timeGaugeWidth
Player.jseffectTime

GameCore.js と gameScene.js が「当たり」のようです。 MAX_PLAYTIME が最大プレイ時間 60 秒を表しているのでしょう。 そのものずばり timeGauge (時間ゲージ) は、画面上の残り時間ゲージを表していそうです。

gameScene.js を開いて、 timeGauge に関わるコードを確認すると、次のようになっています。

js
function createGameScene() {
    ...
    scene.onLoad.add(() => {
        ...
        const timeGaugeWidth = g.game.width;
        const timeGauge = new g.FilledRect({
            scene: scene,
            width: timeGaugeWidth,
            height: 4,
            cssColor: "Red"
        });
        scene.append(timeGauge);
        ...
        scene.onUpdate.add(() => {
            ...
            const maxPlayTimeInFPS = GameCore_1.GameCore.MAX_PLAYTIME * g.game.fps;
            const remainTimeRate = Math.max(maxPlayTimeInFPS - Global_1.Global.gameCore.cntr, 0) / maxPlayTimeInFPS;
            timeGauge.width = timeGaugeWidth * remainTimeRate;
            timeGauge.modified();
            ...
        });
        ...
    });
    ...
});

残り時間ゲージは時間経過で 幅 (width) が縮んでいくことからすると、width に代入するこの部分が重要そうです。

js
timeGauge.width = timeGaugeWidth * remainTimeRate;

* は乗算の演算子です (A * B は A と B の積) 。 積の左辺の timeGaugeWidth は、上の方で以下のように定義されています。

js
const timeGaugeWidth = g.game.width;

g.game.width は Akashic Engine の提供する変数で、「ゲーム画面の幅 (ピクセル数)」を表します。 また timeGaugeWidth は const で定義されているので、後から代入で変更されることはありません。 つまりこの timeGaugeWidth は「時間ゲージの基本の幅」という意味合いの定数なのでしょう。

一方、積の右辺の remainTimeRate は、名前からすると「残り時間 (ramining time) の割合 (rate)」でしょう。 つまり timeGauge.width に代入する文は次のような処理だと推測できます。

js
// (時間ゲージの幅) = (時間ゲージの基本の幅) * (残り時間の割合)
timeGauge.width = timeGaugeWidth * remainTimeRate;

これは時間経過でゲージが縮んでいく挙動とも整合的です。

(残り時間の割合) は、割合なので (残り時間) / (最大プレイ時間) のような値だと推測できます。 実際 remainTimeRate はこのすぐ上で次のように定義されています。

js
const remainTimeRate = Math.max(maxPlayTimeInFPS - Global_1.Global.gameCore.cntr, 0) / maxPlayTimeInFPS;

つまり、次のような対応関係が読み取れます。

意味対応する部分
(残り時間?)Math.max(maxPlayTimeInFPS - Global_1.Global.gameCore.cntr, 0)
(最大プレイ時間?)maxPlayTimeInFPS

さて、これでやっと目的の (残り時間) らしきものが見つかりました。 すぐ近くで定義されている maxPlayTimeInFPS と一緒に、この式をコピーして PlayerStatus.js を書き換えましょう。

js
const maxPlayTimeInFPS = GameCore_1.GameCore.MAX_PLAYTIME * g.game.fps;

INFO

実際には、この (残り時間?) はまだ目的の (残り時間) そのものではありません。 しかしいったん画面に反映して動くところまでこの値で進めてみましょう。

PlayerStatus.js を次のように変更します。

js
class PlayerStatus {
    ...
    update() {
        this.scoreLabel.text = "SCORE " + ("00000" + Global_1.Global.gameCore.player.score).slice(-6);
        this.scoreLabel.invalidate();
        const maxPlayTimeInFPS = GameCore_1.GameCore.MAX_PLAYTIME * g.game.fps;
        this.timeLabel.text = "TIME " + ("00000" + Global_1.Global.hiScore).slice(-6);
        this.timeLabel.text = "TIME " + Math.max(maxPlayTimeInFPS - Global_1.Global.gameCore.cntr, 0);
        this.timeLabel.invalidate();
        ...
    }
    ...
}

動作確認とデバッグ ​

ここで動作確認してみましょう。akashic sandbox を実行します。 するとタイトル画面を終えてメインゲームが始まった瞬間、次のような画面で固まってしまいました。

画面右下にエラーが表示された状態

akashic sandbox では、ゲーム実行中にエラーが発生すると右下にエラーメッセージを表示します。 ここでは「GameCore_1 is not defined」(GameCore_1 は定義されていない) と表示されているので、 定義していない変数を使ってしまったようだと分かります。

先ほどまで動いていたゲームなので、原因は間違いなく今書き換えた部分にあるはずです。

改造部分で GameCore_1 が登場するのは、先ほど PlayerStatus.js にコピーしてきたこの箇所だけでした。

js
class PlayerStatus {
    ...
    update() {
        this.scoreLabel.text = "SCORE " + ("00000" + Global_1.Global.gameCore.player.score).slice(-6);
        this.scoreLabel.invalidate();
        const maxPlayTimeInFPS = GameCore_1.GameCore.MAX_PLAYTIME * g.game.fps;
        this.timeLabel.text = "TIME " + Math.max(maxPlayTimeInFPS - Global_1.Global.gameCore.cntr, 0);
        this.timeLabel.invalidate();
        ...
    }
    ...
}

このコードは違うファイルからコピーしてきたので、このファイルには GameCore_1 という変数がありません。 これが原因でしょう。コピー元の gameScene.js を確認すると、次のような定義が見つかります。

js
const GameCore_1 = require("./GameCore");

require() は、「指定された .js ファイルを読み込み、その結果を取得する」というような機能を持つ関数です。 拡張子は省略できるので "./GameCore" とだけ書かれていますが、この記述で "./GameCore.js" を読み込みます。

PlayerStatus.js でも、ファイル冒頭に require() している行が並んでいるので、そこに同じ行を追加しましょう。

js
bject.defineProperty(exports, "__esModule", { value: true });
exports.PlayerStatus = void 0;
const Global_1 = require("./Global");
const Player_1 = require("./Player");
const GameCore_1 = require("./GameCore");
class PlayerStatus {
    constructor() {

TIP

require() に与えている引数の ./ の部分は、「このファイルと同じフォルダ」を意味する表記です。 つまり "./GameCore.js" は「このファイルと同じフォルダにある GameCore.js 」という意味です。

今回はコピー元の gameScene.js もコピー先の PlayerStatus.js も同じフォルダにあるので、 コピー先でも require("./GameCore.js") のままで正しく動作します。 違うフォルダにコピーした場合は、引数を書き換える必要があります。

残り時間部分の修正 ​

akashic sandbox で改めて動作確認しましょう。 今度は正しくゲームを始めることができました。 画面右上も TIME 表記に変わっていて、時間経過で数字が減っていくのがわかります。

しかしまだ気になる点があります。残り時間の数字が大きすぎますし、数字が減るペースも速すぎます。

この数字は先ほど (残り時間?) としていた値です。改めて式を確認しましょう。

意味対応する部分
(残り時間?)Math.max(maxPlayTimeInFPS - Global_1.Global.gameCore.cntr, 0)
(最大プレイ時間?)maxPlayTimeInFPS

まずこの値は Math.max(値A, 値B) という形になっています。 Math.max() は与えられた引数の中で一番大きい値を返す関数です。 ここでは第二引数が 0 になっているので、「第一引数が 0 以上ならその値、0 未満なら 0」という意味になります。 要は 0 が下限になるように値を制限しているだけです。

TIP Math について

Math は JavaScript の仕様で定義された値で、max() の他にも数学関連の関数や定数を提供しています。 最小値を返す Math.min() や、三角関数 sin() の値を求める Math.sin(), 円周率 π の近似値 Math.PI などが利用できます。

つまり (残り時間?) の本体は、 Math.max() の第一引数として書かれている maxPlayTimeInFPS - Global_1.Global.gameCore.cntr の部分です。 この減算の式が「残り時間」であるなら、それは最大プレイ時間から現在のプレイ時間を引くものでしょう。

js
// (最大プレイ時間?) - (現在のプレイ時間?)
maxPlayTimeInFPS - Global_1.Global.gameCore.cntr;

しかし maxPlayTimeInFPS という名前が示唆するとおり、この式の単位は「秒」ではなく「フレーム数」になっています。 これは定義から確認できます。

js
const maxPlayTimeInFPS = GameCore_1.GameCore.MAX_PLAYTIME * g.game.fps;

MAX_PLAYTIME は先ほど見た通り、60 を代入する箇所が GameCore.js にありました。単位は秒でしょう。 g.game.fps は Akashic Engine の提供する変数で、「(このゲームの) 1 秒あたりのフレーム数 (FPS)」を表します。 その積は明らかに「フレーム数」です。

TIP フレームと FPS

ゲームプログラムは通常、一定速度で画面の描画を繰り返しています。 また描画と同じ回数だけ、ゲームロジックの処理 (ボタンが押された時の処理や、敵との衝突判定、衝突した時の処理など) も行います。 この一回分の処理 (と描画) を行う時間を、1 フレーム と呼びます。フレームはゲーム内の時間の最小単位です。

多くのゲームでは、1 フレームは 1/60 秒です。つまり 1 秒間に 60 フレーム分の処理を行います。 60 は固定ではなく、より大きい値を設定で選べるゲームもありますし、逆にカジュアルなゲームでは 30 ということもあります。 この「1 秒あたりのフレーム数」を FPS (frame per seconds) と呼びます。

Akashic Engine のゲームでは、設定ファイル game.json の "fps" の項目で FPS を指定できます。 ここで改造している Galaxy Wars では、 30 になっています。

この maxPlayTimeInFPS から引かれている Global_1.Global.gameCore.cntr も、 減算した結果も、単位は「フレーム数」ということになります。 「残り時間」にしては大きすぎる値だったのはこのためです。 ここまで (残り時間?) と書いてきたのは、正しくは (残りフレーム数) でした。

フレーム数を秒に換算するには、単に FPS で割るだけです。つまり (残り時間) は次の式で求まります。

js
// ((全体フレーム数) - (経過フレーム数)) / FPS
(maxPlayTimeInFPS - Global_1.Global.gameCore.cntr) / g.game.fps

ただし maxPlayTimeInFPS は GameCore_1.GameCore.MAX_PLAYTIME に g.game.fps を掛けた値だったので、無駄を省くと次のように書けます。

js
// (全体プレイ時間) - ((経過フレーム数) / FPS)
GameCore_1.GameCore.MAX_PLAYTIME - Global_1.Global.gameCore.cntr / g.game.fps

さらに、この式は除算 / が入っているので整数にならないことがあります (0.33333333... など)。 画面上の残り時間は整数で表示したいので、小数部を切り捨てる関数 Math.floor() を使います。

js
// Math.floor((全体プレイ時間) - ((経過フレーム数) / FPS))
Math.floor(GameCore_1.GameCore.MAX_PLAYTIME - Global_1.Global.gameCore.cntr / g.game.fps)

これを改めて PlayerStatus.js に反映しましょう。

js
class PlayerStatus {
    ...
    update() {
        this.scoreLabel.text = "SCORE " + ("00000" + Global_1.Global.gameCore.player.score).slice(-6);
        this.scoreLabel.invalidate();
        const maxPlayTimeInFPS = GameCore_1.GameCore.MAX_PLAYTIME * g.game.fps;
        this.timeLabel.text = "TIME " + Math.max(maxPlayTimeInFPS - Global_1.Global.gameCore.cntr, 0);
        this.timeLabel.text = "TIME " + Math.max(Math.floor(GameCore_1.GameCore.MAX_PLAYTIME - Global_1.Global.gameCore.cntr / g.game.fps), 0);
        this.timeLabel.invalidate();
        ...
    }
    ...
}

利用箇所のなくなった変数 maxPlayTimeInFPS の定義も削除しています。

改めて akashic sandbox で実行すると、次のような表示になります。

これで「残り時間」を表示することができました。

解説 ​

timeLabel とエンティティ ​

今回使った this.timeLabel は「.text に文字列を代入するとそれがゲーム画面に反映される」ものになっていました。 これが何なのか、定義箇所を見てみましょう。

先ほどから改造している PlayerStatus.js には次のようなコードがあります。

js
class PlayerStatus {
  constructor() {
    ...
    this.timeLabel = new g.Label({
        scene: Global_1.Global.gameCore.scene,
        text: "",
        font: Global_1.Global.bmpFont,
        fontSize: 16,
        x: g.game.width - (16 * 9 + 4), y: 4
    });
    ...
  }
}

代入されているのは new g.Label(...) した値だとわかります。

new は JavaScript の機能で、

js
new クラス(引数)

の形式で書くと、その クラス のインスタンスを新たに作る、という意味になります。

TIP クラスとインスタンス

クラス と インスタンス は、 JavaScript に限らないプログラミング用語です。 その詳細はここでは書ききれないのですが、しばしば次のように説明されます。

用語意味合い
関数処理をひとまとめにしたもの
インスタンスデータとそれを操作する関数をひとまとめにしたもの
クラスインスタンスを作るための雛形・テンプレート

「データとそれを操作する関数」と書くと抽象的ですが、たとえば「RPG の街のキャラクター」を考えるとわかりやすいかもしれません。 RPG のキャラクターには現在位置という「データ」があるはずです。 キャラクターは街の中を歩くので、現在位置を変更する (移動する) 「関数」も必要でしょう。 この時、データと関数をばらばらに扱うより、「キャラクター」という単位でまとめてしまおう、 というのがクラスやインスタンスを考えるモチベーションです。

そうしてデータと関数をまとめたものがインスタンスで、クラスはその雛形・テンプレートのようなものだと考えてください。 上の例で言えば、まさに「RPG の街のキャラクター」をクラスとしてプログラムすることができます。 この場合インスタンスは、実際に登場する各キャラクターです。 各キャラクターというインスタンスは、同じ「RPG の街のキャラクター」という雛形から生成され、 しかしそれぞれが異なるキャラ画像・現在位置・話す内容などのデータを持ちます。

Akashic Engine の機能の一部は、クラスとして提供されています。

ここでクラスとして指定されている g.Label は (g. で始まることから分かるとおり) Akashic Engine の機能で、「文字列を表示するエンティティ」です。

Akashic Engine では「画面に表示するもの」を エンティティ と呼んでいて、 g.Label はその一つです。 他には画像を表示する g.Sprite, パラパラアニメを表示するための g.FrameSprite や、 単に複数のエンティティをまとめるための g.E などがあります。

残り時間のリアルタイム更新 ​

今回の改造は、最終的にほぼ this.timeLabel.text に代入する値を変えただけのものになりました。 動作確認でわかる通り、これだけで「残り時間表示がリアルタイムに更新される」処理になっています。 特に「残り時間の変化」を検出しているわけでもないのに、なぜこれでリアルタイムに反映されるのか、不思議に思われるかもしれません。

これは今回変更した PlayerStatus.js のコード、 class PlayerStatus の update() 部分が都合よく「毎フレーム必ず実行される処理」になっていたためです。 残り時間 (の秒数) が変化していてもしていなくても、毎フレーム必ず表示を更新するので、結果的にリアルタイムで残り時間が反映されているというわけです。

Akashic Engine を含め大抵のゲームエンジンには「1 フレーム分の時間が経過する度に何か処理を実行する」機能があります。 もし PlayerStatus.js に都合のよい箇所がなければ、この機能を使って毎フレーム処理させるところから書く必要がありました。

残り時間ゲージの演出を加える ​

残り時間の値がわかるようになったので、もう一つ別のエンティティの紹介を兼ねて、残り時間ゲージの演出も少し変えてみましょう。

残り時間がわずかなことにプレイヤーに伝わりやすいよう、

  • 初期状態ではゲージを緑にする
  • 残り時間が少なくなったら白に変え、さらに明滅する

ようにしてみます。

先ほどの改造で探したとおり、時間ゲージの処理は gameScene.js にありました。 変数 timeGauge が現れる箇所を中心に、改めて抜き出すと次のようになります。

js
function createGameScene() {
    const scene = new g.Scene({ game: g.game });
    scene.onLoad.add(() => {
        ...
        const timeGaugeWidth = g.game.width;
        const timeGauge = new g.FilledRect({
            scene: scene,
            width: timeGaugeWidth,
            height: 4,
            cssColor: "Red"
        });
        scene.append(timeGauge);
        ...
        scene.onUpdate.add(() => {
            ...
            timeGauge.width = timeGaugeWidth * remainTimeRate;
            timeGauge.modified();
            ...
        });
        ...
    });
}

初期状態を緑色にする ​

timeGauge の代入箇所を見ると、この時間ゲージの実態が g.FilledRect だとわかります。

js
        const timeGauge = new g.FilledRect({
            scene: scene,
            width: timeGaugeWidth,
            height: 4,
            cssColor: "Red"
        });

g.FilledRect は Akashic Engine の提供するエンティティの一種です。 名前のとおり、指定した色で塗りつぶされた (filled) 矩形 (rect) を描画するエンティティになっています。

TIP

本格的なゲームでは、画面に表示されるものは大抵が画像か文字になるので、 g.FilledRect はそれほど多用されるエンティティではありません。 今回のようなシンプルなゲージや、背景を半透明の黒で塗りつぶしたい場合などに登場します。

cssColor: "Red" という部分で、赤色が指定されているのだろうと推測できます。

g.FilledRect の cssColor には、CSS の色指定と同じ形で色を与えることができます。 これには "Red" のような色名の他、 rgb(0, 255, 0) のような RGB 値での指定が含まれます。

背景に合わせて少し暗めの緑色にしておきます。

js
        const timeGauge = new g.FilledRect({
            scene: scene,
            width: timeGaugeWidth,
            height: 4,
            cssColor: "Red"
            cssColor: "rgb(0, 160, 0)"
        });

TIP RGB と CSS の色指定

RGB は、光の三原色 (赤 red, 緑 green, 青 blue) の頭文字をとったものです。 コンピュータではしばしばこの三つの成分の組で任意の色を指定します。

CSS の色指定の場合、R, G, B それぞれ 0 以上 255 以下の値をとることができます。 rgb(赤成分, 緑成分, 青成分) で指定するので、たとえば rgb(255, 255, 255) は白に、 rgb(128, 0, 255) は青みの強い紫になります。

残り時間が少ない時の演出を加える ​

残り時間が少なくなった時の演出は、時間ゲージが縮んでいく処理と同じ場所に書けばよさそうです。 この処理は毎フレーム実行されているので、都度チェックして処理すればよいはずです。

js
        scene.onUpdate.add(() => {
            ...
            timeGauge.width = timeGaugeWidth * remainTimeRate;
            timeGauge.modified();
            ...
        });

残り時間の割合 remainTimeRate が既に求められているので、これが 0.2 (20%) を切ったら色を変え、0.1 (10%) を切ったら明滅するようにします。

js
        scene.onUpdate.add(() => {
            ...
            const maxPlayTimeInFPS = GameCore_1.GameCore.MAX_PLAYTIME * g.game.fps;
            const remainTimeRate = Math.max(maxPlayTimeInFPS - Global_1.Global.gameCore.cntr, 0) / maxPlayTimeInFPS;
            if (remainTimeRate < 0.1) {
              // 残り時間が 10% 未満なら明滅させる//
              timeGauge.opacity = Math.sin(0.1 * Math.PI * Global_1.Global.gameCore.cntr) / 4 + 0.75;
            } else if (remainTimeRate < 0.2) {
              // 残り時間が 20% 未満なら白に変える//
              timeGauge.cssColor = "white";
            }
            timeGauge.width = timeGaugeWidth * remainTimeRate;
            timeGauge.modified();
            ...
        });

if, else if は 改造: 回復アイテムを強化してみる の「条件分岐: if...else 文」でも登場したので、紹介が必要なのは明滅処理の部分でしょう。

js
// 残り時間が 10% 未満なら明滅させる
timeGauge.opacity = Math.sin(0.1 * Math.PI * Global_1.Global.gameCore.cntr) / 4 + 0.75;

timeGauge.opacity の .opacity は、エンティティ共通の機能で、不透明度を表します。 0 を代入すると透明に、0.5 なら半透明、1 なら完全不透明になります。 この値を毎フレーム変化させて、不透明と半透明の間をいったりきたりすることで、明滅を表現しています。

代入されている左辺の式は次のような形になっています。

js
Math.sin(...) / 4 + 0.75

Math.sin() は三角関数 sin(θ) を求める関数です。 sin(θ) は引数 θ の値に応じて -1 から 1 の範囲をとる関数で、θ を増やしていくと以下のように振動します。

横軸 θ, 縦軸 sin(θ) のグラフ

この sin(θ) である Math.sin() に、 Global_1.Global.gameCore.cntr (経過フレーム数) を含む値を与えることで、 時間経過で値が振動するようにしています。そしてこの振動を不透明度として使ってゲージを明滅させているというわけです。

Math.sin(...) / 4 + 0.75 の後半の / 4 + 0.75 の部分は、不透明度に代入するために値を調整しています。 sin(θ) は -1 以上 1 以下の値をとるので、4 で割ると -0.25 以上 0.25 以下の値になります。 これに 0.75 を足すと、0.5 以上 1.0 以下の範囲で振動する値になります。

実際に動かしてみると、ゲージが白くなり明滅する様子が確認できます。

TIP 処理の無駄

この改造では、残り時間の割合が 0.1 以上 0.2 未満の間、毎フレームずっとゲージの色を白に設定しつづけます。 実害はありませんが、これは効率のいい処理ではありません。色は一回変えればそれで十分だからです。

たとえばゲーム開始直後に「48 秒後に一回だけ実行する処理」を登録して、 その処理の中でゲージの色を変えることができれば、その方が効率的です。 実際 Akashic Engine はそのようなタイマー処理の機能も提供しています。

ここでは簡単のため、多少の効率は気にせず改造の単純さを優先しています。

次は? ​

サンプルゲーム Galaxy Wars の改造を通して、プログラミングの基本的な概念と、Akashic Engine の機能をいくつか紹介しました。 より本格的にニコ生ゲームを作成するための文書としては、以下も参照してみてください。