エフアンダーバー

個人開発の記録

【RPGツクールMV】イマドキのプラグイン記述術

ツクールMVが発売されたのが2015年の年末だそうで、その頃からはずいぶんWeb周りも進化しました。 ツクール自体もアップデートが繰り返され、使える機能もいろいろ増えたので、 プラグインの『イマドキ』の書き方についてまとめておきます。

ツクールMZが発表されてツクールMV自体がすでに『イマドキ』じゃないとの話もありますが、とりあえずそれは置いておきます。 また、『イマドキ』のWebフロントエンドの三種の神器、 「パッケージマネージャ」「モジュールバンドラ」「altJS」には一切触れませんが、とりあえずそれも置いておきます。

『イマドキ』とは……。

概要

ツクールMVの発売当初と今で大きく変わった点としては次の二つかなと思います。

  • プラグインパラメータの指定方法
  • JavaScriptのバージョン

特にJavaScriptはちょうどツクールMVが発売された年の前後で大きく記法が変わりました。 発売時のツクールで使用されていたバージョンはES5と呼ばれるもので、 その次のバージョンがES2015(ES6)、以降、ES2016、ES2017、と毎年更新されています。 中でもES2015は最も変化が大きかったバージョンで、 これによって今まで普通に使っていたvarなどはほぼ使われなくなりました。

ツクールで使えるJavaScriptの機能はデプロイ方法によって違いますが、 現在各ブラウザのJavaScript対応状況はツクールエディタ(NW.js)の遥か先を行っているので、 基本的にツクールエディタで動く機能はブラウザでも動きます。 現在のツクールエディタ(ver1.6.2)は概ねES2018相当なので、この辺りまでは使って問題ないと思われます。

この記事ではJavaScriptバージョンの更新に伴う記述の変化は扱いますが、JavaScriptの機能自体の解説はあまりしません。 この手の話はバージョン名で検索すれば山ほどヒットするので、知らない方は他所の記事を参照してください。

記事の以降は、各内容ごとに書き方をまとめていきます。

プラグインコメント

プラグインコメントは大枠はそれほど変化していませんが、ver1.5.0でプラグインパラメータに型が指定可能になりました。

/*:ja
 * @plugindesc (プラグインの説明文)
 * @author (作者名)
 *
 * @help
 * (プラグインのヘルプ)
 *
 * @param (パラメータのID)
 * @type (パラメータの型)
 * @default (パラメータのデフォルト値)
 * @text (パラメータの表示名)
 * @desc (パラメータの説明文)
 * 
 * ...
 */

パラメータに型を指定すると、その型に応じた選択方法で選択が可能になります。 使用できる型は次の通りです(詳細は公式のお知らせを参照してください)。

入力対象 フォーマット 追加オプション
string 単行文字列 入力値文字列
number 数値 JSON互換数値 @min 最小値
@max 最大値
@decimals 小数部桁数
boolean 真偽値 JSON互換真偽値 @on 真値テキスト
@off 偽値テキスト
select 選択値 対応値文字列 @option 選択肢表示名
@value 選択肢対応値
combo 選択可能値 入力値文字列 @option 選択肢
file ファイル名 入力値文字列 @dir ディレクトリ
animation アニメーション ID文字列
actor アクター ID文字列
class 職業 ID文字列
skill スキル ID文字列
item アイテム ID文字列
weapon 武器 ID文字列
armor 防具 ID文字列
enemy 敵キャラ ID文字列
troop 敵グループ ID文字列
state ステート ID文字列
tileset タイルセット ID文字列
common_event コモンイベント ID文字列
switch スイッチ ID文字列
variable 変数 ID文字列
note 複数行文字列 JSON文字列
X[] 配列 JSON配列
struct<X> 構造体 JSONオブジェクト

配列は、例えば、number[]とすることでnumberの値を複数入力できるようになります。

構造体は別のブロックコメントによって構造体を定義した上で、 そこに指定した名前と一致する名前を<>内に記述します。 例えば、Fooという名前の構造体を指定するには次のように定義して、struct<Foo>と書きます。

/*~struct~Foo:ja
 * @param (パラメータID)
 * @type (パラメータ型)
 * @default (パラメータのデフォルト値)
 * @text (パラメータの表示名)
 * @desc (パラメータの説明文)
 * 
 * ...
 */

アンチパターン

プラグインコメントでよく見られるアンチパターンについてです。

見出し

パラメータごとに一行ずつ表示されることを利用して、 実際には使用しないパラメータを見出しのように使っているケースが見られます。

/*:ja
 * @param ~戦闘関係~
 * @desc このパラメータは見出しです。
 *
 * @param 戦闘モード
 * @type select
 * ....
 */

また、この亜種として使用しないパラメータでグループ化しているケースもよくあります。

/*:ja
 * @param ~戦闘関係~
 * @desc このパラメータは見出しです。
 *
 * @param 戦闘モード
 * @parent ~戦闘関係~
 * ....
 */

パラメータはプラグインの挙動を設定するデータであって、 見た目を整えるためのものではないのでこういった使い方をすべきではないです。 パラメータの表示方法が変更されないとも限らないですし、無用な混乱を招きます。

見出しをつけなければならないほど、パラメータが増えてしまった場合には二つの対処法があります。 ひとつは、プラグインを複数のプラグインに分割すること、 もうひとつは、構造体によって整理することです。 どちらを使うべきかは状況によりますが、傾向として巨大すぎるプラグインが多い印象です。 まずはひとつのプラグインに機能を詰め込み過ぎていないかどうか考えてみるといいと思います。

矛盾するライセンス

ツクールMVのプラグインのライセンスにはよくMITライセンスが使われますが、 MITライセンスを理解しないままにとりあえず書いておいたような記述が散見されます。

例えば、次のような記述がMITライセンスに併記されていることがあります。

  • 「利用に一切の条件はありません」
  • 「著作権表示は必要ありません」
  • 「プラグイン自体の再配布は禁止します」

もちろんこれらが「ただし、~」のようにMITライセンスを打ち消すように書かれていればいいのですが、 「つまり、~」のようにまるでMITライセンスの説明かのように書かれていることがあります。 これはMITライセンスの記述と矛盾します。

当たり前のことですが、MITライセンスの意味を理解していないのであれば、 MITライセンスだと書いてはいけません。 必ず何かのライセンスを適用しなければならないわけではないので、 わからなければ自分の言葉で利用規約を書きましょう。 許可内容を理解していないよりは幾分マシなはずです。

MITライセンスであるとだけ書くことが不親切だと思うのであれば、 説明よりもライセンス文の記述が優先されることを明記しておくといいです。 しかし、多くのプラグインがMITライセンスであることを考えると、 「わからなければググって」くらいでもいい気がします。

MITライセンスよりもっと緩くしたい場合には、可能な限り著作権を放棄するCC0というのもあります。

全体構成

プラグインの全体構成として、自分は次のように書いています。

/*:ja
 * ...
 */

"use strict";

{
    // プラグインの処理
}

以前はIIFE(即時関数)が必須でしたが、 constletによってブロックスコープが利用可能になったためいらなくなりました。 もちろんミスを防ぐために書いてもいいとは思います。

残念ながらツクールではモジュールが利用できないため、Strictモード指定は未だに必須です。 IIFEで囲む場合には関数単位で指定してもいいと思います。

(() => {
    "use strict";

    // プラグインの処理
})();

アンチパターン

プラグインの全体構成というか、テンプレ的記述としてよく見られるアンチパターンについてです。

プラグインの有無の判定

それ自体がアンチパターンというわけではないんですが、 少しコメントしておきたいのが次のコードです。

var Imported = Imported || {};
Imported.MyAwesomePlugin = true;

いくつかあるんですが、簡単な内容から順にコメントしていきます。

まず、このコードはプラグインの先頭によく書かれますが、 "use strict";より前に書くとStrictモードにならないので注意してください。

次に、このコードはvarを単純にconstletに置き換えてもうまく動きません。

このコードでは変数を宣言するとともに、その変数を初期化処理に使用しています。 varではこのような書き方も許されましたが、constletでは未初期化変数の参照としてエラーになります。 加えて、varでは同スコープ内での同名変数の宣言が許可されましたが、constletでは許可されません。 そのため、複数のプラグインでグローバルスコープに同様の宣言を記述すると、重複変数の宣言としてエラーになります。

varと同等の定義をするには次のように記述する必要があります。

// this は window でも可
this.Imported = this.Imported || {};
this.Imported.MyAwesomePlugin = true;

// ワンライナー
this.Imported = Object.assign(this.Imported || {}, { MyAwesomePlugin: true });

最後に、利用側の話になるのですが、競合を解決するための手段としてこの変数を参照しないでください。

if (Imported["ConflictTarget"]) {
    // ○○を入れている場合には××なので△△するようにする。

    // ...
}

こういったケースごとに分岐処理を記述していると、プラグインの本来の処理がとても読みづらくなります。 また、こうして参照したプラグインすべてがそのプラグインの依存対象となるため、 とても複雑な依存関係が構築されてしまい、管理もしづらくなっていきます。

プラグイン間で競合が発生してしまい、その解決策を提供したい場合には新たにアダプタプラグインを製作するといいと思います。 つまり、プラグインAとプラグインBが競合を起こした場合、それらを解決するプラグインCを導入します。 大抵の場合は片方のプラグインをもう片方のプラグインに対応させる形になります。

競合を解決するケースを除くと、 他のプラグインを参照するケースというのはオプションプラグインを製作するときくらいだと思われるので、 オプションプラグインを想定していないケースではそもそもこのような変数を定義しないのもいいと思います。

ちなみに、プラグインの有無を確認するために、上記のようなオブジェクトを定義する必要は特にありません。 次のようなコードで十分だったりします。

// 定義側
const PLUGIN_IMPORTED_MY_AWESOME_PLUGIN = true;

// 確認側
if (typeof PLUGIN_IMPORTED_MY_AWESOME_PLUGIN !== 'undefined') {
    // ...
}

もっと言うと、他のプラグインを参照するケースというのはそのプラグイン独自の機能を利用する場合が多いため、 次のようなコードでもいいと思います。

if (Game_Actor.prototype.myAwesomeFunction) {
    // myAwesomeFunction を利用するコード
}

バージョンに関する誤解

そもそもコードとしてバージョンを記述すること自体それほど役に立つとは思いませんが、 バージョン関係で次のようなコードが散見されます。

// ケースA
Imported.MyAwesomePlugin = 1.23;

// ケースB
if (Utils.RPGMAKER_VERSION >= "1.2.3") { /* ... */ }

現在、バージョンの付け方はセマンティックバージョニングという方法がデファクトスタンダードとなっています。 必ずこれを採用しなければならないというわけではありませんが、例えば、ツクールのコアスクリプトもこれに従っています。

細かい部分を除くと、セマンティックバージョニングは"X.Y.Z"のように"."で区切られた三つの整数で表現されます。 Xはメジャーバージョンで、破壊的変更があった場合この数値を上げてYとZを0にリセットします (実際には破壊的変更かどうかではなく大きな変更があった場合に上げられることも多いです)。 Yはマイナーバージョンで、破壊的変更を伴わない機能追加が行われた場合にこの数値を上げてZを0にリセットします。 Zはパッチバージョンで、バグ修正が行われた場合にこの数値を上げます。

セマンティックバージョニング以外のバージョンの付け方も基本は同じで、大抵は複数の整数を"."で区切ります。 違うのは数字の数とそれらの意味くらいです。 これを念頭にあらためて上記のコードを見てみます。

ケースAではバージョンを区切る"."を小数点と勘違いしています。 何が問題かというと、1.99の後に2.00にせざるを得ず、各数字に特に意味がなくなってしまうことです。 単にバージョンの大小比較をしたいだけであれば整数で十分です。

ケースBではバージョンが上がるとコードが壊れてしまいます。 "1.9.X"の次のバージョンとして"2.0.0"を想像しているのだと思いますが、実際には"1.10.0"となる可能性が高いです (「いってんじ(ゅ)ってんれい(ぜろ)」と読みます)。 そうなると、文字列比較の"1.10.0" >= "1.2.3"falseなので、期待通りに動かなくなってしまいます。 というかよく考えると、メジャーバージョンでも同じことが起きるので、バージョンの構造とかあんまり関係ないですね。

セマンティックバージョニングのバージョンを正しくパースするには次のように書きます。

const [major, minor, patch] = Utils.RPGMAKER_VERSION.split(".").map(Number);
if (major > 1 || major === 1 && (minor > 2 || minor === 2 && patch >= 3)) { /* ... */ }

ツクールプラグインでは見たことがありませんが、 セマンティックバージョニングではパッチバージョンの後にハイフンを繋げて追加情報(β版かどうかなど)を加えることも可能なため、 厳密にやろうとするともう少し複雑になります。

パラメータのパース

プラグインパラメータの取得には次のコードを使います。

const PLUGIN_NAME = document.currentScript.src.split("/").pop().replace(/\.js$/, "");
const PARAMS = PluginManager.parameters(PLUGIN_NAME);

プラグインパラメータの取得にはプラグインのファイル名と同じ文字列が必要なため、 以前はプラグイン名をソースコード内にハードコーディングしていましたが、 Document.currentScriptが使用可能になったことで、 多少無理やりながらもファイル名を取得できるようになりました。

以前はハードコーディングした文字列と一致する必要があったためプラグイン名を変更できず、 プラグイン名が被らないように作者ごとのプリフィックスを付ける慣習がありましたが、 コードから取得することでリネームできるようになったため、これは特に必要なくなりました。 もちろん一種のアピールとして付けてもいいとは思います。

上記のコードは覚えやすいワンライナーで書いていますが、 もっとしっかりと書きたければ次のように書くこともできます。

const PLUGIN_NAME = document.currentScript.src.match(/\/([^\/]*)\.js$/)[1];
const PLUGIN_NAME = document.currentScript.src.replace(/^.*\/([^\/]*)\.js$/, "$1");

プラグインパラメータのパースの基本形は次の通りです。

const STRING = PARAMS["StringParam"];
const NUMBER = Number.parseFloat(PARAMS["NumberParam"]);
const INTEGER = Number.parseInt(PARAMS["IntegerParam"], 10);
const BOOLEAN = PARAMS["BooleanParam"] === 'true';

配列や構造体の場合には、上記の基本形とJSON.parseを組み合わせます。

const NUMBER_ARRAY = JSON.parse(PARAMS["ArrayParam"]).map(x => Number.parseInt(x, 10));

const parseMyStruct = s => JSON.parse(s, (key, value) => {
    switch (key) {
        case "foo": return Number.parseFloat(value);
        case "bar": return value === 'true';
        default: return value;
    }
});
const MY_STRUCT = parseMyStruct(PARAMS["StructParam"]);

また、独自形式での入力を前提としている場合には、ここで解析してしまうといいと思います。

const parseNaturalRange = s => {
    const matches = s.match(/^([1-9][0-9]*|0)\.\.([1-9][0-9]*|0)$/);
    if (matches !== null) {
        const start = Number.parseInt(matches[1], 10);
        const end = Number.parseInt(matches[2], 10);
        return start <= end ? { start, end } : undefined;
    } else {
        return undefined;
    }
};
const NATURAL_RANGE = parseNaturalRange(PARAMS["RangeParam"]);

上記のコードはすべて正しい形式でパラメータが設定されることを期待したコードなので、 もっとしっかりと書きたいのであれば、 例外時にわかりやすいエラーメッセージで落としてやるべきだと思います。

プラグインコメントとの対応を維持するのが大変なので、 自分は空欄時のデフォルト値は書きませんが(空欄を正しい設定とすべきでないという立場)、 もし書きたければ書いてもいいとは思います。 書き方はいろいろありますが、次のどちらかがいいんじゃないかと。

const INTEGER = Number.parseInt(PARAMS["IntegerParam"] || "99", 10);

const parseOr = (s, default_, fn) => s !== "" ? fn(s) : default_;
const INTEGER = parseOr(PARAMS["IntegerParam"], 99, s => Number.parseInt(s, 10));

アンチパターン

プラグインパラメータのパースでよく見られるアンチパターンについてです。

eval

コアスクリプトが使っているため、 ツクールプラグインでは比較的よく使われるevalですが、 一般的なプログラミングでは禁止されることもあるようなシロモノだったりします。

理由はいろいろありますが、次のようなものが大きいと言われています。

  • 脆弱性の原因となりやすい
  • デバッグがしづらい
  • 最適化の邪魔をする

要するに、人間にとっても解析ツールにとっても何が起こるのか推測しづらいのです。 任意のJavaScriptコードが実行できるので当たり前ですね。 他のユーザに対してなんでもできる環境を与えてしまえば脆弱性となりますし、 あらゆる可能性を考えないといけない状態はデバッグがしづらく、 何をしているのかわからないコードを最適化することはできません。 プログラミングでは適切な『制限』を与えることがとても大切です。

そんなわけで、evalを使う場合には最大限の注意を払い、 別の方法で容易に実現可能であればevalを使用しないようにする、というのが鉄則です。

簡単に書けるプラグインパラメータのパースに使用するようなものではありません。

const INTEGER = eval(PARAMS["IntegerParam"]);

JSON.parse(JSON.stringify(...))

いろいろ亜種はあるみたいですが、次のようなコードでパラメータを一括でパースしているのをよく見かけます。

const parseParams = params => JSON.parse(JSON.stringify(params, (_, value) => {
    try {
        return JSON.parse(value);
    } catch (e) {
        try {
            return eval(value);
        } catch (e) {
            return value;
        }
    }
}));
const ALL_PARAMS = parseParams(PARAMS);

かなり複雑なことをしているので一見すごいコードに見えるかもしれませんが、 実はやってはいけないことのオンパレードです (そもそもプログラミングにおいて複雑というのはそれ自体が欠点ですが)。

まず、このコードは@typestringfileselectcomboのパラメータに対して正しく動作しません。 何故かというと、これらのパラメータはどのような文字列をも正しい入力として受け入れる可能性があるためです。 もしJSONやJavaScriptのコードとして解釈できる文字列を渡されてしまうと、 JSON.parseevalで例外が発生しないため、解釈して評価された値になってしまいます。

例えば、[42].pngという名前のファイルを指定すると、 パラメータとしては"[42]"という文字列が渡されるため、 JSON.parse42という要素をひとつだけ持つ配列と解釈されてしまいます。 evalだけに引っかかるケースはよりひどくて、例えば、 パラメータとして"this"という文字列が渡された場合には、 循環構造を持つオブジェクトをJSONに変換しようとしているというエラーが表示されます。 なんのこっちゃという話ですよね。

この挙動だけでもすでに使えそうにないのですが、 プログラミングの作法的にもこのコードはいろいろとマズイです。

まず、evalについては前述の通りです。 実際、起こりうる可能性をしっかりと把握できていなかったために正しく動作していません。

次に、try-catchについてですが、正常な動作としてエラーを捕捉してはいけません。 catch節は期待していない結果が起こった場合の処理を書く場所です。 特定の失敗のみを想定してcatch節を書くと、 思わぬ原因(スタックオーバーフローなど)でエラーが投げられた際に想定外の挙動をしてしまいます。 基本的にcatch節はエラーのログをとるためか、 失敗した操作をなかったことにして正常系に戻すために使用するものです。

最後に、JSON.parseJSON.stringifyについて、これらは基本的にJSONとの相互変換のための関数です。 一応、コアスクリプトでも使われている通り、 stringifyしたものをparseすることでディープコピーの簡易実装として利用されることはありますが、 よく挙動を理解して使用しないとバグの原因となりやすいため、 本来の用途以外での使用はあまり推奨されません。 今回のケースで言うと、stringifyは単に再帰処理を書くためだけに使用されていて、 それを囲むparseに至ってはそのstringifyによるJSON文字列化という副作用を消すためだけに使用されています。 完全に使い方を誤った例だと思います。

ゲーム処理の記述

処理を上書きするコードについては残念ながらあまり変化がなく、次のようなコードになります。

const Scene_Map_update = Scene_Map.prototype.update;
Scene_Map.prototype.update = function () {
    Scene_Map_update.apply(this, arguments);

    // ...
};

コアスクリプトで基底型でしか定義されていないメソッドを派生型で定義したい場合には、 次のようなヘルパー関数を用意すると便利です。

const __base = (obj, prop) => {
    if (obj.hasOwnProperty(prop)) {
        return obj[prop];
    } else {
        const proto = Object.getPrototypeOf(obj);
        return function () { return proto[prop].apply(this, arguments); };
    }
};

const Game_Actor_addState = __base(Game_Actor.prototype, "addState");
Game_Actor.prototype.addState = function (stateId) {
    Game_Actor_addState.apply(this, arguments);

    // ...
};

また、新しくクラスを定義する場合に限ればclass構文が使用できます。

class MyAwesomeSprite extends Sprite {
    initialize(bitmap) {
        super.initialize(bitmap);

        // ...
    }
}

スプライトやウィンドウ、シーンなどでは利用できる機会も多いと思います。

アンチパターン

処理の記述でよく見られるアンチパターンについてです。

俺用オブジェクト

アンチパターンかどうか以前に意図がよくわからないのですが、 プラグイン用や作者用のオブジェクトをつくって、 とにかくそこにいろいろ詰め込まれていることがよくあります。

const MyAwesomePlugin = {};

MyAwesomePlugin.Param_AwesomeFeature = PARAMS["AwesomeFeature"] === 'true';

MyAwesomePlugin.Scene_Map_update = Scene_Map.prototype.update;
Scene_Map.prototype.update = function () { /* ... */ };

変数ではなくオブジェクトのプロパティにしてしまうと、 様々な参照方法が考えられるために解析が困難となり、 エディタによる支援機能が限定的になってしまいます。 また、解析が難しいということは実行時最適化の妨げにもなり得ます。

もし明確な意図がないのであれば、 オブジェクトのプロパティではなく変数として記憶しましょう。

Graphics.frameCountの誤用

経過フレーム数を表すGraphics.frameCountを利用して、次のような処理が書かれていることがあります。

// nフレームごとに処理する
if (Graphics.frameCount % n === 0) { /* ... */ }

残念ながらこれは正しく動きません。

何故かというと、Graphics.frameCountは更新フレーム数ではなく、描画フレーム数だからです。 詳細はSceneManager.updateMainの実装を見てもらえればわかると思いますが、 ツクールでは一度の描画に対して行われる状態更新の数が決まっていません。 そのため、Graphics.frameCountが一切増えずに何度も更新処理が呼び出されたり、 Graphics.frameCountが一度に2以上増えたりします。 上記のif文の処理は最悪の場合まったく呼び出されません(確率的にはあり得ないと思いますが)。

ちなみに、コアスクリプトではGraphics.frameCountをプレイ時間の算出に使用していますが、 現実の時間に近いのは描画フレーム数ではなく更新フレーム数なので間違っています。 まあ、常に処理落ちしているとかでなければそれほどずれないので実害はあまりありませんが。

あと、上記のコードで負荷軽減を図っているプラグインを見かけましたが、 フレーム毎の処理を間引いて負荷軽減するのは、 あらゆる最適化を尽くしても間に合わない場合の最終手段なので、 あまりやらない方がいいと思います。

プログラミング一般

ツクールとは直接関係ないけど書いておきたいJavaScript関連の話です。

初心者向けの基本的な話とES2015以降の便利機能についてです。

命名規則

プログラミング言語にはたいてい各要素(変数や型など)に対してどのような名前を付けるべきかというガイドラインがあり、 プログラマは言語ごとにそのルールに従ってプログラムを書きます。 JavaScriptは比較的このルールが緩い言語ですが、それでもルールを外れすぎた命名はマズイです。

よく見られるマズイ具体例として、スネークケースと呼ばれる小文字の単語を"_"で繋いだ形式の名前があります。

const my_integer_value = 42;

Game_Actor.prototype.do_something = function () { /* ... */ };

これは以前のツクールで使われていたRubyではよく使用する形式なのですが、JavaScriptではまず使われません。

JavaScriptで使用される大文字小文字の形式は主に次の三つです。

  • キャメルケース または ローワーキャメルケース "lowerCamelCase"
    • 先頭の単語はすべて小文字、以降の単語は一文字目だけ大文字
    • 変数名や関数名など
  • パスカルケース または アッパーキャメルケース "UpperCamelCase"
    • すべての単語の一文字目だけ大文字
    • コンストラクタ(型名)など
  • スクリーミングスネークケース "SCREAMING_SNAKE_CASE"
    • 単語すべてが大文字、"_"で連結
    • 定数など

とはいえ、初めに述べたように結構緩いので、 コアスクリプトが提供するクラスからして厳密にはこれに則っていなかったりします。 結局のところ最終的な判断は「空気読め」ということになっています。

等価演算子

JavaScriptにはイコールを意味する等価演算子が=====の二種類あります。 前者を単純に等価演算子、後者を厳密等価演算子といいます。 名前からわかる通り、前者は緩くそれっぽければ一致とするのに対して、後者はほぼ同じものでない限りは一致としません。

ではどう使い分けるかというと、ほぼすべて===を使います。 何故かというと、==の「それっぽければ一致」の正確な条件がとてもわかりづらく、バグの原因となりやすいからです。 これはノットイコールを意味する!=!==についても同じです。

一応、==!=が使われることがある唯一のケースとしてnullとの比較があります。

if (x == null) { /* ... */ }
if (x != null) { /* ... */ }

これはそれぞれ次のコードと等価です。

if (x === undefined || x === null) { /* ... */ }
if (x !== undefined && x !== null) { /* ... */ }

短く書けるので略記として使われることがありますが、 知っていなければ間違いなく挙動がわからないので、 後者のわかりやすい記述の方が好まれると思います。

const/let

ES2015になり、変数宣言のためのvarconstletによって置き換えられました。 これによりvarで変数宣言をすべきケースというのは一切なくなりました。 変数宣言は状況によりconstletを使い分けることになります。

constletの違いは再代入ができるか否かです。 constはできませんが、letはできます。 制限が強いほど考えるべき可能性が減ってプログラムが理解しやすくなるため、 基本的にはconstを使い、constで書きづらい場合のみletを使います。 自分の場合にはだいたい99%がconstで1%がletくらいの割合です。

var/let/constの書き換えのコードを示すので、 うまく書き換えられないときの参考にしてみてください。

/*** 条件分岐 ***/

// var
if (conditions) {
    var result = "TRUE";
} else {
    var result = "FALSE";
}

// let
let result;
if (conditions) {
    result = "TRUE";
} else {
    result = "FALSE";
}

// const #1
const check = conditions => {
    if (conditions) {
        return "TRUE";
    } else {
        return "FALSE";
    }
};
const result = check(conditions);

// const #2
const result = conditions ? "TRUE" : "FALSE";


/*** ループ ***/

// var
for (var i = 0, sum = 0; i < xs.length; i++) {
    sum += xs[i];
}

// let
let sum = 0;
for (let i = 0; i < xs.length; i++) {
    sum += xs[i];
}

// const #1
const rec = (xs, i, acc) => {
    if (i < xs.length) {
        return rec(xs, i + 1, acc + xs[i]);
    } else {
        return acc;
    }
};
const sum = rec(xs, 0, 0);

// const #2
const sum = xs.reduce((acc, x) => acc + x, 0);

WeakMap

ES2015で追加された機能はたくさんありますが、 そのうち普通のプログラミングではそれほど使われないけど、ツクールでは便利なやつを紹介しようかと。

まず一つ目はWeakMapです。

WeakMapはキーを弱参照(weak reference)として持つMapです。 弱参照というのはガベージコレクションによるメモリ回収を阻害しない参照のことです。 そのため、WeakMapがキーとして参照を保持していたとしても、 そのオブジェクトのメモリは解放される可能性があります。

WeakMapではキーに設定したオブジェクトがすでに存在しない場合に、 そのエントリにアクセスできないようにAPIが設計されています。 これによりWeakMapの内部実装では、 すでに存在しないオブジェクトをキーとするエントリを自動的に削除することができます (というか、そういう実装となっているはずです)。

このようなよくわからない挙動をするWeakMapですが、 何に使うかと言えば、オブジェクトに外部からプロパティを生やすのに使います。 オブジェクトをキーとして、値にプロパティ値を設定すれば、 まるでオブジェクトが新しいプロパティを持っているかのように扱うことができます。

const NEW_PROPS = new WeakMap();

const obj = {};

// obj.prop = value;
NEW_PROPS.set(obj, value);

// const value = obj.prop;
const value = NEW_PROPS.get(obj);

もし同じことをMapでしようとすると、 オブジェクトが不要になったことを何らかの方法で検知してエントリを削除しなければなりません。 でなければ、Mapがオブジェクトの参照を持ち続けるため、 Mapのメモリが解放されるまでオブジェクトのメモリも解放されず、メモリリークが発生してしまいます。

さて、なぜWeakMapがツクールで便利かという話ですが、 ツクールではGame_系のオブジェクトの多くが、そのプロパティをそのままセーブデータに保存してしまいます。 そのため、むやみやたらにGame_系オブジェクトにプロパティを生やすことはできません。 しかし、WeakMapを使えば、セーブしたくないプロパティをこれらに簡単に生やすことができます。 例えば、戦闘中のみ使用したいプロパティをGame_Battlerに生やして使用し、 戦闘終了時にWeakMapごと捨ててしまうといったことも可能になります。

Proxy

普段使わないけどツクールで便利なやつ、二つ目はProxyです。

Proxyはオブジェクトに対する操作を検知して、別の操作に置き換えることのできる機能です。 無理やりな方法で実現せざるを得ないことが多いツクールプラグインにおける最終兵器ですね。

const obj = { a: 1234, b: 5678 };
const proxy = new Proxy(obj, {
    get(target, prop, receiver) {
        return prop === "a" ? 42 : Reflect.get(target, prop, receiver);
    },
});

console.log(`${obj.a}, ${obj.b}`);     // 1234, 5678
console.log(`${proxy.a}, ${proxy.b}`); // 42, 5678

Game_ActionResultを書き換えてほしくないときとかに便利です。

Game_Battler.prototype.withoutActionResult = function () {
    const result = new Game_ActionResult();
    return new Proxy(this, {
        get(target, p, receiver) {
            return p === "_result" ? result : Reflect.get(target, p, receiver);
        },
    });
};

Game_Battler.prototype.addStateSilently = function (stateId) {
    this.withoutActionResult().addState(stateId);
};

ただし、基本的にコードが読みにくくなりますし、バグの原因にもなりやすいので濫用注意です。 あくまでも最終手段と考えてください。

polyfill

ツクールプラグインで使えるのはES2018相当までですが、 やっぱりES2019の機能も使いたい、ということでよく使うpolyfillを書いときます。 polyfillというのは機能を模倣するためのコードのことです。

if (!Object.fromEntries) {
    Object.fromEntries = iterable => {
        const entries = Array.isArray(iterable) ? iterable : [...iterable];
        return entries.reduce((result, [key, value]) => {
            result[typeof key === 'symbol' ? key : String(key)] = value;
            return result;
        }, {});
    };
}

if (!Array.prototype.flat) {
    Array.prototype.flat = function (depth) {
        const rec = (flattend, array, depth) => {
            for (const elem of array) {
                if (Array.isArray(elem) && depth > 0) {
                    rec(flattend, elem, depth - 1);
                } else {
                    flattend.push(elem);
                }
            }
            return flattend;
        };
        return rec([], this, depth !== undefined ? Math.floor(depth) : 1);
    };
}

if (!Array.prototype.flatMap) {
    Array.prototype.flatMap = function () {
        return Array.prototype.map.apply(this, arguments).flat(1);
    };
}

ユーティリティ

以前書いた、プラグインを書く際に役立つかもしれないコードを置いときます。

同値比較

いわゆるdeepEqualの実装です。 ただし、プロトタイプがObjectでないオブジェクトについては、同一の参照でない限りfalseを返します。 また、Symbolによるプロパティには対応していません。

Object.isを利用しているので、+0-0falseを返したり、NaN同士がtrueを返す点も場合によっては注意が必要かもしれません。

const semiDeepEqual = (a, b) => {
    const arrayEqual = (a, b, eq) => a.length === b.length && a.every((v, i) => eq(v, b[i]));
    const pojoEqual = (a, b, eq) => {
        const hasOwnProperty = Object.prototype.hasOwnProperty;
        const has = (obj, key) => hasOwnProperty.call(obj, key);
        const akeys = Object.getOwnPropertyNames(a);
        const bkeys = Object.getOwnPropertyNames(b);
        return akeys.length === bkeys.length && akeys.every(k => has(b, k) && eq(a[k], b[k]));
    };
    const isPojo = x => Object.getPrototypeOf(x) === Object.prototype;

    if (Object.is(a, b)) return true;
    if (typeof a !== typeof b) return false;
    if (typeof a !== 'object') return false;
    if (a === null || b === null) return false;
    if (Array.isArray(a)) return Array.isArray(b) && arrayEqual(a, b, semiDeepEqual);
    if (isPojo(a)) return isPojo(b) && pojoEqual(a, b, semiDeepEqual);
    return false;
};

プラグインコマンドのパーサ

ソースコード:Gist

プラグインコマンドでコマンドラインツールみたいなことやりたいな、と思って書いたパーサコンビネータです。 割と雰囲気で設計したので、どれくらいまともに使えるかは不明。

こんな感じで使います。

const parsePluginCommand = Parser.build(
    Parser.commands([
        {
            name: "foo",
            parser: Parser.arguments([
                {
                    name: "first",
                    parser: Parser.string(),
                },
                {
                    name: "second",
                    optional: true,
                    parser: Parser.number(),
                },
            ]),
        },
        {
            name: "bar",
            parser: Parser.arguments([
                {
                    name: "type",
                    parser: Parser.oneOf([
                        Parser.keyword("mv"),
                        Parser.keyword("mz"),
                    ]),
                },
                {
                    name: "value",
                    optional: { default: 42 },
                    parser: Parser.integer(),
                },
                {
                    name: "options",
                    parser: Parser.options([
                        {
                            name: "id",
                            alias: ["-i"],
                            parser: Parser.natural(),
                        },
                        {
                            name: "flag",
                            default: false,
                            parser: Parser.succeed(true),
                        },
                    ]),
                },
            ]),
        },
    ])
);

const log = args => console.log(parsePluginCommand("Awesome", args.split(" ")));
log("foo xyz 42");
log("foo abc");
log("bar mv --flag -i 42");
log("bar vx --id 42");

// { value: { command: "foo", first: "xyz", second: 42 } }
// { value: { command: "foo", first: "abc", second: undefined } }
// { value: { command: "bar", type: "mv", value: 42, options: { id: 42, flag: true } } }
// { error: "didn't match any patterns\nAwesome bar |> vx --id 42" }

JSONシリアライザ

ソースコード:Gist

コアスクリプトのものと互換性のあるJSONシリアライザ/デシリアライザです。

なんでそんなものを書いたかというと、 以前、実行時とセーブ時で別々の形式をとりたいデータに対して、 Proxyを使ってセーブ時に形式を別のものに見せかけたらバグったからです。 コアスクリプトのシリアライザは対象データに対して、 変換→シリアライズ→逆変換というよくわからないことをやっていて、 値を別のものに見せかけるとそれを自身に再代入してデータを壊してくれます。

また、コアスクリプトのデシリアライザは、 プロトタイプの設定にObject.createよりもObject.setPrototypeOfを優先して使うため、 JavaScript実行エンジンの最適化を無効化してくれます (まあ、もともとプラグインが好き勝手プロパティを生やすオブジェクトに最適化が効くのかどうか怪しいですが)。

製作したシリアライザは対象データを書き換えることがなく、 デシリアライザはプロトタイプの設定にObject.createを使用します。

ライセンス

この記事中のソースコードおよび記事中からリンクされたGistのソースコードは CC0でライセンスされているものとします。

おわりに

読んでいて気付いた方もいるかもしれませんが、 実はこの記事、『イマドキ』の書き方以上に『イマイチ』な書き方にフォーカスしています。 何故かというと、プラグインの改造依頼としてよくそういうプラグインが持ち込まれてとてもツラいからです。 解読する手間やリスクを含めて請求すると「高くない?」となり、 新規に作り直しましょうと提案すると「なんで?」となり、 これは改造できませんと断ると「実力がないのかしら?」となります。 だからといって「クソコードなんでムリです(意訳)」なんて言えば角が立つのは避けられないわけで……。 少しでもこういう事態が減ればいいなと思って書きました。 自分本位ですみません。

本来は設計に関する話を中心にすべきだとは思うんですが、 このあたりはわりと私論として話さざるを得ない部分が多くて説得力に欠けるので、 今回は簡単に直せる具体的なミスを中心に指摘することにしました。 というか、ツクールプラグインの素晴らしい設計法なんてものがあるならこっちが教えてほしい。



執筆時のRPGツクールMVのバージョン:1.6.2

【RPGツクールMV】コモンイベントの力を見せてやる!!

RPGツクールはシンプルな使い勝手にもかかわらず、 使い方次第で多彩なゲーム表現を可能とするツールです。 今回はそんなRPGツクールにて、隠されたアイテムを発見するダウジング機能を実装してみたいと思います。

そう、コモンイベントのみで……。

ダウジング機能のアニメGIF

サンプル(RPGアツマール):
https://game.nicovideo.jp/atsumaru/games/gm11759

ココナラRPGツクールMVのプラグイン製作サービスを出品してみたのですが、 依頼が来ないので宣伝というかパフォーマンスとしてやってみました。 コモンイベントは簡易版プログラムみたいなものなので、 プログラマならこんなこともできんだよ~ってことで。

はじめに

今回つくるダウジング機能はこんな感じの仕様です。

  • プレイヤーがアイテムから一定距離内に近づくと反応する。
  • 反応するとアイテムの存在する方向を指し示す。
  • 指示点は常にゆっくりと揺れている。
  • アイテムに近づくほど揺れが激しくなる。

一見するとプラグインの領域っぽい、というか、実際プラグインでやるべきものをあえて選んでみました。 この記事ではこれをコモンイベントで実現する方法を解説しますが、絶対に真似しないでください(!?)。 何故ならば、これは単なるパフォーマンスであって、とんでもなく効率の悪い方法だからです。

もし同じような機能が欲しい場合にはプラグイン製作者に頼んでみてください。 前述の通り、私は有料で製作サービスをやっていますし、 ツクール界隈には無料でプラグインを製作してくれる親切な人がたくさんいますので。

仕組みについてはプログラマ以外にも配慮して解説はしますが、プログラミング未経験者には結構難しいと思います。 どれくらい難しいかというと、プログラミング言語を覚えてプラグインとして同じものをつくる方が簡単かもしれないくらい難しいです。 逆に、この記事をすべて理解できたならプラグイン開発を始めてみるといいかもしれません。

と、まあそんなわけで、仕組み自体にそんなに興味のない人はさらっと眺めて、 プログラミングってこんなこと考えるんだなぁくらいに思ってもらえればこの記事は大成功です。 「コモンイベントの ちからって すげー!」っと言って帰りましょう。

実装方針

基本的な流れとして並列処理のコモンイベントで毎フレーム特定の処理をすることでダウジング機能を実装します。 フレームというのはゲームの更新処理一回分のことで、だいたい1秒間に60回程度実行されることが多いです (1秒間に実行されるフレーム数をFPSといいます)。 ゲームが動いているように見えるのは1/60秒ごとにこの更新処理によって絵が変化するためです。

毎フレーム実行する処理の概要は次の通りです。

  1. プレイヤー近辺のタイルにアイテムがあるか調べる。
  2. 一番近くにあるアイテムの方向に指示点を動かす。
  3. 適当に揺らしながら指示点を表示する。

具体的な処理内容は実際のコモンイベントを見ながら説明していきますが、 その前に次の内容について説明しておきます。

  • コモンイベントと関数
  • 位置の移動
  • 三角関数

コモンイベントと関数

プログラミングでは「関数」と呼ばれる単位に処理を分割して整理するのが一般的となっています。 この名称はもちろん数学の関数から来ているのですが、表すものは少しだけ違います。 関数とは、いくつかの設定(入力)を受け取り、設定に従って処理をして、結果(出力)を返す、という流れに従った一連の処理を指します。 入力と出力はない場合もあります。

これだけ聞いてもいまいちピンとこないかもしれませんが、RPGツクールのイベントコマンドはだいたいこの関数の形になっています。 例えば、「指定位置の情報取得」コマンドについて考えてみましょう。 このコマンドでは、どの位置のタイルのどんな情報を取得したいかという『設定を受け取って』、 それに従った情報を取得する『処理をして』、指定された変数にその値を入れることで『結果を返します』。 他にも「所持金の増減」コマンドでは、 所持金をどれくらい増やすまたは減らすのかという『設定を受け取って』、 所持金を増減させる『処理をします』。 出力はありません。 「ゲームオーバー」コマンドでは、ゲームオーバーにする『処理をします』。 入力も出力もありません。 なんとなく関数のイメージが掴めたでしょうか?

さて、何故こんな関数の話をしたかというと、 コモンイベント自体もこの関数の考えに従って整理することができるからです。 今回実装したダウジング機能は30ものコモンイベントによって構成されていますが、 そのすべてが一画面に収まる長さの短いコモンイベントになっています。 実は長いコモンイベントにすれば2つに収まるのですが、 長いコモンイベントではどの部分で何をやっているのか把握するのが困難で理解しづらいのです。

一般にプログラマというと、特殊技能に精通した人というイメージが強いかもしれませんが、 実は「いかに文章(プログラム)をきれいにわかりやすく書くか」ということを真面目に考えている人たちだったりします。 正直、化学者や生物学者なんかよりは論説とかの物書きの方が近いんじゃないかと思ったり……。

閑話休題。

では、コモンイベントで関数をどう表現するかについて考えてみましょう。 関数は入力・処理・出力の三要素から成るのでした。 このうち処理はイベントコマンドを羅列するだけなので特別な機構は要りません。 入力や出力には「指定位置の情報取得」でも使っていたように変数を使います。 ただし、どの変数を使うかは決め打ちです。 「このコモンイベントの実行前には必ずこの変数とこの変数に値を設定してね」 「このコモンイベントの実行後はこの変数に結果が入ってるよ」 とあらかじめ決めておくことでコモンイベントの入力と出力を表現します。

関数には必ずその実行を指示する(「呼び出す」と言う)別の関数(コモンイベント)が存在します。 呼び出し側のコモンイベントでは、まず関数の仕様に従って必要な設定を変数に入れ、 「コモンイベント」コマンドを実行してから、関数の仕様に従って変数から値を取り出します。

これでコモンイベントで関数を表すことができました。 ……とはいっても、言葉の説明だけではなかなか理解しづらいかもしれません。 実際のコモンイベントの実装をみてみると理解しやすいと思うので、意識しながら読んでみてください。 特に「揺れの更新」あたりの実装は説明したことがそのまま反映されているのでわかりやすいと思います。

位置の移動

今回のダウジング機能の仕様には、指示点の表示位置を決めるための座標がいくつかあります。

  • 表示の中心座標
  • 指示点の目標位置
  • 指示点の現在位置
  • 揺れによる変位

これらを使ってどのように指示点の位置を計算するかについて説明します。

「表示の中心座標」は、指示点を含めたダウジング表示全体の中心となる座標です。 この値は設定として与えられる固定値であり、唯一の絶対座標です。 他の座標はすべて、この座標からの相対座標か、あるいはさらにそれの相対座標です。

「指示点の目標位置」と「指示点の現在位置」は、ダウジング表示上のどのあたりに指示点を表示するかです。 これらはともに中心座標からの相対座標になります。 目標位置はプレイヤーからアイテムに向かう方角上かつ円周上の点で、 指示点の現在位置は毎フレームこの点に向かって、設定された最大速度を超えないように移動します。

「揺れによる変位」はX方向とY方向それぞれの振動の合成による指示点の位置ずれです。 これは指示点の移動とは独立した運動であるため、指示点の現在位置からの相対座標になります。

まとめると次のようになります(min(a, b)abの小さい方を表します)。

表示位置 = 中心座標 + 現フレームの現在位置 + 揺れによる変位

現フレームの現在位置 = 前フレームの現在位置
    + min(現フレームの目標位置 - 前フレームの現在位置, 最大移動速度)

三角関数

三角関数は今回の実装における最大の難所です。

方角から円周上の点を求めたり振動させたりと、 三角関数を使いたくなる仕様でいっぱいなのですが(もちろんわざとです)、 残念ながら……非常に残念ながらコモンイベントには三角関数を計算する機能はありません。 というか、まず小数がありません。

そこで、「小数をどう表現しようか」「三角関数をどうやって計算しようか」というのが、 この記事のプログラマ的見せ場となっております。 順に説明していきます。

小数の表現

コモンイベントでは小数を扱うことができません。 除算結果の小数点以下は切り捨てられるため、例えば1 / 20になります。 sincosが仮に計算できたとしても、そのままでは至る所で0になってしまいます。 円周率だって3です。 このままでは「ゆとり」だと揶揄されてしまいます。

小数を表現するアイデアというのは実は非常に単純で、単にn倍するだけです。 例えば、n=1000としましょう。 そのままでは1 / 2 + 1 / 2の結果は0ですが、 1000倍された状態で計算すれば、1000 / 2 + 1000 / 21000となり、 1000で割って戻してやれば1となって正しい計算結果になります。

さて、これはアイデアとしては単純ですが、実装するとなると結構ややこしかったりします。 先の例では分子だけを1000倍しましたが何故でしょうか? 掛け算の場合はどうすればいいのでしょう? 1000倍した数同士を掛けて、1000で割れば正しい結果になるでしょうか? ……n倍した数同士の演算手順については慎重に考える必要がありそうです。

まず、単純な四則演算について考えてみましょう。 計算に使う値と計算結果がそれぞれn倍された状態になるように注意すると、 四則演算は次のようになります。

n(x + y) = nx + ny
n(x - y) = nx - ny
n(x * y) = (nx * ny) / n
n(x / y) = (nx / ny) * n = (nx * n) / ny

【xとyの演算結果のn倍】を、【xのn倍】と【yのn倍】と【n】による演算で表現できました。 割り算で計算順序を入れ替えていることに注意してください。 割り算をするとその時点で小数点以下が切り捨てられてしまうため、 先にn倍してから割った方が誤差が少なくなります。

次に複数の四則演算を組み合わせた演算についてですが、 可能な限り割り算の回数を少なくかつ後回しにすると計算誤差が減ります。 例えば、1 / 3 * 3を先ほどの四則演算の手順に従って計算してみましょう。 すべての数を1000倍して同等の演算に置き換えると次のようになります。

[ n(x / y) = (nx * n) / ny ]
1000 * (1 / 3) = (1000 * 1000) / 3000 = 333

[ n(x * y) = (nx * ny) / n ]
1000 * ((1 / 3) * 3) = ((1000 * (1 / 3)) * 3000) / 1000 = (333 * 3000) / 1000 = 999

1 / 3 * 3 = 999 / 1000 = 0

計算誤差のせいで正しい結果にはなりませんでした。 しかし、これを(1 * 3) / 3と変形してやれば当たり前ですが、正しい計算結果になります。 1 / 3 + 2 / 3なんかも同じで、(1 + 2) / 3としてやれば正しく計算できるようになります。

後述する三角関数の実装ではこのようなことに注意して計算しているため、 一見おかしな計算方法に見えるかもしれません。 しかし、計算誤差を抑えるためには必要な手順なのです。

これまでの説明と後述の三角関数の実装で一点だけ違うところとして、 説明では入力と出力のどちらもn倍された状態を考えましたが、 実装では入力をn倍、出力をm倍と値を変えています。 つまり、f(x * n) * mを計算するように三角関数は実装されています。

三角関数の計算

さて、小数の計算はできるようになりましたが、依然としてできるのは四則演算だけです。 なんとかして四則演算によって三角関数を計算しなければなりません。 そこで、マクローリン展開というものを使います。

マクローリン展開は関数f(x)と等価な多項式を求める方法です。 つまり、関数f(x)に対して次の式の定数anをすべて求めます。

 \displaystyle
    f(x) = a_0 + a_1 x + a_2 x^2 + a_3 x^3 + a_4 x^4 + \cdots

微分やら階乗やら無限級数やらで複雑なので、マクローリン展開の詳しい説明はここではしません。 他所の解説を参照してください。 ここで重要なのは三角関数を多項式で表すことができるという点のみです。

とはいえ、変形結果は無限多項式なのでこれをそのまま計算することはできません。 ではどうするのかというと、最初の方のいくつかの項までだけ計算して残りは無視してしまいます。 「そんなことしていいの?」と思うかもしれません。 もちろん普通はダメです。 しかし、特定の条件下であればこれは近似式として使うことができます。 それはxの値が0に近いときです。

0に近い値、例えばx=0.1としましょう。 このときx2=0.01で、x3=0.001です。 後ろの方の項ほど非常に小さい値になるのがわかると思います。 非常に小さい値を足したり引いたりしたところで値が大きく変化することはないため、 これらは誤差として許容すれば無視することができます。

0に近い値というのがどれくらいまでなのかは許容できる誤差の大きさによりますが、 今回は絶対値が1以下の範囲くらいまでとして使っています。 これはそれなりに誤差が大きく、角度に数度のずれが生じますが、 指示点を揺らしたりしてごまかしているのでまず気づかれません。

今回の実装に必要な(逆)三角関数はarctansincosの三つです。 arctantanの逆関数で、tanθの値からθを求めます。 これらをマクローリン展開して、x3の項までとった結果は次の通りです。

 \displaystyle
\begin{eqnarray*}
    arctan(x) & \simeq & x - \frac{x^3}{3} \\
    sin(\theta) & \simeq & \theta - \frac{\theta^3}{6} \\
    cos(\theta) & \simeq & 1 - \frac{\theta^2}{2}
\end{eqnarray*}

ここでarctanについて、これは値域が-πからπであるためすべての角度を取得することができません。 しかし、今回はこれをアイテムのある方角の計算に使いたいのでそれでは困ります。 そこで、代わりにプログラミング言語でよく使われるatan2という関数を採用します。 これはxとyの二つの値を受け取って、(デカルト)座標(x,y)を極座標に変換した際の偏角を求めるものです。 xが正の値のとき、atan2(y,x)arctan(y/x)に一致するため、実装にはarctanを使います。

前述したようにマクローリン展開による近似関数の有効定義域はせいぜい[-1, 1]です。 このままでは利用できないため、次の性質を利用して定義域を広めます。

 \displaystyle
\begin{eqnarray*}
    atan2(y,-x) & = & atan2(-y,x) + \pi \\
    atan2(-y,x) & = & -atan2(y,x) \\
    atan2(x,y) & = & \frac{\pi}{2} - atan2(y,x) \\
    sin(-\theta) & = & -sin(\theta) \\
    sin(\pi - \theta) & = & sin(\theta) \\
    sin(\frac{\pi}{2} - \theta) & = & cos(\theta) \\
    cos(-\theta) & = & cos(\theta) \\
    cos(\pi - \theta) & = & -cos(\theta) \\
    cos(\frac{\pi}{2} - \theta) & = & sin(\theta)
\end{eqnarray*}

atan2について。 まず、最初の式で第二象限と第三象限の計算を、第一象限と第四象限の計算に変換します。 そのあと、次の式で第四象限の計算を、第一象限の計算に変換します。 そして、最後の式でy > xの領域の計算を、y <= xの領域の計算に変換します。 これでy / xの取り得る値の範囲は[0,1]となり、マクローリン展開したarctanに適用できます。

sincosについて。 まず、最初の式で-πから0までの角度の計算を、0からπまでの角度の計算に変換します。 そのあと、次の式でπ/2からπまでの角度の計算を、0からπ/2までの角度の計算に変換します。 そして、最後の式でπ/4からπ/2までの角度の計算を、0からπ/4までの角度の計算に変換します。 これでマクローリン展開したsincosに適用できます。

なんとか三角関数を実装する目処が立ちました。

コモンイベント解説

各コモンイベントの解説です。

全部で30あるので、真面目に読むと死ねます。ほどほどに。

ダウジング機能

コモンイベント『ダウジング機能』

ダウジング機能の準備と後片付けです。 「ダウジング機能」スイッチが有効な間、動き続けます。

やっていることはダウジング機能に使う設定値や状態の初期化とピクチャのON/OFFです。 「ダウジング中」スイッチにより、ダウジング状態はON/OFFできるようになっているため、 それに合わせてダウジング表示に使うピクチャの表示・非表示を切り替えています。

設定の初期化

コモンイベント『設定の初期化』

設定値の初期化です。

内容は次の通り。

設定 説明
最大有効距離 アイテムが発見可能なマンハッタン距離
表示の中心座標 ダウジング表示の中心座標
表示の半径 揺れを含まない指示点の移動円の半径
数値の精度 小数の計算のために数値に掛けられる倍率
最大移動速度 1フレームに移動できる最大の距離
揺れの振幅 揺れる幅の大きさ
揺れの最小周期 最もアイテムに近づいたときの揺れ一周にかかる時間
揺れの最大周期 最もアイテムから離れたときの揺れ一周にかかる時間
揺れの初期位相 揺れが始まる角度

マンハッタン距離というのは、X方向とY方向の位置差分の絶対値を足し合わせたものです。 要はグリッド上を上下左右移動のみで移動した場合に最短ルートで何歩かかるか、 という測り方の距離です。

状態の初期化

コモンイベント『状態の初期化』

フレーム間にわたって保持する必要のある状態の初期化です。

内容は次の通り。

状態 説明
現在位置の座標 現在の揺れを含まない指示点の座標
揺れの位相 現在の揺れの角度

ピクチャの表示

コモンイベント『ピクチャの表示』

ダウジング表示のためのピクチャの表示です。

ピクチャの消去

コモンイベント『ピクチャの消去』

ダウジング表示のためのピクチャの消去です。

ダウジング中

コモンイベント『ダウジング中』

ダウジング中に毎フレーム実行される処理です。 「ダウジング中」スイッチが有効な間、動き続けます。

処理内容は次の通りです。

  1. プレイヤー近辺のタイルを調べてアイテムを探す。
  2. 最も近いアイテムの方角を示す指示点の目標位置を算出する。
  3. 現在位置から目標位置に向かって最大速度を超えないように移動する。
  4. 現在の揺れの状態を計算する。
  5. 現在位置に揺れを加えてダウジング表示を更新する。

タイル走査前の初期化

コモンイベント『タイル走査前の初期化』

タイル走査中に参照あるいは操作する変数の初期化です。

「走査」というのはプログラミング用語で、 たくさんあるものをひとつひとつ調べることを言います。 英語のスキャン(scan)に対応する言葉だというとわかりやすいでしょうか(むしろわかりにくい?)。

タイル走査の目的は最も近くにあるアイテムをみつけることです。 そのためにはそれまでに発見した最も近いアイテムまでの距離を記憶しておき、 その距離と新しく発見したアイテムまでの距離を比較します。 新しいアイテムまでの距離の方が短ければ、それをその時点での最有力候補として記憶しておきます。 すべてのタイルを調べ終えた時点での最有力候補が最も近いアイテムです。

ただし、この方法には考慮すべき事項が二つあります。 ひとつは一番最初に発見したアイテムをどう扱うか、 もうひとつはアイテムをひとつも発見できなかった場合にどうするか、です。

前者について、最初のアイテムはもちろんその時点での最有力候補にすればいいのですが、 問題はそれが最初のアイテムであるとどうやって判断するかです。 適当な変数にそれまでの発見数を記憶しておいてもいいのですが、 ここでは「それまでに発見した最も近いアイテムまでの距離」を大きな値で初期化することで解決します。 なぜなら、この値が十分に大きければ最初のアイテムまでの距離は必ずそれより短いため、 特別扱いしなくても最有力候補になるからです。

また、この方法によって自動的に後者の問題も解決します。 もし、すべて調べ終えても「それまでに発見した最も近いアイテムまでの距離」が有効距離内にないのなら、 それは発見できなかったことに他ならないからです。 初期化に使う「大きな値」は有効距離より大きければ何でもいいのですが、 ここでは有効距離より1大きい数にしています。

タイル走査

コモンイベント『タイル走査』

最大有効距離内のタイルすべてが収まる最小の矩形内のタイルを走査します。

各タイルに対する処理は別のコモンイベントに任せます。

タイル毎の処理

コモンイベント『タイル毎の処理』

タイルごとにアイテムが存在するかどうかとその距離を調べます。

これまでに発見したアイテムと距離が同じかそれより遠い場合には調べる意味がないので、 最初にそれをチェックしています。

今回の実装の最がっかりポイントなのですが、 「アイテムが存在するかどうか」を「イベントが存在するかどうか」で判定しています。 つまり、すべてのイベントに対してダウジングが反応します(!?)。 全然実用的じゃないですね。 まあ、そもそもコモンイベントによる実装自体が実用的じゃないんですが……。

コモンイベントから取得できるタイルの情報はいくつかありますが、 発見後に消滅させられるものと考えるとイベントくらいしか思いつきませんでした。 ちなみに、イベントの消滅も「イベントの一時消去」では効かないので、 マップの遥か彼方へとイベントを吹っ飛ばして消し去っています。

この記事執筆中に思いついた別案として、 「奇数のリージョンID上にあるイベント」 というように条件を絞り込めばもう少し使えそうかなとは思いますが、 どうせ実際のプロダクトには使わないのでこのままにしておきます。

距離の計算

コモンイベント『距離の計算』

プレイヤーと現在タイルとのマンハッタン距離を計算します。

絶対値の和をとるだけなのですが、まず絶対値をとるのが面倒という……。

目標位置の計算

コモンイベント『目標位置の計算』

タイル走査の結果を元に、指示点の目標位置を計算します。

発見できなかった場合と、プレイヤーの真下にアイテムがあった場合には目標位置は中心です。 それ以外の場合にはアイテムのある方向の円周上になります。

目標位置(反応時)

コモンイベント『目標位置(反応時)』

アイテムのある方向の円周上の点の座標を計算します。

atan2で角度を取得してから、cossinでそれぞれX座標とY座標を算出します。 この際、あらかじめ数値の倍率に半径を掛けておくことで、cossinの結果が半径倍されます。

目標位置(非反応時)

コモンイベント『目標位置(非反応時)』

指示点の目標位置を中心に設定します。

現在位置の更新

コモンイベント『現在位置の更新』

目標位置に向かうように指示点の現在位置を更新します。

処理の大部分は移動速度が設定された最大値を超えていないかのチェックです。 目標位置に一瞬で移動した場合の移動距離(ユークリッド距離)を、 三平方の定理(ピタゴラスの定理)で算出したいのですが、平方根を計算する術がありません。 そこで発想を変えて、最大値の方を二乗することで二乗した値同士の比較でチェックします。 正の数の大小関係は値を二乗しても変わらないため、これで問題ありません。

移動量の修正

コモンイベント『移動量の修正』

最大移動速度を超えてしまう場合に移動量を調整します。

atan2で移動方向を取得してから、 cossinで最大移動速度に一致するようにX方向とY方向の移動量を算出します。

揺れの更新

コモンイベント『揺れの更新』

X方向、Y方向それぞれの揺れの変位を計算します。

ここではX方向とY方向の設定値を変数に入れるだけで、 実際の計算は別のコモンイベントで行います。

揺れの周期の決定

コモンイベント『揺れの周期の決定』

揺れの最小周期と最大周期の間をアイテムとの距離に合わせて補間します。

補間の方法は線形補間と呼ばれるもので、 tを0から1の範囲の値としてa(1 - t) + btという式でaとbの間の値を得ます。 tが0ならaに一致して、1ならbに一致します。 しかし、小数を扱うのは面倒なので、 ここでは適当な重みuとvを使って(au + bv) / (u + v)という式に変形して使っています。 t = v / (u + v)とすれば一致するのがわかると思います。

あとはこの重みを距離に応じて設定するだけです。 aを最小周期、bを最大周期とすると、 vをアイテムまでの距離、u+vを最大有効距離とすればOKです。

揺れの変位の計算

コモンイベント『揺れの変位の計算』

揺れの設定値に従って、揺れの位相を更新し、揺れによる変位を求めます。

揺れはA sin(θ + 2π/T)という式で計算しています。 Aが振幅、Tが周期で、θが前フレームの位相です。

コモンイベントの前半部分では、 周期から角速度を計算して、それに従って位相をずらしています。 設定値との兼ね合いや扱いやすさから、この時点では度数法を使っています。

後半部分では現フレームの位相の範囲を[0,360)から[-180,180)に調整し、 弧度法へと変換した後に振幅倍したsin関数を適用しています。

表示の更新

コモンイベント『表示の更新』

現フレームの表示位置を計算した後に、ダウジング表示のピクチャを移動させます。

表示位置の計算

コモンイベント『表示位置の計算』

現フレームの表示位置を計算します。

いくつかの座標は計算の精度を上げるためにn倍されているため、nで割って元の倍率に戻します。

atan2(y,x)

コモンイベント『atan2(y,x)』

atan2の計算その1です。

すべての計算が第一象限で行われるように入力と出力を変換して、あとはatan2(y,x)'に任せます。

atan2(y,x)'

コモンイベント『atan2(y,x)'』

atan2の計算その2です。

y/xの値が1以下になるように入力と出力を変換して、arctan(y/x)の近似計算を呼び出します。

arctan(x) [0,1]

コモンイベント『arctan(x) &#091;0,1&#093;』

arctanの計算です。

入力の倍率(S_x)と出力の倍率(S_theta)に注意して、 マクローリン展開による近似式x - x^3 / 3を計算します。

sin(theta)

コモンイベント『sin(theta)』

sinの計算その1です。

角度が0からπ/2の間になるように入力と出力を変換して、あとはsin(theta)'に任せます。

sin(theta)'

コモンイベント『sin(theta)'』

sinの計算その2です。

角度が0からπ/4の間になるように入力と出力を変換して、sinの近似計算を呼び出します。

sin(theta) [0,PI/4]

コモンイベント『sin(theta) &#091;0,PI/4&#093;』

sinの計算その3です。

入力の倍率(S_theta)と出力の倍率(S_x)に注意して、 マクローリン展開による近似式x - x^3 / 6を計算します。

cos(theta)

コモンイベント『cos(theta)』

cosの計算その1です。

角度が0からπ/2の間になるように入力と出力を変換して、あとはcos(theta)'に任せます。

cos(theta)'

コモンイベント『cos(theta)'』

cosの計算その2です。

角度が0からπ/4の間になるように入力と出力を変換して、cosの近似計算を呼び出します。

cos(theta) [0,PI/4]

コモンイベント『cos(theta) &#091;0,PI/4&#093;』

cosの計算その3です。

入力の倍率(S_theta)と出力の倍率(S_x)に注意して、 マクローリン展開による近似式1 - x^2 / 2を計算します。

PI

コモンイベント『PI』

πの取得です。

指定倍率(S_theta)の円周率を取得します。

おわりに

めちゃくちゃ長いですね……全部まともに読んだ人がいたならお疲れさまでした。

元々プラグインをつくれない人たちに向けたパフォーマンスだったはずなんですが、 途中からもはや誰向けなのかよくわからなくなってますね……。 ただまぁ、プログラミングというのがそれなりに手間と技術が必要な作業だと感じてもらえればいいなと思います。 ココナラのリクエストボードなんかみてると、 プログラムに対する一般の人の料金感覚と実際の相場が10倍から100倍くらい違うみたいなので……。

最後に自分でネタ振っといて投げっぱなしなのもなんなので、一言。

『コモンイベントの ちからって すげー!』



執筆時のRPGツクールMVのバージョン:1.6.2

ツクールMVでローグライク-ライクなゲームをツクった話

ふと「ツクールMV買ったのにゲームひとつもツクってないな」と気づき、衝動的にゲーム制作。 一週間くらいでできるかな、とか考えていたのですが、どうやらRPGの設定項目の多さを甘く見過ぎていたようです・・・。 結局、倍の二週間をかけ、とりあえず動いて、とりあえず遊べるくらいのものをRPGアツマールにアップしました。

今回はそんな感じでつくったものの紹介と製作途中のあれこれについてです。

制作物

ミニローグライク(RPGアツマールに飛びます)

ランダムダンジョンかつ死んだら終わりというローグのシステムを採用したゲームです。

f:id:fspace:20170404204124p:plain:w300

「不思議のダンジョン」シリーズのようなローグのシステムをまるまる受け継いだものを『Roguelike(ローグライク)』と呼ぶのに対し、 システムの一部だけを採用したゲームを『Roguelike-like』や『Rogue-Lite』、『PDL』と呼ぶそうです。 自分がやったことあるのだとSpelunkyとかですね。 今回つくったのは単なる妥協版ローグライクなのですが、一応ローグ的な性質を持っているのでローグライクライクゲームということに。

ゲームの目的は最上階にいるボスを倒すことです。 途中の階層では、障害物・敵・アイテム・罠がランダムに配置されます。 敵を倒してレベルをあげたり、アイテムを拾ったりして、 できるだけ有利な状況で最上階にたどり着くことで、ボスを撃破できる確率が上がります。

ローグライクゲームでは普通、敵がマップ上で様々な行動をとりますが、 このあたりのシステムをつくっていると時間がかかるので、このゲームでは単に徘徊するだけです。 敵と接触するとツクール標準のバトルが始まります。 戦闘中は一対一ですが、戦闘後にマップでアイテムやスキルを使うとターンが消費されるので、囲まれると連戦になります (とはいえ初期配置が悪くない限りはほとんどそうなりませんが)。

またローグライクゲームのダンジョンというと、部屋と道に分かれていてどこに何があるかわからないというのが普通ですが、 実装が面倒だったので、見通しのいい一画面にボンバーマンよろしく破壊可能な障害物をちりばめただけのダンジョンになっています。 一応、破壊に1ターンかかるので、うまく逃げられないような状況は作り出せているかな、と。

未識別なアイテムや攻撃系のアイテムは実装するつもりだったのですが、これ以上時間を割きたくなかったので断念しました。 バグっぽい挙動も修正が面倒そうなものは仕様ということにして目を瞑ってます。 実のところ、ろくにテストプレイもしていないのでゲームバランスはてきとーです。

ここまで読んでなお興味を持ってくれる人がいたならば、RPGアツマールにて遊んでみてください。

発想

このゲームシステムに至るまでの思考の垂れ流し。

もともと開発開始の時点で期間を一週間程度と決めていたので、まず短期間で面白いものをつくるためにどうするか考えました。 ツクールの本分ではないRPG以外のゲームをつくるのは時間がかかりますし、 かといってまともにRPGのマップやストーリーを作るような時間はありません。 そこで、ランダム生成ダンジョンをひたすら探索するゲームにすることに決めました。

以前のプラグイン開発(過去記事 #1 #2 参照) でまともにローグライクのシステムを組み込むのが面倒なことはわかっていました。 そこで、まず真っ先にマップ上での特殊行動(行動順の制御)と敵の追加発生(マップデータの動的変更)を切ることに決めました。 しかし、この二つがなくなるとマップの探索はただただ歩き回るだけで極端につまらなくなります。

どんなダンジョンにすれば探索が面白くなるのか考えた結果、ふとボンバーマンを思い出して、障害物の下にアイテムを隠すことにしました。 障害物を置くだけのマップ生成は非常に簡単なので一石二鳥です。 マップの大きさをどうするかは悩んだのですが、無駄に広くして壮大な感じを出すよりはわかりやすい一画面にしました。 クソゲーはクソゲーとしての身分をわきまえていた方が遊ぶ側としても気楽に手を出せると思ったので。

敵との戦闘は結局普通のツクールの戦闘を採用しました。 下手に複雑なシステムを組むよりわかりやすいですし、なにより実装の手間が省けるので。 ツクールには敵の群れを設定する機能がありますが、マップ上の敵キャラ表示からイメージがずれないように敵は一体ずつにしました。 同時に複数の敵と接触したら同時に戦闘、というような仕様も考えましたが実装が面倒なのでボツに。

そんなこんなで基本的なゲームシステムは決まりました。

絵や音を作ったり探したりしている時間はないだろうということで、これらは最初からデフォルト素材を使うことに決めていました。 とりあえず素材を一通り眺めたところ、パッケージキャラクターのデザインがいい感じだったのでこれらを使うことに。 主人公っぽいやつ一人だけだと少し面白みに欠けるので、アイテムを使ってキャラを切り替えることにしました。 「今のキャラの方がいいから切り替えなくてもいいや」となるとつまらないので、 切り替えアイテム(鏡)には、先制かつ状態異常含め全回復という特殊効果をつけました。

あとは各パッケージキャラクターの特徴をイラストから推測して能力に落とし込み、 それぞれが生きそうな状況を考えつつ、状態異常やスキル、敵キャラの能力なんかを設定してゲームが完成しました。

小技・改造

制作中の小技とか改造とかの話です。

ツクールでのゲーム制作の参考になれば。

TypeScript

ツクールMVは基本的にES5なのでJavaScriptでコードを書くのは結構苦痛だったりします。 そのため、今回のコードはすべてTypeScriptで書きました。

ツクールMVとTypeScriptについては過去記事を参照してください。

www.f-sp.com

ただし、過去記事ではモジュールを用いていたのに対し、今回はすべて名前空間で済ませてしまっています。 他のプラグインとの併用を考えないくてよいのであれば、名前空間で十分なので。

ダメージ計算式

ツクールMVでは各スキルごとにダメージの計算式を記述できます。 a.atk * 4 - b.def * 2のような感じで書くのですが、すべてのスキルに同じような計算式を書くのは結構面倒だったりします。 特に途中で計算式を変更しようものなら、全スキルの記述をいちいち直さなければなりません。

しかしこの計算式、実は内部的にはevalしているだけなのでプラグインで定義した関数呼び出しができます。 そこで、$dmgのようなグローバル関数を定義して$dmg(a,b)とだけ書いておくと、 プラグイン側の定義変更で全スキルの計算式が一括設定できます。 地味に便利。

取得経験値

各敵キャラには撃破時の取得経験値を設定できます。 最初、取得経験値は手動でそれっぽい値を入れていたのですが、 非常に面倒だったので最終的にプラグイン側でおおまかな値を計算してそれに係数をかける方式にしました。

const AUTOEXP_TAG = 'autoexp';

const _Game_Enemy_exp = Game_Enemy.prototype.exp;
Game_Enemy.prototype.exp = function (this: Game_Enemy): number {
    const meta = this.enemy().meta;
    if (AUTOEXP_TAG in meta) {
        const value = meta[AUTOEXP_TAG];
        const number = (value !== true ? parseFloat(value) : NaN);
        const rate = (isFinite(number) ? number : 1);

        return Math.ceil((this.mhp * 3 + this.atk + this.def + this.mat + this.mdf + this.agi) * rate);
    } else {
        return _Game_Enemy_exp.apply(this, arguments);
    }
}

あとは敵キャラのメモ欄に<autoexp:2>のようなタグをつけるだけです。

スキルのレーティング

各敵キャラの使用スキルにはレーティング(使用度合)が設定できます。 しかし、マニュアルをみるとわかるように結構わかりづらいアルゴリズムで割合が決まります。

そのままだと調整がしづらかったため 2^{x-1}で重みを設定する簡単な方式に変えました。 ただし、最大値が設定された場合には条件を満たす限り必ずそのスキルを使用するようにしています。

コードはこんな感じ。

Game_Enemy.prototype.selectAction = function (this: Game_Enemy, actionList: Action[], ratingZero: number): Action | null {
    if (actionList.length !== 0) {
        const MAX_RATING = 9;
        const max = actionList.filter(action => action.rating === MAX_RATING);
        if (max.length === 0) {
            const weights = actionList.map(action => 1 << (action.rating - 1));
            const total = weights.reduce((sum, weight) => sum + weight, 0);
            const value = Math.randomInt(total);
            for (let i = 0, sum = 0; i < weights.length; i++) {
                if (value < (sum += weights[i])) return actionList[i];
            }
        } else {
            return max[0];
        }
    }

    return null;
}

バトルイベントのスパン

ボス戦にて被ダメによるステート解除を検出する必要があったのですが、どう実装すべきか結構悩みました。 そもそもツクールにはステート変更に関するコールバックはありませんし、 戦闘中はコモンイベントの並列処理も働かないようで監視できません。

そこで、バトルイベントのスパン「モーメント」で監視することを考えたのですが、 この「モーメント」というのは通常イベントの「自動実行」と同じで、 条件を満たさなくなるまで他の処理より優先的に実行され続けます。 つまり、内部で条件を無効にするような処理を書かない限り、無限ループとなって戦闘が進みません。 一方で、条件を無効にする処理を書いてしまうと、再度有効にしない限り処理が実行されないので監視ができません。

仕方がないので、プラグイン側で「モーメント」の仕様を「並列実行」に近いものに書き換えて使用することにしました。 バトルイベントはボス戦でしか使わないので大丈夫だろうという妥協策です。

そもそも条件式としてスクリプトを書ければそれで済んだのだけど、 使ってみると案外ツクールのエディタからスクリプト差し込める場所って少ないんですよね・・・。

苦労話

開発期間が延びた原因とかハマったところの話。

名前・説明文

正直、今回のゲーム開発で一番苦労したのが名前や説明文といった文章問題だったり。

今まであまり王道RPGをやってこなかったことや、RPG開発経験がほぼなかったため、とにかくかっこいい言葉が出てこない・・・。 あんまりベタな英語なんかを使うとかっこ悪いし、かといってよく意味を知りもしない言葉を使うと誤用が怖いので、 なんとか日本語でそれっぽい感じにできないかといろいろ考えました。

しかしまあ、こうして記事を書きながら改めて見直してみると結構恥ずかしいこと書いてますね。 こういうのって案外、羞恥心との戦いなのかもしれません。

Edgeとアツマール

実はゲーム自体は投稿日より一日早く完成していたのですが、RPGアツマールにアップする過程でいろいろあって投稿が遅れてます。

そんな問題のひとつめがEdgeとアツマールの問題。

私は基本的にOSのデフォルトブラウザを使う人間なので、ブラウザはEdgeを使ってます。 しかし、Edgeからアツマールにゲーム情報を入力して投稿ボタンを押すとなぜか何度やってもエラー。 試しにFirefoxでやってみたらすんなり投稿できました。

Edgeが悪いのか、アツマールが悪いのか、はたまた私のブラウザ設定が悪いのかはわかりませんが、 とりあえずEdgeとアツマールの相性はよくない模様。 アツマールでゲームをプレイするときもEdgeだとキー入力がハンドルされなかったり、画面がぶれたりするんですよね・・・。

『未使用』ファイルを含まない

アツマールにどうにか投稿できたということでとりあえずテストプレイしてみたところ、敵を攻撃した瞬間に画面がフリーズ。 ブラウザのデバッグ機能を使ってみたところ、実際には動いているけれど、どこかで無限ループに近い状態に陥り進行不能となっているようでした。

ステップ実行で処理を追っていった結果、ロードに失敗した画像のロード待ちを延々としている状態のようでした。 ローカルでは動いていたことを考えると、おそらくデプロイ時の「未使用ファイルを含まない」というオプションで使用しているファイルが削除されたんだろうな、と。 そこで、出力されたデータのアニメーション画像を確認してみると、通常攻撃時の画像が含まれていませんでした。

データベースを確認して通常攻撃のアニメーション設定を見てみると「通常攻撃」になっているのを発見。 これは通常攻撃に使用するアニメーションと同じものを使用、という意味だと思われるのですが、 通常攻撃のスキル自体に設定した場合にどうなるのかわからなかったためコアスクリプトを確認。 すると、どうも武器を装備している場合には武器のアニメーションが使われ、そうでない場合には1番のアニメーションが自動的に選択されるようでした。

1番のアニメーションはデータベース上のスキルや武器などから明示的に参照されていないため、 未使用ファイルとみなされデプロイ時に削除、結果としてロードエラーで止まっていたようです。 通常攻撃スキルのアニメーションに明示的に1番のアニメーションを設定したところ、削除されなくなり正しく動作しました。

ありがちなバグだとは思いますが、正直勘弁してほしい。

セーブデータとアツマール

アツマールでは一人あたり90ブロックのセーブデータ領域が与えられます。 アツマールのAPIの説明を読むと、1ブロックあたり1KBと書いてあるのですが、 一人分の領域が90KBしかないなんてことはないだろうと勝手に書き間違いだと思い込んでいました。

が、実際にテストプレイしてみると、セーブデータひとつあたり4,5ブロック持っていかれます。 この時点で、このゲームは階層ごとに別ファイルにオートセーブする仕様だったため、 これはまずいということで急遽セーブ方法とデータ容量を見直すことにしました。

セーブ方法はこれまでの方式をオプションとして残しつつ、 基本的にひとつのファイルを上書き保存していく形に変えました。 データ容量は自動生成しているマップと使っていない仲間の情報を捨てることで軽量化しました。 それでも1ブロックにはなりませんでしたが、 これ以上軽量化しようと思うとかなり細かく制御する必要があり、 バグを生みそうなので諦めました。

おそらくビジネス上の戦略としてこの仕様なのだと思うのですが、 プログラマが1KB単位でセーブデータ容量を調整するのは非常に労力の無駄なので、 1ブロックの上限10KBで30ブロックくらいにしてほしいところ。

やりのこしたことメモ

現在特にアップデートの予定はありませんが、いつか気が向いたときにでもやるべきことのメモ。

  • 接触時の戦闘開始を移動と床イベントの発生後まで遅延orキャンセル
  • 足元コマンドの実装
  • 未識別状態の実装
  • 攻撃系アイテムの追加
  • マイナス効果アイテムの追加
  • マップ上で機能するアイテムの拡充
  • アイテム所持可能数の制限
  • 逃走時の敵体力の引継ぎ
  • 各種メニュー画面の簡素化
  • 戦闘画面の簡素化
  • 罠回避手段の考案
  • お金の取得とその利用法の考案
  • バランス調整

逆にやる必要がないこと、または迷っていること。

  • 攻撃による罠の発見
    • 現状、ターンを消費するリスクが少なく、単にテンポを悪くする
    • プレイヤーの感覚とずれるためなんとなく不完全感がある
  • 空腹度の実装
    • 歩数を意識させると障害物の破壊の作業感が増す
    • 階層数が少ないため、現状ひとつの階層の持つ役割が大きく、急かすと詰む確率が格段に上がる
    • 敵をアイテムから遠ざけるためにでたらめに歩き回ることを抑止できる
    • プレイヤーの生命値として体力以外の要素を組み込むこと自体は面白い
  • 階層ごとの行動回数制限
    • 現状、特に階層に留まるメリットがなく制限の必要がない

おわりに

ゲームの紹介だけだとあまり書くことないな、と思い、 とりあえずやったことや考えたことを片っ端から書いてみました。 わりと勢いだけで書いたので、間違いとか不快な部分とかあったらごめんなさい。

前回、Unityについていろいろ書くとか言いつつ、ツクールの記事で申し訳ない。
次こそUnity関係で記事書こうと思います。



執筆時のRPGツクールMVのバージョン:1.3.5

TypeScriptでRPGツクールMVプラグイン

RPGツクールMVになってからプラグインの言語がJavaScriptになりました。 しかし、JavaScriptは静的解析がしづらく、開発ツールによるサポートが効きづらいのは誰もが知るところ。 そこで、JavaScriptの代替言語AltJSの中でも人気の高いTypeScriptでツクールプラグイン開発をする環境を整えてみました。

ついでにツクールのシリアライザを置き換えて、より多様な形式のオブジェクトをシリアライズできるようにしました。

TypeScriptとは?

f:id:fspace:20161202181938p:plain

TypeScriptはJavaScriptの代替言語AltJS(Altanative JavaScript)のうちの一つです。

JavaScriptはすべてのブラウザがサポートする、Web上唯一のプログラミング言語です。 しかし、現在主流のプログラミング言語の内ではやや特殊な仕様を持つことや動的型付け言語でツールのサポートが受けづらいこともあり、 JavaScriptは大規模Webアプリケーション開発には不向きでした。 そんな中、JavaScriptを置き換える言語として開発されたプログラミング言語群をAltJSと言います。

初めのうちは各ブラウザに直接実行されることを目指したAltJS言語もあったのですが、 各主要ブラウザが息を合わせて新しい言語をサポートするというのはなかなか難しかったようで、 現在はJavaScriptにコンパイルする形式が主流となっています。 そのような経緯の中、まったく別の言語ではなく、 JavaScriptを拡張したような言語仕様であったTypeScriptは比較的高い人気を誇っています。

TypeScriptはMicrosoftが開発した言語で、同じくMicrosoftが開発した言語であるC#の生みの親が開発に関わっていることで有名です。 文法もC#erにとって馴染みやすいものとなっており、それもまた人気の理由の一つです。

一方、JavaScriptに対する歩み寄りの姿勢も強く、ES6やES7の仕様を早いうちから取り込んでいます。 うれしいことに、TypeScriptではES6やES7の仕様に沿った文法で書いたコードを対応するES5のコードにコンパイルすることができます。 ツクールMVの環境はES5相当なので、プラグイン開発ではこの機能を活用します。

名前空間とモジュール

TypeScriptには大きく二つの開発形式があります。 ひとつは名前空間を用いたもの、もうひとつはモジュールを用いたものです。

名前空間は昔から使われているプログラムの管理方法です。 JavaScript自体には名前空間の機能はありませんが、オブジェクトを用いて同等の機能を得る手法は一般的によく用いられています。 TypeScriptではこの名前空間が言語仕様に取り込まれています。 当然ですが、コンパイル結果は従来JavaScriptで用いられてきた手法によるものになります。

モジュールは最近(というほどでもないが)JavaScriptで流行りの管理方法で、ES6の仕様にも採用された方式です。 名前空間が道具を整理して保管するためのものなら、モジュールは道具箱から逐一取り出して使うイメージです。 モジュールはTypeScriptの文法に取り入れられているものの、 ES5以前で使うためにはモジュールローダと呼ばれるスクリプトが必要となります。

ツクールMVの環境はES5相当なので、基本的には名前空間の方が楽なのですが、 今回はモジュールを扱えるようにしてみます。 理由は、モジュールでは依存関係が扱えるためです。 プラグインという形態である以上、スクリプトの配置はユーザに委ねるしかなく、 ここにプラグイン同士の順番についての制約を与えることはできる限り避けたいのです。 そのため、配置順に依らず依存先のプラグインが先に実行されるようにモジュールによる実装を採用します。 ただし、モジュールローダが必要となるため、この部分だけは配置順に制約がかかります。

また、現時点ではプラグインとして配置されていないスクリプトの動的読み込みには対応していません。 これはユーザに対して実行されるスクリプトの存在を明確に示すためにそうしたのですが、 単なるライブラリをプラグインとして配置する手間がかかるので、変更すべきかもしれません。

開発環境の構築

TypeScriptによる開発環境の構築例について説明していきます。

あくまでも一例なので、TypeScriptの設定くらい自分でできるよ、という人は適宜飛ばしながら読んでください。

TypeScriptとVSCodeの設定

TypeScriptがMicrosoft製なので、 Microsoft製のテキストエディタであるVSCode(Visual Studio Code)はTypeScriptに対するサポートが厚いです。 ということで、ここではVSCodeの使用を前提に話を進めていきます。 無料なので、インストールしていない方は公式サイトよりダウンロードしてください。

フォルダ構成

ここではRPGツクールのプロジェクトフォルダ下にtsフォルダを作成し、その内部に開発用のファイルを配置します。

フォルダ構成は次の通り。

  • プロジェクトフォルダ
    • ts
      • (.vscode)
        • 自動生成されるVSCodeの設定フォルダ
      • decl
        • 型定義ファイル(*.d.ts)
      • src
        • TypeScriptソースファイル(*.ts)
      • tsconfig.json
        • TypeScriptの設定ファイル
      • tsconfig-debug.json
        • TypeScriptの設定ファイル(デバッグ用)
    • js
    • その他(*.rpgproject, index.html, 各種フォルダ)

tsconfig.json

tsconfig.jsonはTypeScriptのコンパイラ(tsc)に対する引数をまとめた設定ファイルです。 コンパイル時にこのファイルを指定することでまとめて設定できるようにします。

{
    "compilerOptions": {
        "target": "es5",
        "module": "system",
        "baseUrl": "src",
        "outDir": "../js/plugins",
        "inlineSourceMap": true,
        "noFallthroughCasesInSwitch": true,
        "noImplicitAny": true,
        "noImplicitThis": true,
        "strictNullChecks": true,
        "removeComments": true,
        "experimentalDecorators": true
    },
    "include": [
        "src/**/*",
        "decl/**/*"
    ]
}

設定の意味は次の通り。

設定項目 重要度 説明
target 必須 出力のJavaScriptバージョン。
module 必須 出力のモジュール仕様。
baseUrl 必須 モジュール探索のルートURL。
outDir 必須 出力先のディレクトリ。
(inlineSourceMap) 推奨 ソースマップ(入力-TypeScript-と出力-JavaScript-のコード位置の対応表)を出力ファイル内に埋め込むかどうか。
noFallthroughCases
InSwitch
任意 switch文にてフォールスルー(breakせずに後続のcaseに移行すること)をエラーとするかどうか。
noImplicitAny 任意 暗黙的なany型(型チェックされない型)をエラーとするかどうか。
noImplicitThis 任意 関数(メソッドでない)内にて型指定なしのthisをエラーとするかどうか。
strictNullChecks 任意 undefinedとnullを型として厳密にチェックするかどうか。
removeComments 任意 コメントを自動的に削除するかどうか。
experimentalDecorators 推奨 試験的に導入されているデコレータの機能を有効にするかどうか。

tsconfig.jsonとtsconfig-debug.jsonの設定内容はほぼ同じですが、 tsconfig-debug.jsonの方でのみinlineSourceMapなどのソースマップに関する設定を有効にしておくと、 TypeScriptのソースコードでデバッグができるようになります。

「任意」と書いたオプションについて、もし意味がわからないのであればすべて無効にしておくといいと思います。 これらはそれぞれ厳密な書き方を強制するためのもので、意図を知らずに遵守するのはなかなかに苦痛ですので。

removeCommentsはソースコード中のコメントを削除しますが、/*!...*/と!をつけた形式で書かれたブロックコメントは削除されません。 これを利用して/*!/*:...*/という形式でプラグインコメントを書くと、削除されずにプラグインの説明を書くことができます (ツクールの雑な寛容な処理のおかげで助かった)。

experimentalDecoratorsは必須ではありませんが、 いくつかの機能をデコレータできれいに書けるようにしているため、 基本的には有効にしておいてください。

tasks.json

TypeScriptのコンパイラ(tsc)を直接実行してもいいのですが、 VSCode上からより簡単に実行するためにタスクを定義します。 そのための定義ファイルがtasks.jsonで、.vscodeフォルダ内に配置されます。

tsフォルダをVSCodeで開いた状態で、Ctrl+Shift+P(Windowsの場合)でコマンドパレットを開き、 Configure Task Runnerコマンドを実行すると、自動的にtasks.jsonが生成されます。 これを次のように書き換えます。

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "0.1.0",
    "command": "tsc",
    "isShellCommand": true,
    "showOutput": "silent",
    "tasks": [
        {
            "taskName": "debug",
            "args": ["-p", "./tsconfig-debug.json"],
            "suppressTaskName": true,
            "problemMatcher": "$tsc",
            "isBuildCommand": true
        },
        {
            "taskName": "release",
            "args": ["-p", "./tsconfig.json"],
            "suppressTaskName": true,
            "problemMatcher": "$tsc"
        }
    ]
}

あとはコマンドパレットでRun Taskコマンドを実行し、debugかreleaseかを選択すれば、 それぞれの設定でソースコードがコンパイルされます。 また、コマンドパレットかCtrl+Shift+B(Windowsの場合)でRun Build Taskコマンドを実行すると、 debug設定でコンパイルされます。これはisBuildCommandオプションがdebugに設定されているためです。

コアスクリプトの型定義ファイル

コアスクリプトの型定義ファイルについて、 とりあえずWeb上を探してみたところ公開してくれている方がいたのですが、 入れてみると設定が悪かったのかエラーが大量に発生したので、TypeScriptの学習も兼ねて自分で作成しました。 さすがにコードをがっつり追うのは厳しかったのでnullabilityあたりはかなり怪しいですが、公開しておくのでご自由にお使いください。

github.com

注意点として、lib.d.tsに変更を加えたものが含まれるため、その他のlib系ファイルは使えません。 これはRPGツクールのWindowクラスとグローバルオブジェクトとしてのWindowクラスの競合を防ぐためです。 どうしても他の定義ファイルを使いたい場合には同様の変更を施して置き換えてください。

前述の方法でTypeScriptの環境構築をした方は、declフォルダ内に型定義ファイル(*.d.ts)をすべて放り込んでください。 declフォルダ内の構成はどのようになっていてもかまいません。

モジュールローダ

モジュールローダはES6 Micro Loaderの実装を参考に簡易的なものを自作しました。 モジュールを動的に読み込むことはしないので、元ソースコードからロード機能を削って、TypeScriptで書き直したような代物です (ロードしてないのでモジュールローダというよりモジュールマネージャですね)。

Gist: PluginSystem.ts

System.register形式なので、TypeScriptコンパイラのmodule設定をsystemにしておく必要があります。 動的な依存解決ができる形式としてAMDにしようかとも思ったのですが、 AMDだと関数定義を文字列として解析する必要があるようなのでやめました。

モジュールローダプラグインは他のどのプラグインよりも先に実行される必要があります。 ツクールのプラグイン設定で必ず最初に配置するようにしてください。

前述の方法でTypeScriptの環境構築をした方は、srcフォルダ内にPluginSystem.tsファイルを配置すると型情報を使うことができます。 PluginSystem.ts自体をコンパイル対象にしたくない場合には、TypeScriptコンパイラを使用して型定義ファイルを生成し、 それをdeclフォルダ内に配置してください。

プラグインの記述

ここまでの設定でTypeScriptによるプラグイン開発は可能ですが、 より簡単に書くために、以降では、モジュールローダプラグインのプラグイン開発支援機能について説明します。

TypeScriptの構文に関する説明はここではしないので、公式のドキュメントやWeb上の記事を参照してください。

プラグイン情報の取得

各種プラグイン情報にアクセスするためのプラグインオブジェクトを取得するにはMVPlugin.get静的メソッドを使用します。 MVPlugin.getメソッドはプラグイン名を引数に、その名前のプラグインオブジェクトを返します。

let plugin = MVPlugin.get(__moduleName);

__moduleNameというのはモジュール内からそのモジュールの名前を取得するための変数で、 特別な設定がなされていない限りはプラグイン名が格納されています。

パラメータの取得

プラグインパラメータの取得にはプラグインオブジェクトを使用します。

let plugin = MVPlugin.get(__moduleName);
let params = plugin.parameters.value;

parametersプロパティで取得できるのはパラメータオブジェクトで、 単なる連想配列を得たい場合にはさらにvalueプロパティを参照します。

パラメータオブジェクトには手軽に値を取得するためのメソッドがいくつか用意されています。

let intValue = plugin.parameters.int('XXXCount');

プラグインオブジェクトのvalidateメソッドを使うと、パラメータのバリデーションがシンプルに書けます。

let plugin = MVPlugin.get(__moduleName);
let params = plugin.validate($ => {
    return {
        intValue: $.int('IntParam'),
        floatValue: $.float('FloatParam'),
        boolValue: $.bool('BoolParam'),
        stringValue: $.string('StringParam'),
        UPPER_VALUE: $.upper('UpperParam'),
        lower_value: $.lower('LowerParam'),
    };
});

コアスクリプトの書き換え

ツクールの基本的な拡張スタイルはメソッドの置き換えなので、次のような形式のコードを書くことが多いと思います。

var _Foo_bar = Foo.prototype.bar;
Foo.prototype.bar = function() {
    _Foo_bar.call(this);

    ...
};

TypeScriptでもこの書き方はできるのですが、 せっかくクラスベースで書いている中にprototypeが出てくるのは嫌なので、デコレータを使った代替記法を用意しました。 ただし、デコレータを利用するにはクラスを定義する必要があるため、コード量は少し増えます。

拡張クラスの定義

まずは拡張のためのクラスを定義します。 "~Extensions"のような名前をつけたクラスにMVPlugin.extensionデコレータを付けます。

@MVPlugin.extension(Foo)
class FooExtensions {}

MVPlugin.extensionの第一引数は拡張対象クラスのコンストラクタです。 このため、拡張クラスは対象クラスごとにひとつずつ定義する必要があります。 また、静的メソッドを置き換えたい場合にはMVPlugin.extensionの第二引数にtrueを設定します。

@MVPlugin.extension(Foo, true)
class FooStaticExtensions {}

特定条件下でのみ置き換えたい場合にはMVPlugin.extensionIfを使用してください。

@MVPlugin.extensionIf(Foo, plugin.parameters.bool('Enabled'))
class FooStaticExtensions {}

メソッドを置き換えるには拡張クラス内に次のようなメソッドを定義します。

@MVPlugin.method
public static bar(base: Function) {
    return function(this: Foo) : void {
        base.call(this);

        ...
    };
}

注意すべき点は四つ。

  • @MVPlugin.methodデコレータを付けること。
  • 静的メソッドであること。
  • 置き換える対象と同じ名前のメソッドであること。
  • 置き換える処理の関数を返すこと。

メソッドの引数baseはもともと定義されていた関数です。 これに対してcallapplyを呼ぶことで、元の処理を実行することができます。 拡張対象クラス内に指定した名前のメソッドが存在しない場合には、この値はundefinedとなり、 単純に新しいメソッドが定義されることになります。

プロパティを置き換えたい場合にはMVPlugin.propertyデコレータを使用します。

@MVPlugin.property
public static get baz(this: Foo): number { return this._baz; }
public static set baz(this: Foo, value: number) { this._baz = value; }

プロパティの場合には元の処理を参照することはできません。

また特定の条件下でのみ置き換えたい場合には、MVPlugin.methodIfデコレータやMVPlugin.propertyIfデコレータも使用できます。

型定義の変更

拡張のために新しいメンバーを定義した場合には、型定義を変更する必要があります。 型定義の変更は次のようなコードで行います。

declare global {
    interface Foo {
        _baz: number;
        baz: number;
    }
}

モジュール内でグローバル空間の型定義を変更する場合には、 declare globalでグローバル空間に定義されていることを明示する必要があります。 また、定義済みクラスのインターフェースを拡張する場合にはclassキーワードではなくinterfaceキーワードを使います。

シリアライズ

ツクール標準のシリアライズ機能では、グローバル空間に定義されたクラス以外はプロトタイプオブジェクトが保持されません。 そのため、モジュールに書いたクラスをシリアライズしようとすると型情報が抜け落ちてしまいます。

この問題を解決するため、モジュールローダプラグインを実行すると、 自動的にシリアライズ処理が独自のものに切り替わります。 グローバル空間以外のクラスの型情報が復元できるほか、 循環参照の保持やシリアライズ・デシリアライズイベントの取得ができるようになります。

なお、元のシリアライズ方式との互換性はないためご注意ください。

型情報の登録

グローバル空間以外に定義されたクラスの型情報を復元するには、 あらかじめ型情報を登録しておく必要があります。 登録にはSystem.Typeクラスのregister静的メソッドを使用します。

class Foo {}
System.Type.register(Foo, 'HogeModule');

第一引数がクラスのコンストラクタで、第二引数がモジュール名です。 通常はコンストラクタの関数名がIDとして用いられますが、第三引数にIDを指定するとそちらが優先されます。

また、プラグインオブジェクトとデコレータを用いた指定もできます。

@plugin.type
class Foo {}

IDを指定したい場合にはtypeAsデコレータを使用します。

@plugin.typeAs('FOO')
class Foo {}

イベントハンドラ

クラスに特定の名前のメソッドを定義しておくと、対応するタイミングで呼び出されます。

シグネチャ 説明
onSerialize():void シリアライズ直前に呼び出されます。
onDeserialize():void デシリアライズ直後に呼び出されます。
toJSON():any シリアライズ時に呼び出され、戻り値をオブジェクトの代わりにシリアライズします。
fromJSON(data:any):void デシリアライズ時に呼び出され、引数にデシリアライズしたデータが渡されます。

fromJSONが定義されている場合、オブジェクトはクラスのプロトタイプオブジェクトがセットされただけの状態で初期化されます。 fromJSONにてthisに値を代入し、オブジェクトを復元してください。

シリアライズ形式

各オブジェクトはオブジェクトテーブルに分離して保存されることで参照を保持します。

シリアライズ後のJSONデータは次のような形式となります。

{
    root: { "@ref": 0 },
    table: [
        {
            "@": "@HogeModule.Foo",
            "_": {
                "_baz": 100,
                "_fuga": { "@ref": 1 }
            }
        },
        ...
    ]
}

記述例

記述例。処理に特に意味はないです。

/*!
/*:
 * @plugindesc サンプルプラグイン
 * @author F_
 *
 * @param MyParam
 * @desc 私のパラム。
 * @default True
 */

import { Foo } from 'HogeModule';

let plugin = MVPlugin.get(__moduleName);
let params = plugin.validate($ => {
  return {
    mine: $.bool('MyParam')
  };
});

declare global {
  interface Game_Map {
    readonly mine: MyClass;
  }
}

@plugin.type
export class MyClass {
  public constructor(public mine?: boolean);
  public sweep():void { delete this.mine; }
}

@MVPlugin.extension(Game_Map)
class GameMapExtensions {
  @MVPlugin.property
  public static get mine(this: Game_Map): MyClass { return new MyClass(params.mine); }

  @MVPlugin.method
  public static checkPassage(base: Function) {
    return function(this: Game_Map, x: number, y: number, bit: number): boolean {
      this.mine.sweep();
      return base.call(this, x, y, bit);
    };
  }
}

let foo = new Foo();
foo.bar();

前回記事のプラグインでも利用しているので、そちらもご参照ください。

www.f-sp.com

おわりに

せっかく環境整えたのでとりあえず公開してみたのですが、 こうまでしてTypeScript使いたい人ってどれくらいいるんでしょう? 他人のスクリプト利用してまで使おうなんて人は限りなく0に近いような・・・。

まあ、読み物として楽しんでもらえたなら幸いです。



執筆時のRPGツクールMVのバージョン:1.3.4

続・RPGツクールでローグライクな雰囲気を醸してみた

なんやかんやでまた書き始めたRPGツクールMVのローグライクプラグイン。

www.f-sp.com

JavaScriptでの整理が難しくなったのでTypeScriptに切り替えたり、 循環参照を保つためにシリアライザを置き換えたりしていたのですが、 それでもなお拡張に限界を感じたため、 とりあえずここまでの成果を曝して、また無期限延期とすることにしました。

ああ、やっぱり今回も駄目だったよ。

進捗

基本的に内部をいじっていたので、見てわかる変化は階段と敵が増えたくらいです。 機能としては階層管理機能とターン制の導入になります。

f:id:fspace:20161201153300g:plain
階層移動

f:id:fspace:20161201153301g:plain
ターン制

仕様

一応プラグインとして作る以上、エディタから設定しやすいものにしたいな、と思いいろいろと考えていました。

階層管理

階層管理(どの階層にどういったフロアを割り当てるか)は専用のタグ<blueprint>を設定したマップを作成することでできるようにしています。

二次元のマップの左上から順に一次元のインデックスを割り当て、階段などで特定階層へと移動する際には、 移動先階層と一致するインデックスの位置に設定されたイベントのコマンドを実行するという仕様になっています。 ここに特定マップへと移動するコマンドを書き込んでおけば、階層とフロア設定が関連付けられるという仕組みになっています。 また、イベントコマンドならなんでも書き込めるので、体力回復やダンジョンでない通常のマップへの移動などもできます。

インデックスの0番(一番左上)は特殊な番号として扱っていて、 現在は階層管理マップに直接移動したときのダンジョン初期化用として使っています。 現状未実装ですが、ひとつのイベントには複数のイベントページを作成できるので、 階層管理マップ範囲外のインデックスにアクセスした際のデフォルトコマンドの設定等への活用も考えていました。 これを実装すると、ダンジョン最後の階層の階段を上った際に実行するコマンドの設定ができるようになります。

f:id:fspace:20161201153258p:plain
全8階層の階層管理マップ

フロア管理

フロア管理(フロアのデザインや敵の設定等)もまた専用のタグ<dungeon>を設定したマップを作成することでできるようにしています。

マップ上の好きな場所に対応する設定を施した上で、専用のタグを設定したイベントを配置することで設定が読み込まれます。 例えば、フロアの壁に使用するタイルを設定したい場合には、好きな場所に壁用のタイルを設定し、その場所に空のイベントを配置、 イベントのメモ欄に<wall>タグを設定することで、そのタイルがダンジョン生成時に壁となった場所に配置されます。 敵や階段の設定も同様で、好きな場所に配置したイベントに<enemy>タグや<stairs>タグを設定し、適当な画像を設定しておくと、 そのイベントが自動的に複製されて、ダンジョン内に配置されます。

また、生成されるダンジョンの大きさはフロア管理マップの大きさと同じになります。 ダンジョン生成に使われるアルゴリズムもフロア管理マップにタグをつけることで切り替えられるような仕組みになっています (現状テスト用のシンプルなアルゴリズムひとつしかありませんが)。

f:id:fspace:20161201153259p:plain
森っぽいフロア管理マップ

拡張の限界

言い訳という名の愚痴。

階層やフロアの管理などをつくっている段階まではまだよかったのですが、 敵を表示するためにイベントを動的に生成し始めたあたりから管理に限界を感じ始めたため、実装を中止することにしました。

ツクールをローグライク化するためには、なかなかに大規模な変更を加える必要があるため、 直接コアエンジンのクラスを拡張していくと他のプラグインとの競合が発生しやすくなってしまいます。 そこで、今回はローグライク関連のモデルをツクールとは独立で作成し、 そのモデルに合わせてツクール側の要素を調整することでローグライク化を実現しようとしていました。 が、結局このツクール側の要素を調整するという部分が非常に面倒だったのです。

基本的にツクールはツクール自体の仕様を実現するためだけにできており、拡張性はほとんど考慮されていません。 具体的な例を挙げると、イベントの動的生成ができません。 マップにイベントを追加する機能がないのはもちろん、 そもそもイベントの内容はすべてグローバル変数として読み込まれているデータを直接参照していて、新たな挙動のイベントインスタンスが作れません。 グローバル変数のイベントデータはシリアライズされないので、ここにイベントデータを追加してもセーブ&ロードで消えてしまいます。 また、クラスの分割単位が非常にでかいので、同じインターフェースのクラスをつくって誤魔化すことも現実的ではありません。 さらには、これらの問題を解決してイベントインスタンスを生成したとしても、 マップの初期化がすでに終わっているので、プライベート変数を繰り、スプライトの追加等々の調整をする必要があります。

・・・とまあ、何かしようとするたびにやたらとコストがかかるのです。 そして変更に弱いというリスクも抱えます。

もともと想定されていない機能を盛り込むわけなので、もちろんある程度のコストは仕方のないことなのですが、 もう少しオブジェクト指向っぽい設計になっていればなぁ、と思うことがたびたび。 さすがにコスパが悪いのでここらで切り上げることにしました。

ソースコード

前回と同じとこに置いてあります。

github.com

ライセンスはMITです。

おわりに

今回、とりあえず一旦休止しますが、もしもいい設計案とか思いついたらまた始めるかもしれません。 前回も、もうやめるとか言っておきながらやってますので・・・。

次回の記事は、冒頭で述べたTypeScriptやらシリアライザやらの話になる予定です。



執筆時のRPGツクールMVのバージョン:1.3.4

【RPGツクールMV】 タイル仕様

前回とは打って変わって今回はただメモを曝すだけです。 特に解説はしません。

オートタイルを動的に表示したい場合なんかに必要なタイルの仕様についてです。 マニュアルの素材規格と照らし合わせながら見るとわかりやすいと思います。

www.f-sp.com

【修正 16/09/28】マップデータ仕様の層の種類にリージョンIDが漏れていたため修正。

タイルID仕様

タイルIDの構成

  • オートタイル
    • (セットの開始番号) + (種別ID) * 48 + (形状ID)
  • それ以外
    • (セットの開始番号) + (タイル番号)

タイルID区分

セット 番号
B 0~
C 256~
D 512~
A5 1536~
A1 2048~
A2 2816~
A3 4352~
A4 5888~
~8192

オートタイル仕様

  • セットAのパーツ1~4まで (A1~A4)
  • タイル1種類あたり48のID領域が割り当てられる。
  • パーツ番号と種別IDから基準位置とテーブルを決定。
  • 形状IDとテーブルから4つのサブタイルを決定。
  • サブタイルを組み合わせて一枚のタイルとして描画。

種別ID (kind)

  • A1のみ特殊な割当
  • A2~A4は順番通り
A1

(表の値) + 0

###### ## ###### ##
### 0 2 4 5
### 1 3 6 7
### 8 9 12 13
### 10 11 14 15
A2

(表の値) + 16

## ## ## ## ## ## ## ##
### 0 1 2 3 4 5 6 7
### 8 9 10 11 12 13 14 15
### 16 17 18 19 20 21 22 23
### 24 25 26 27 28 29 30 31
A3

(表の値) + 48

## ## ## ## ## ## ## ##
## 0 1 2 3 4 5 6 7
## 8 9 10 11 12 13 14 15
## 16 17 18 19 20 21 22 23
## 24 25 26 27 28 29 30 31
A4

(表の値) + 80

## ## ## ## ## ## ## ##
### 0 1 2 3 4 5 6 7
## 8 9 10 11 12 13 14 15
### 16 17 18 19 20 21 22 23
## 24 25 26 27 28 29 30 31
### 32 33 34 35 36 37 38 39
## 40 41 42 43 44 45 46 47

形状ID (shape)

基本構造 (floor)
  • 4方向隣接パターン (16通り)
  • 3方向隣接パターン (16通り)
  • 2方向隣接パターン (10通り)
  • 1方向隣接パターン (4通り)
  • 非隣接パターン (1通り)
  • 代表パターン (1通り)

計48通り

f:id:fspace:20160925125449p:plain
f:id:fspace:20160925125452p:plain

非隣接パターン(左)と代表パターン(右)

集合パターンのみ (wall)
  • 4方向隣接パターン (1通り)
  • 3方向隣接パターン (4通り)
  • 2方向隣接パターン (6通り)
  • 1方向隣接パターン (4通り)
  • 非隣接パターン (1通り)
f:id:fspace:20160925125450p:plain

計16通り

滝 (waterfall)
  • 両方向隣接パターン (1通り)
  • 片方向隣接パターン (2通り)
  • 非隣接パターン (1通り)

計4通り

f:id:fspace:20160925125451p:plain

タイルセット仕様

構成

ルートは配列。0番目は未使用値でそれ以降は番号に対応した各タイルセットの情報。

プロパティ 説明
id ID番号
flags 各タイルIDに対応したフラグの配列 (8192)
mode 0: フィールドタイプ, 1: エリアタイプ
name 名前
note メモ
tilesetNames A1, A2, A3, A4, A5, B, C, D, E の順のタイルセット画像名

フラグ仕様

ビットマスク 説明
0000 0000 0000 1111 通行不可フラグ(上位から上右左下)
0000 0000 0001 0000 裏側通行フラグ
0000 0000 0010 0000 梯子フラグ
0000 0000 0100 0000 茂みフラグ
0000 0000 1000 0000 カウンターフラグ
0000 0001 0000 0000 ダメージ床フラグ
0000 0110 0000 0000 船移動不可フラグ(上位から大型船・小型船)
0000 1000 0000 0000 飛行船着陸不可フラグ
1111 0000 0000 0000 地形タグ

マップデータ仕様

三次元の配列を一次元配列化して保存。要素としてタイルIDを格納。

説明
x x方向の座標
y y方向の座標
z 層の種類
index = x + (y * width) + (z * width * height);

層の種類

番号 説明
0 下層
1 下層重ね合わせ
2 上層1
3 上層2
4 影ビットフラグ(下位から左上/右上/左下/右下)
5 リージョンID

下層重ね合わせはA1-B,A1-C,A2-Bが該当。

RPGツクールMV プラグイン開発の手引き

前回の記事にて公開したプラグインの開発中に得た知識を基に、 これからRPGツクールMVのプラグイン開発をしようと考えている人向けにコアエンジンの概観について記しておきます。 正直、今更感はありますが、誰かの理解の一助となれば幸いです。

www.f-sp.com

対象読者

RPGツクールのプラグイン開発はプログラミング初心者を対象としたものではありません。 したがって、内容をシンプルにまとめるため、本記事もある程度の技術を持った読者を対象として書きます。

対象とする技術レベルは次の通り。

  • JavaScriptによるプログラミングができる
    • プロトタイプチェーンの仕組みを理解している
    • プロトタイプベースの継承を理解している
  • ゲームプログラムの基本的な処理について理解している

要はJavaScriptの文法については一切説明しないし、 一般的なゲームプログラミングで使用される用語は説明なしに使うよ、ということです。

プラグインの基本中の基本

プラグインの適用方法

RPGツクールにプラグインを適用するには次の三つが必要となります。

  • プラグインファイルの先頭にプラグインであることを示すコメントを記述すること
  • プラグインファイルをプロジェクトディレクトリのjs/plugins/以下に配置すること
  • エディタのプラグイン設定でプラグインを選択し、ONにすること。

先頭に記述するコメントは次のようなものになります。

/*:
 * @plugindesc プラグインの概要
 * @author 製作者名
 *
 * @help
 * ヘルプの内容
 */

最初の行に:(コロン)があることに注意。

このあたりは公式マニュアルにしっかりと記述があるので、 詳しいことはそちらを参照するのがよいと思います(パラメータの設定方法などもあります)。 マニュアルはエディタのヘルプメニューから開けます。

プラグインによる拡張

RPGツクールのプラグインにおける拡張方法の基本は関数の置き換えです。

RPGツクールのコアエンジンには特に拡張を意識したような仕組み(例えば、イベントハンドラの登録など)はありません。 その代わり、非常に細かく分割された関数が大量に定義されています。

RPGツクールによるゲームの動作は

  1. コアエンジンの定義
  2. プラグインの実行
  3. コアエンジンの起動

という流れになっているため、プラグイン内でコアエンジンの定義を置き換えると、 その置き換えた動作でゲームが実行されます。 これがプラグインによる拡張の仕組みです。

プラグインによる拡張はだいたい次の二通りの形式になります。

  • もとの処理を内部で呼び出す関数に置換する
  • もとの処理の一部または全部を書き換えた関数に置換する
var _Foo_bar = Foo.prototype.bar;
Foo.prototype.bar = function() {
    // 追加の処理

    _Foo_bar.call(this);

    // 追加の処理
};
Foo.prototype.bar = function() {
    // もとの処理のコピー

    // alert('bar');
    alert('baz');

    // もとの処理のコピー
};

これら二つの大きな違いはプラグイン同士の衝突が発生しやすいか否かです。 前者は別のプラグインが同じ関数を変更しても動作する可能性が高いですが、 後者は他のプラグインの動作を破壊してしまう可能性が高いです。 可能な限り前者の方法で対処し、それが不可能な場合のみ後者を用いるのがよいと思います。

コアエンジン概要

プラグインの拡張方法で示した通り、 プラグインの拡張は既存コードの置き換えのため、コアエンジンの理解が不可欠です。 それゆえ、本記事の以降の内容はこのコアエンジンの解説になります。

動作の仕組み

動作の仕組みを理解するため、まずはindex.htmlを確認してみます *1。 すると、body内に大量のscriptタグがあることがわかります。

これらで読み込むのは順に、

  1. 外部ライブラリ
  2. "rpg_"から始まるコアエンジンの定義
  3. プラグインに関する情報を持った実質JSONファイル
  4. 起動コード

となっています。

今度は起動コードにあたる"main.js"を確認してみると、非常に簡潔なコードで実行が開始されることがわかります。

一行目は各プラグインを実行するコードです。 次の行ではwindow.onloadにイベントハンドラを設定しています。 その内部では、最初のシーンとしてScene_Bootを与え、SceneManagerrun関数を呼び出すことでゲームを起動しています。 どうやらこのSceneManagerがゲームループの管理をしており、 「シーン」というものがゲーム画面を構成するルートとなる要素のようです。

SceneManagerの定義は"rpg_managers.js"内にあります。 run関数から処理を辿っていくと、requestAnimationFrameという関数の呼び出しコードに到達します。 これはJavaScriptのAPIで、指定したコールバック関数を再描画時に呼び出すというものです。 これにupdate関数を繰り返し登録することでゲームループを実現しているようです。

ゲームループ内では、一般のゲームプログラム同様に、 ユーザ入力の更新、ゲーム状態の更新、描画命令の発行を行っています。

描画の仕組み

RPGツクールではあらゆる描画をPixi.jsという外部ライブラリを用いて行っています。

Pixi.jsはhtml5のcanvas要素を用いた描画とWebGLを用いた描画の二種類の描画方法をサポートしています。 RPGツクールではプラットフォームによってこれら二種類の描画を使い分けているようです。 可能であれば高速なWebGLを、そうでなければcanvasを用いた描画をしているのでしょう。 もしもPixi.jsの機能を用いた拡張を考えるのであれば、これらのモードを意識する必要があります。

Pixi.jsにおけるレンダリングはシーングラフのようなものを構成することで行います。 Pixi.jsのレンダラはシーングラフを受け取り、それを辿りながら各ノードオブジェクトを画面に描画していきます。 シーングラフのノードはPIXI.DisplayObjectまたはそれらを束ねるPIXI.Containerになります(Compositeパターンによる構成)。

RPGツクールではPixi.jsの機能を"rpg_core.js"内でラップしており、 各基本クラス *2PIXI.Containerを継承しています。 継承階層は次の通り。

  • PIXI.Container
    • PIXI.Sprite
      • PIXI.extras.TilingSprite
        • TilingSprite
      • Sprite
    • Tilemap
      • ShaderTilemap
    • ScreenSprite
    • Window
    • WindowLayer
    • Weather
    • ToneSprite
    • Stage

コアエンジンのコードを読んでいると度々addChildremoveChildという関数の呼び出しが現れます。 これはPIXI.Containerの関数で、シーングラフの子の登録・削除を行う関数です。 またchildrenはシーングラフの子を参照するプロパティです。

すべてのシーンの基底となるScene_BaseStageを継承しており、 SceneManager内ではこのScene_Baseを継承したオブジェクトをレンダラへと渡すことで、 その子となっているWindowSpriteを含む、画面上のあらゆるオブジェクトを描画しています。

構成概要

RPGツクールのコアエンジンは次の六つのファイルから成っています。

ファイル名 概要
rpg_core.js 外部ライブラリやWeb仕様に対するラッパー。描画関連が主だが、入力制御や音声制御、ユーティリティなどもここ。
rpg_managers.js 各種管理クラスの定義。主にエディタで設定した内容の取得用。'$'から始まるグローバル変数の定義もここ。
rpg_objects.js ゲーム内オブジェクトのモデルの定義。ゲームロジックのほとんどはここ。唯一セーブ時に状態が保存される。
rpg_sprites.js スプライトの定義。表示位置更新などの処理もすべてここなのでプレゼンテーションロジックの定義といった方がいいかも。
rpg_windows.js ウィンドウの定義。ウィンドウに関してはドメインロジックもプレゼンテーションロジックもすべてここ。
rpg_scenes.js シーンの定義。スプライトやウィンドウの管理やゲーム状態の更新など。他に書きづらいことは結構雑多に書いてある。

名前空間のような仕組みは特に用いられておらず、 "rpg_core.js"と"rpg_managers.js"を除く、 各ファイルに定義されたクラスには対応したプリフィックスが付いています。

ファイル名 プリフィックス
rpg_objects.js Game_
rpg_sprites.js Sprite_ または Spriteset_
rpg_windows.js Window_
rpg_scenes.js Scene_

rpg_core.js

"rpg_core.js"は外部ライブラリやWeb仕様に対するラッパークラスを提供します。 ここには二種類の注目すべきクラス群があります。

ひとつめは他の要素の基底クラスとなっているクラス群。 各スプライトの基底となっているSprite、 各ウィンドウの基底となっているWindow、 各シーンの基底となっているStage、 とすべてこのファイル内で定義されています。

ふたつめは直接呼び出せるプロパティや関数を持った静的クラス群。 例えば、画面の大きさを取得するためのプロパティなどを持ったGraphicsや、 入力を扱うInputTouchInputはこのファイル内で定義されています。 また、NumberArrayに対するユーティリティ関数も定義されていたりします(clampなんかは覚えておくと非常に便利)。

他に覚えておくと役に立つかもしれないクラスとして、 画像を表すBitmapやタイルに関する情報を扱うTilemapなんかがあります。

rpg_managers.js

"rpg_managers.js"は各種管理クラスを提供します。

エディタで設定した内容を取得したい場合などには、 ここに対応したクラスが存在する可能性が高いので探してみるとよいかもしれません。 ファイルの読み込み・書き込み関係もここです。

しかし、おそらくこのファイルの内容で最も重要なのは、 '$'から始まる名前で定義されているグローバル変数です。 RPGツクールではゲーム状態をシーンのプロパティなどではなく、グローバルスコープの変数として記憶しています。 それに当たるのが'$'から始まる各種変数です。

'$data'から始まる変数はエディタで作成したJSONデータをそのままオブジェクト化したものです。 '$dataMap'などコンテキストによって読み込み対象が変わることはありますが、 基本的に読み込み専用で、値の変更は保存されません。

一方、'$game'から始まる変数はまさにゲームの状態を表しており、 その多くがセーブ時にシリアライズされて保存、ロード時にデシリアライズされて復元されます。 '$game'から始まる変数には"rpg_objects.js"にて定義されているモデルオブジェクトが格納されています。

rpg_objects.js

"rpg_objects.js"はプレイヤーや敵といったゲーム内オブジェクトのモデルを提供します。

ここで定義されているモデルの多くは、 "rpg_managers.js"内にて'$game'から始まるグローバル変数にインスタンス化された値が記憶されています。 そのため、任意の場所から容易に値の取得・操作が可能です。 また、これらグローバル変数の値はセーブ時にシリアライズされて保存されるため、 これらのモデルに値を書き込んでおくとセーブデータに記録されます。

モデルの内容は特に共通項目もなく、量も膨大なためここでは説明はしません。 必要に応じて、対応するモデルのコードを読んでください。

ひとつだけ覚えておくとよいかもしれないクラスとしてGame_Interpreterがあります。 これはイベントコマンドの実行クラスです。 もしも実行したい処理に似た処理がコマンドとしてもともと提供されているのであれば、 そのコマンドの処理内容を読むことで動作理解のとっかかりを得ることができるかもしれません。

rpg_sprites.js

"rpg_sprites.js"はスプライトの定義を提供します。

スプライトの定義というと、描画基盤を提供するクラス群について記述しているように聞こえますが、 それらに相当するものはPixi.jsや"rpg_core.js"がすでに提供しています。 どうやらRPGツクールにおける「スプライト」とはモデルを参照して見た目を決定するビューのことを指すようです *3。 したがって、このファイルにはプレゼンテーションロジックが記述されています。

スプライトでは毎フレーム呼び出されるupdate関数内でモデルの変更を確認し、 それに対応して見た目を変化させています。 このupdate関数はスプライトなら必ず呼び出される仕様のように見えますが、 実際には親のシーンが自身のupdate関数内ですべてのchildrenに対してupdate関数を呼び出しているだけなので注意。 つまり、スプライト内で階層を持った場合にはupdate関数が勝手に呼び出されることはありません。

このファイル内に定義されているクラスには、 "Sprite_"から始まるスプライトと"Spriteset_"から始まるスプライトセットがあります。 スプライトセットは単純に複数のスプライトを子に持つ、スプライトの集合です。 マップ画面を表すSpriteset_Mapと戦闘画面を表すSpriteset_Battleしかないので、 それだけ覚えておけばよいと思います。

すべてのスプライトクラスは"rpg_core.js"内で定義されているSpriteクラスを継承しています。 ファイルの先頭にSprite_Baseという、 いかにもあらゆるスプライトの基底となっていそうなクラスがありますが、 実際には一部のクラスしか継承していないので注意。

rpg_windows.js

"rpg_windows.js"はウィンドウの定義を提供します。

すべてのウィンドウクラスはWindow_Baseクラスを継承しており、 さらにそのWindow_Baseクラスは"rpg_core.js"内で定義されているWindowクラスを継承しています。

Windowクラスは背景やフレームなど複数のスプライトを内部的に持ちますが、 独自ウィンドウを作成する場合に特に重要となるのはcontentsという名前のプロパティで公開されているBitmapクラスのオブジェクトになります。 これはウィンドウ内部に表示する内容を表す画像で、 この画像を書き換えることによってウィンドウに様々なものを表示することが可能となります。

ウィンドウの書き方については、 似通った機能を持つウィンドウを探して、それを真似して書くのがよいと思います。 項目選択のためのWindow_Selectableと、 それを継承してコマンド実行機能を実装したWindow_Commandは覚えておくと記述が楽になるかもしれません。

rpg_scenes.js

"rpg_scenes.js"はシーンの定義を提供します。

すべてのシーンクラスはScene_Baseクラスを継承しており、 さらにそのScene_Baseクラスは"rpg_core.js"内で定義されているStageクラスを継承しています。

シーンのライフサイクルは"rpg_managers.js"のSceneManagerによって管理されています。 SceneManagerから呼び出される関数は次の通り。

関数名 タイミング
create シーンの生成時。
terminate シーンの終了時。
start シーンの開始時。最初のupdate呼び出し前。
update 毎フレーム。
stop シーンの停止時。次の移行シーンが決定した時点。
isReady シーンの開始前。シーンが開始可能かどうかを判定する(画像の読み込み待ち等)。
isBusy シーンの停止後。シーンの移行を延期するかどうかを判定する(遷移エフェクト等)。

createでスプライトやウィンドウを生成して子として登録、 updateでゲーム状態と子の更新、というのが基本的なシーンの書き方となります。

シーンの遷移もまたSceneManagerによって管理されます。 シーン遷移に関するSceneManagerの関数には次のものがあります。

  • goto
  • push
  • pop

名前から想像できる通り、 gotoは単なる移行、 pushは現在シーンをスタックにプッシュしてから移行、 popはスタックからポップしたシーンに移行です。 注意すべきはスタックの状態とは無関係に、常にひとつのシーンだけが実行されるという点です。 例えば、マップ画面のシーンからpushでメニュー画面のシーンを開くと、マップ画面のシーンは破棄されます。 popの呼び出しで再度マップ画面のシーンへと戻れますが、このときマップ画面のシーンは再生成されます。

おまけ:各スプライトの前後関係

ソースコード内に各スプライトのZ値に関する記述があったのでメモ。

Z値 内容
0 Lower tiles
1 Lower characters
3 Normal characters
4 Upper tiles
5 Upper characters
6 Airship shadow
7 Balloon
8 Animation
9 Destination

おわりに

RPGツクールMVのプラグイン製作についてググったところ、 プログラミング初心者向けの記事ばかりで、 プログラマ向けの内容が少なかったので一通りの内容について書いてみました。 まだ、プラグイン製作を始めて日が浅いので、何か間違い等あれば指摘してもらえるとありがたいです。

正直、RPGツクールのプラグインもJavaScriptという言語自体も初心者向けではないと思うのですが、 RPGツクールからプログラミングを始める人って多いんでしょうか? 初心者向けの記事をいくつか読んだところ、 解説者自身も初心者ということも多いようでなんだかなぁ、という感じです。 RPGツクールほどシンプルに設計され、規格化された素材に溢れた環境もないのだから、 プログラミングさえしやすければいい教育環境になると思うのだけれど。



執筆時のRPGツクールMVのバージョン:1.3.1

*1:著作権の問題があるため基本的にコードは載せません

*2:プロトタイプベースの継承なので「クラス」と呼ぶのは適切でない気もしますが、他に適当な言葉が思い当たらないため本記事では「クラス」と呼びます

*3:"rpg_core.js"内ではScreenSpriteやToneSpriteといったクラスが定義されており、これらはただの描画基盤なので若干不正確かもしれません

RPGツクールでローグライクな雰囲気を醸してみた

夏の終わりにRPGツクールMVのセールをしていたので思わず衝動買い。 おまけでシレンのグラフィックがついてきたので、 これはローグライクゲームをつくれというメッセージだと受け取り、プラグインの製作に取り掛かりました。

実は人生初ツクールです。
前々から気になってはいたもののコスパの面で二の足を踏んでいました。
そんなわけでツクール初心者なので、なにか見当違いなこと書くかもしれませんが、 その辺は温かい目でスルーしてください。

【追記 2016/12/01】続編書きました。

www.f-sp.com

RPGツクールのプラグイン

RPGツクールのプラグインというので、 最初はScratchプラグラミンのようなものを想像していたのですが、 どうやらそのような機能はツクール内では「イベント」として提供されているようです。

ではプラグインとは何を指すのか、 ということで、デフォルトで入っているプラグインを覗いてみたところ飛び込んできたのが次のコード。

var _Scene_Menu_create = Scene_Menu.prototype.create;
Scene_Menu.prototype.create = function() {
  _Scene_Menu_create.call(this);

  // 略...
};

これは・・・プラグイン?

どうも自分の想像していた拡張ポイントにイベントハンドラを設定するような方式ではなく、 好きな関数を置き換えてどうぞという非常に自由度の高いAPIフック的な方式らしいです。

言語は闇の深いJavaScript。 マルチプラットフォームが叫ばれる昨今、選択肢がないので仕方ないといえば仕方ないですが。

しかし、実際にプラグインを書き始めてみるとそんなことよりなによりもドキュメントがないことが非常につらい。 一応ヘルプの付録にリファレンスらしきものがついているのですが、 ほんの一部のクラスしか書かれていない上に、識別子についての説明が一行書いてあるくらいなのでほぼ役には立ちません *1。 実質ドキュメントはソースコードです。 そして、JavaScriptなので定義や参照箇所の自動検出がうまく働きません。

ただそんな状況を鑑みてか、ソースコードは比較的きれいに整理されているため、 当たりをつけて検索をかければ動作がだいたいわかるようにはなっているようです。 まあ、その当たりをつけられるようになるまでがとてつもなくきつかったのですが・・・。

つくったもの

そんなこんなで苦労しながら二週間ほどかけて出来上がったものがこちら。

f:id:fspace:20160922161036g:plain

ローグライクというか、不思議のダンジョンシリーズですね。

本当に雰囲気だけなので、ターン制の仕組みすらできてません。 ダンジョン生成アルゴリズムも簡易的なものです。 マップのメモ欄に<dungeon>と書くと、 マップ読み込み時にダンジョンを生成する仕様になっています。

やったことはこんな感じ。

  • プラグイン製作支援機能
  • デバッグ支援機能
  • ズーム機能
  • 八方向移動
  • 方向転換等の入力制御
  • 円形または矩形の覗き穴エフェクト
  • マップ画面へのHP等の表示
  • オートタイルの配置
  • 簡易ダンジョン生成アルゴリズムの実装

このうち、いくつかはローグライク以外でも使えそうなので一応紹介しておきます (詳細な説明はプラグインのヘルプやサンプルを参照してください)。

プラグイン製作支援機能

どうも標準のプラグイン開発環境が物足りなかったので、 欲しい機能をひとまとめにしたプラグインの製作を支援するためのプラグイン。

追加される機能は次の通り。

  • プラグインの依存および実行順序解決
  • 名前空間およびスコープ
  • シリアライズの拡張

プラグインの依存および実行順序解決

プラグインの機能を細かく分割して書いていると、別のプラグインに依存するプラグインというのがでてきます。 このとき、依存先のプラグインが存在しない場合にエラーをだしたり、 依存先のプラグインよりもあとに実行するようにしたりする必要があるのですが、 現状これをツクールエディタ上以外でやるのは手間がかかります。

そこで、これらを自動で行うための機能を実装しました。 使用方法はプラグイン内でPluginSystem.require('依存先のプラグイン名');と記述し、コールバック関数を登録するだけです。 すべてのプラグイン実行後に依存関係を考慮しながらシステムがコールバック関数を順に実行します。

名前空間およびスコープ

JavaScriptには名前空間がありません。 そのためオブジェクトそのものを名前空間として用いることがあるようなのですが、 これは階層構造を持ち始めると、支援関数なしには扱いづらいです。

そこで、階層構造を持った名前空間を扱うための名前空間オブジェクトと、 それら名前空間に定義されたオブジェクトを展開するためのスコープオブジェクトを定義しました *2。 パフォーマンスの問題はありますが、スコープオブジェクトのextract関数を呼ぶと、 特定の名前空間のオブジェクトがすべてプロパティとして定義されるため記述が楽になります。

シリアライズの拡張

RPGツクールはJavaScript標準のJSONシリアライザを用いたセーブ&ロードの仕組みを持っていますが、 この仕組みにより正しく復元するためにはコンストラクタをグローバルスコープに定義する必要があります。 これは前述の名前空間の機能と非常に相性が悪いです。

そこで、指定したクラスのコンストラクタを記憶し、 任意のクラスをシリアライズ可能にする機能を実装しました。 ついでに、"onSerialize"や"onDeserialize"という名前の関数プロパティを定義しておくと、 シリアライズ前やデシリアライズ後に自動的に呼び出されるようにしました。

デバッグ支援機能

RPGツクールにはもちろん入力を取得するための機能が備わっているのですが、 これらはゲーム実行のためにチューニングされており、 デバッグ時にちょっとした操作をするためのトリガーとしては使いづらい仕様です。

そこで、登録したコマンドを任意のタイミングで呼び出すための仕組みを実装しました。 ゲーム実行中にタブキー(変更可)を押すと、コマンド入力のためのボックスが現れます。 そこにコマンドを入力してエンターキーを押すと、コマンドが実行されます。 簡単な履歴機能も付いています。

f:id:fspace:20160922161035g:plain

ズーム機能

RPGツクールにもズーム機能はついているのですが、 これは指定した点を原点に拡大縮小する機能であり、何かに注目するときのズームとしては不適です。

作成したプラグインの提供するズーム機能は、 指定した点が画面中央に来るように画面を拡大縮小するものです。 何かに注目してほしい場合にはこちらを使うと直感的に正しくなると思います。

ちなみにどちらのズームも座標指定はスクリーン座標で行います。 これは、マップ座標に対応しようとすると、マップ画像の生成部分にまで影響が及んでしまうためです (特にプラグインでこれをやるとなると他のプラグインとの競合が発生しやすくなります)。

f:id:fspace:20160922161034g:plain

八方向移動

RPGツクールでは基本的に四方向にしか移動できません。 これはグラフィックの制作コストを抑えるという意味でよい規格化ではあるのですが、 ローグライクゲームをつくる上では非常に困ります。

作成したプラグインではこの四方向移動の制限を取っ払います。 しかし、当然斜め方向のグラフィックがないため見た目は奇妙になります。 少しでも斜めを向いているように見せかけるため、剪断変形のオプションを用意していますが、 まともな見た目にしたいのであれば、さらに改造して斜め方向のグラフィックを表示する必要があります。

このプラグインを適用するとキャラクターの向きを取得した際に八方向の値が返るようになります。 これはAPI仕様の変更であり、他のプラグインの挙動を破壊しやすいため注意が必要です。

方向転換等の入力制御

RPGツクールが想定するゲーム中において、移動と時間の流れというのは特に関係がありません。 しかし、ローグライクゲームでは移動した分だけ時間が進みます。 そのため、慎重な移動操作が要求されます。

作成したプラグインでは操作ミスを防ぐための制限や特殊な入力方法を提供します。 コントロールキーを押しながら、方向キーを入力すると移動はせずに方向転換のみ行います。 コントロールキーとシフトキーを同時押ししている状態で方向キーを入力しても上下左右へ移動しなくなります。 すなわち、八方向移動が可能な場合には斜め移動のみ可能になります。 タッチ入力でプレイヤーから一歩以上離れた位置をタップしても移動しなくなります。

ソースコード

正直、未完成品なので公開する意味もあまりないのですが、 もしかすると副産物のほうには需要があるかもしれないのでGitHubに上げておきます。

github.com

ライセンスはMITです。 ざっくり言うと、著作権表示とライセンス文章(へのリンク)を書いてくれれば自由に使っていいよ、 ただし何も保証しないし何が起きても責任はとらないよ、というライセンスです。

著作権表示とライセンス文章はプラグイン内のコメントに含めておいたので、特に気にしなくても勝手に入るはず。 READMEなんかに書く必要もありません (というか、私的に大事なのは免責のあたりなので最悪含まれてなくてもかまいません)。

開発について

とりあえずここまでで開発中止にしようかな、という感じです。

簡単にできそうなら最後まで作ってもよかったのですが、 やるべきことをいろいろと考えていたらまだひと月以上はかかりそうだったので・・・

収入になりそうなら続きを書くのも悪くないのですが、 いかんせん需要がわからないので闇雲に続けるわけにもいかず。

おわりに

正直もともとRPGツクールを画一的と馬鹿にしていたところがあったのですが、 結構プラグイン次第でどうにでもなりそうですね。 ただこのプラグイン製作のハードルが高いのが、実際のツクール製ゲームの幅が狭くなりがちな理由なのかもしれません。 あとは、デフォルト素材を多用する製作者の怠慢とかもあるか・・・

今後はプラグイン製作中にメモした内容とかを公開しようかなと思ってます。 この記事があまりに人気がないようだったらやめますが。



執筆時のRPGツクールMVのバージョン:1.3.1

*1:マップのdataプロパティに「マップデータの三次元配列」的なことが書かれているのを見たときは唖然とした(ちなみに実体は謎の整数の一次元配列)

*2:正直これを「スコープ」と呼ぶのが正しいのか怪しいところですが、他にいい名前が思いつかなかったので