ツクール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(即時関数)が必須でしたが、
const
やlet
によってブロックスコープが利用可能になったためいらなくなりました。
もちろんミスを防ぐために書いてもいいとは思います。
残念ながらツクールではモジュールが利用できないため、Strictモード指定は未だに必須です。 IIFEで囲む場合には関数単位で指定してもいいと思います。
(() => { "use strict"; // プラグインの処理 })();
アンチパターン
プラグインの全体構成というか、テンプレ的記述としてよく見られるアンチパターンについてです。
プラグインの有無の判定
それ自体がアンチパターンというわけではないんですが、 少しコメントしておきたいのが次のコードです。
var Imported = Imported || {}; Imported.MyAwesomePlugin = true;
いくつかあるんですが、簡単な内容から順にコメントしていきます。
まず、このコードはプラグインの先頭によく書かれますが、
"use strict";
より前に書くとStrictモードにならないので注意してください。
次に、このコードはvar
を単純にconst
やlet
に置き換えてもうまく動きません。
このコードでは変数を宣言するとともに、その変数を初期化処理に使用しています。
var
ではこのような書き方も許されましたが、const
やlet
では未初期化変数の参照としてエラーになります。
加えて、var
では同スコープ内での同名変数の宣言が許可されましたが、const
やlet
では許可されません。
そのため、複数のプラグインでグローバルスコープに同様の宣言を記述すると、重複変数の宣言としてエラーになります。
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);
かなり複雑なことをしているので一見すごいコードに見えるかもしれませんが、 実はやってはいけないことのオンパレードです (そもそもプログラミングにおいて複雑というのはそれ自体が欠点ですが)。
まず、このコードは@type
がstring
、file
、select
、combo
のパラメータに対して正しく動作しません。
何故かというと、これらのパラメータはどのような文字列をも正しい入力として受け入れる可能性があるためです。
もしJSONやJavaScriptのコードとして解釈できる文字列を渡されてしまうと、
JSON.parse
やeval
で例外が発生しないため、解釈して評価された値になってしまいます。
例えば、[42].png
という名前のファイルを指定すると、
パラメータとしては"[42]"
という文字列が渡されるため、
JSON.parse
で42
という要素をひとつだけ持つ配列と解釈されてしまいます。
eval
だけに引っかかるケースはよりひどくて、例えば、
パラメータとして"this"
という文字列が渡された場合には、
循環構造を持つオブジェクトをJSONに変換しようとしているというエラーが表示されます。
なんのこっちゃという話ですよね。
この挙動だけでもすでに使えそうにないのですが、 プログラミングの作法的にもこのコードはいろいろとマズイです。
まず、eval
については前述の通りです。
実際、起こりうる可能性をしっかりと把握できていなかったために正しく動作していません。
次に、try-catch
についてですが、正常な動作としてエラーを捕捉してはいけません。
catch
節は期待していない結果が起こった場合の処理を書く場所です。
特定の失敗のみを想定してcatch
節を書くと、
思わぬ原因(スタックオーバーフローなど)でエラーが投げられた際に想定外の挙動をしてしまいます。
基本的にcatch
節はエラーのログをとるためか、
失敗した操作をなかったことにして正常系に戻すために使用するものです。
最後に、JSON.parse
とJSON.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になり、変数宣言のためのvar
がconst
とlet
によって置き換えられました。
これによりvar
で変数宣言をすべきケースというのは一切なくなりました。
変数宣言は状況によりconst
とlet
を使い分けることになります。
const
とlet
の違いは再代入ができるか否かです。
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
と-0
がfalse
を返したり、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