この記事はRPGツクールMV Advent Calendar 2016の4日目の記事です。
みなみよつばさんによる前日の記事はこちらです。
はじめに
RPGツクールMVに限らず、JavaScriptに少しでも触れている方はES2015(ES6)について一度くらいは耳にしたことがあるかと思います。ご存じない方は、さきに以下の記事をご一読することをお勧めします。
- ES2015 (ES6)についてのまとめ
http://qiita.com/tuno-tky/items/74ca595a9232bcbcd727
極めて簡潔に説明すると、JavaやC#、Rubyなど他のオブジェクト指向言語が当然のように持っている便利な機能や記述方法がJavaScriptでも使用可能になる次期標準仕様です。すばらしいですね。特にRGSSに親しんできたツクールユーザにとって読みやすく、書きやすいコードになると思います。
ところが、2016年が終わろうとしている現在でもES2015にはブラウザの互換性の問題があり、恩恵をフルに受けるにはBabelなどを用いてトランスパイル(ES2015で書かれたJavaScriptをES5に変換すること)するのが一般的のようです。また、ツクールMVのコアスクリプト自体も当然、すべてES5で記述されています。
したがってツクールMVのプラグインを制作する際も基本的にはES5で記述することになるのですが、実はES2015に分類される記法がまったく使えないわけではありません。すべてはブラウザ(NW.js含む)の実装次第です。特にツクールMVの場合はIEやその他の古いブラウザは実質的に切り捨てられているので意外と使えたりします。むしろネックになっているのはローカル実行(NW.js)の方ですね。
そこで私自身の勉強やスキルアップも兼ねて、RPGツクールMVですぐに使えるES2015の記法を調査してみました。本稿ではその結果をまとめてご報告しています。よろしければお付き合いください。
下準備
まずは使用しているエディタやIDEの設定からES2015の使用を前提に静的解析を行えるように設定を変更しましょう。キャプチャの例はWebStormですが、他もおそらく似たような感じになると思います。あくまでも静的解析を快適に行うためのものなので、エディタに設定がなければ特に何もする必要はありません。
-
プロジェクト全体に適用するバージョンを設定します。
Settings > Languages & Frameworks > javaScript
-
ES2015用のライブラリを有効にします。
Settings > Languages & Frameworks > javaScript > Libraries
-
JSHintなどを使っている場合は一応、設定を変更しておきましょう。
Settings > Languages & Frameworks > javaScript > Code Quality Tools > JSHint
Setリンクをクリックしてバージョンを設定します。
プラグインの紹介
早速始めたいところですがその前に(宣伝を兼ねて)ES2015で作成したプラグインを紹介します。
「ファイルダウンロードプラグイン」というプラグインで、任意のURLを引数にプラグインコマンドを実行すると、URLのファイルをダウンロードしてプロジェクトフォルダに動的に追加、上書きできるようになるプラグインです。動的に画像ファイルを差し替えたり、プラグインを配布元から再ダウンロードして最新化することができます。
-
プラグインの紹介ブログ
http://triacontane.blogspot.jp/2016/11/rpgmv-url-fdmyplugin-filedownloader.html -
ソースコード
https://raw.githubusercontent.com/triacontane/RPGMakerMV/master/FileDownloader.js
すでに公開中のこのプラグインはES2015の記法が使用できるかどうかを検証しつつ作成したものです。各種機能ごとに使えたのか、使えなかったのかをこれから示していこうと思います。
各機能の確認
それでは早速、ES2015の主要な機能がツクールMVで使用できるか確認していきましょう。
確認環境
基本的にローカル環境で確認して問題なければ、RPGアツマールにアップロードしてブラウザで確認する……という方法を採用しました。
- NW.js(Windows)
- NW.js(Mac)
- PCブラウザ(Google Chrome 54.0.2840.99 m (64-bit))
- PCブラウザ(Microsoft Edge 38.14393.0.0)
- PCブラウザ(Firefox 47.0.1)
- モバイル(Android 4.4.4)
- モバイル(IOS 10.0.2)
使用できた機能
まずは、無事使用できた機能からです。
let,const
letは従来の変数宣言であるvarの代わりに使用するものです。varと異なりスコープが自身が定義されたブロックのみになるほか、varのよく分からない(笑)挙動が改善されています。また、constはletの特徴に加えて再代入を禁止します。
- letの詳細
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/let - constの詳細
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/const
とりあえずこんな感じでプラグインのひな形を作成して実行してみます。
(function() {
const pluginName = 'FileDownloader';
})();
ところが、早速怒られてしまいました。
Uncaught SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode
どうやらStrict Modeでないと動作しないようです。Strict Modeはコード全体に適用する方法と、特定の関数にのみ適用する方法があります。前者は実質不可能ですが、後者は一般的なツクールMVのプラグインの記述方法なので、迷わずStrict Modeにしましょう。
余談ですが、ES2015を使わない場合でも基本的にはStrict Modeを適用する方が望ましいと思います。
(function() {
'use strict';
const pluginName = 'FileDownloader';
})();
これならツクールMVでも正常に動作しました! letとconstが使えるだけでも大いに意味があると思います。
Class関連構文
ES2015の本丸とも言える機能です。メソッドごとにWindow_Base.prototype.textPadding
とか記述しなくてもよくなります。
かなり簡潔になりました。constructor
やstatic
の動作も問題ありません。
class GameChildProcess {
constructor() {
this._normalSwitchId = paramNormalEndSwitch;
this._abnormalSwitchId = paramAbnormalEndSwitch;
}
execute() {
this.executeChildProcess(this.getCommand.apply(this, arguments));
}
static getInstance() {
return this.isNwjs() ? (this.isWindows() ? this.getWindowsInstance() : this.getMacInstance()) :
this.getNoNwjsInstance();
}
}
ファイルダウンロードプラグインでは、node.jsのchild_processを使って、OSごとに異なるコマンドを実行しています。また、ウェブブラウザから実行した場合は何も行いません。今回はせっかくクラスベースで実装するので、ポリモーフィズムを使ってオブジェクト指向っぽい実装を試してみました。
継承やオーバーライドもバッチリ使用可能です。型や抽象クラスの概念がないので、オブジェクト指向の恩恵をフルに受けているとは言えないですが、それでもかなりスッキリした感はあります。(特にオーバーライド)
class GameFileDownload extends GameChildProcess {
execute(url, localDir) {
const path = require('path');
const projectBase = path.dirname(process.mainModule.filename);
const localPath = path.join(projectBase, localDir) + path.basename(url, true);
super.execute(url, localPath);
}
}
しかし、残念ながら既存のメソッドの上書きには使えないようです。
const _Game_Interpreter_pluginCommand = Game_Interpreter.prototype.pluginCommand;
Game_Interpreter.prototype.pluginCommand = function(command, args) {
_Game_Interpreter_pluginCommand.apply(this, arguments);
this.pluginCommandFileDownloader.apply(this, arguments);
};
そして、既存のクラスにメソッドを追加する場合もうまくできませんでした。もともとのGame_Interpreter
とプラグインで定義したGame_Interpreter
が別物と認識されてしまうようです。うまいやり方があればご教示ください。
Game_Interpreter.prototype.downloadFile = function(args) {
args[1] = addSlashOfEnd(args[1]);
this._downloader.execute(args[0], args[1]);
};
Class関連構文が使えるのは、プラグインで新規作成したクラスに限られるようです。プラグインで新しくクラスを定義するケースは少ないかもしれませんが、戦闘システムをプラグインで一からガッツリ設計する場合などは相当楽になると思います。
連想配列
ES5ではオブジェクトで連想配列モドキを実現していましたが、ES2015ではMapを使ってIterable(繰り返し処理可能)かつキーに文字列以外も指定できるちゃんとした連想配列を定義できます。
プラグインコマンドの呼び出しに連想配列を使っています。実はオブジェクトでも特に問題ないのですが、試しに使ってみたということで。
const pluginCommandMap = new Map ([
['FD_FILE' ,'downloadFile'],
['FD_ファイル' , 'downloadFile'],
['FD_START_SITE', 'startupWebSite'],
['FD_サイト起動', 'startupWebSite']
]);
Game_Interpreter.prototype.pluginCommandFileDownloader = function(command, args) {
const pluginCommand = pluginCommandMap.get(command.toUpperCase());
if (pluginCommand) {
args = convertAllArguments(args);
this.makeDownloader(args);
this[pluginCommand](args);
this._downloader = null;
}
};
その他、Set、WeakMap、WeakSetも使用可能です。Setは重複を許可しない配列で、WeakMapは弱い参照による連想配列です。WeakMapはオブジェクトのみをキーに指定可能かつキーの参照が他にない場合はGCによって破棄されます。
const pluginCommandSet = new Set();
pluginCommandSet.add(1);
console.log(pluginCommandSet.has(1)); // true
const pluginCommandWeakMap = new WeakMap();
const obj1 = {};
pluginCommandWeakMap.set(obj1, 1);
console.log(pluginCommandWeakMap.get(obj1)); // 1
const pluginCommandWeakSet = new WeakSet();
const obj2 = {};
pluginCommandWeakSet.add(obj2);
console.log(pluginCommandWeakSet.has(obj2)); // true
for...of文
for...of文は、Iterableな値について繰り返し処理を行う構文です。連想配列と組み合わせると便利です。
const getParamOther = function(paramNames) {
for (let name of paramNames) {
const paramName = PluginManager.parameters(pluginName)[name];
if (paramName) return paramName;
}
return null;
};
テンプレートリテラル
テンプレートリテラルは、文字列の中に変数を埋め込んで表示できる機能です。RPGツクールMVには類似機能であるString.format
が定義されているので必須ではありませんが、こちらの方がやはりスッキリ書けます。
class GameFileDownloadMac extends GameFileDownload {
getCommand(url, localPath) {
return `curl ${url} -o ${localPath}`;
}
}
Promise
Promiseは簡単に説明すると複雑な非同期処理をスマートに記述できる機能です。特に非同期処理の結果によってさらに非同期処理を実施するような場合に効果があります。RPGツクールMVのプラグインで使う機会はさすがにないかもしれませんが、一応動作します。
もともと単純な処理に対して使ったのでかえって分かりにくくなった気もしますが…… 使用例ということでご容赦ください。
executeChildProcess(command) {
const childProcess = require('child_process');
const promise = new Promise(function(resolve, reject) {
childProcess.exec(command, function(error, stdout, stderr) {
return error ? reject(error) : resolve();
}.bind(this));
}.bind(this));
promise.then(this.onNormalEnd.bind(this), this.onAbnormalEnd.bind(this));
}
ジェネレータ関数
ジェネレータ関数はひと言で説明するのが難しいですが、ひとつの関数を分割して実行するための仕組みで、関数の実行を中断したり、非常に柔軟な繰り返し処理を実現したりできます。RGSS3のFiberと似ています。
以下の例はもはやFileDownloader.jsとはなんの関係もありませんが、ブザーを1秒間隔で3回演奏する処理です。(うまい例が思い浮かばなかったので……)
使い方次第で非常に強力な機能になり得るのは確かです。
class GamePlaySound extends GameChildProcess {
execute() {
this._generator = this.getGeneratorPlaySound();
this.executeGenerator(null);
}
executeGenerator(count) {
const result = this._generator.next(count);
if (!result.done) {
setTimeout(this.executeGenerator.bind(this, result.value + 1), 1000);
}
console.log(result.value);
}
*getGeneratorPlaySound() {
SoundManager.playBuzzer();
let count = yield 1;
SoundManager.playBuzzer();
count = yield count;
SoundManager.playBuzzer();
return count;
}
}
使用できなかった機能
続いて残念ながら使用できなかった機能です。使用できなかった機能について長々語っても仕方がないので箇条書きで挙げていきます。いずれもローカル環境でシンタックスエラーになりました。
- 関数のデフォルト引数
- 関数の可変長引数
- Arrow関数
- 配列展開
- 分割代入
- import/export
ブラウザでの動作確認プログラム
ここまで機能ごとに使用可否を説明してきましたが、ローカル環境と私の手持ちのブラウザだけの動作確認ではやはり不安が残るでしょう。
そこで、これまでに紹介したES2015の記法で作成したプラグイン「FileDownloader.js」を組み込んだプロジェクトをRPGアツマールに投稿しました(大迷惑)。一般にプラグインとして公開しているものに加えてES2015のテストコードが組み込まれています。
- ES2015確認プログラム
http://game.nicovideo.jp/atsumaru/games/gm318
各種ブラウザから上のリンクを起動してニューゲームで開始すると、メッセージの後にブザーが1秒間隔で3回演奏されるはずです。ぜひお試しください。
まとめ
ES2015の記述は一部難解なものもありますが、基本的にはJavaScriptをこれまでよりも明確に分かりやすくしてくれるものです。今後のことも考えて、クラス設計が必要になるような大規模プラグインはもちろん、ちょっとした小物であってもletやconstなど積極的に取り入れていきたいと思います。
ここまでお読みくださりありがとうございました。明日以降もRPGツクールMV Advent Calendar 2016にご期待ください。5日目はツクマテ管理人の弓猫さんが担当します。