RPGツクールMV 続・良いプラグインとは
はじめに
良いプラグインの定義については 前回の記事 をご参照ください。
今回もその定義に従って、可読性や保守性を意識したプラグインの書き方を紹介していきます。
前回よりも難しい領域に踏み込むため、この記事の説明だけでは理解しにくい内容もあるかもしれません。
今回はできるだけ具体的なコード例を書きながら書き進めていくつもりです。
良いプラグインの書き方
セーブデータの互換性を破壊しない
RPGツクールMVの Game_****
系のクラスは、内容をゲームのセーブデータに保存しています。1
このクラス群にメンバを追加する場合は、十分に注意する必要があります。
以下の例を見てみましょう。
const _Game_Character_initMembers = Game_Character.prototype.initMembers; |
こう書くと、 Game_Character
クラスに配列要素が一つ追加されます。
その分だけセーブデータが大きくなるのはもちろんなので、そこも注意しなければなりませんが、最も恐ろしいのは再現条件のわかりにくいバグの温床になることです。
Game_****
系のクラスはニューゲーム開始時に $game****
なる変数にインスタンスを生成します。
そして、セーブデータをロードした際には、ロードしたデータを $game****
変数に展開します。
上記のコードを含むプラグインを導入する前にセーブしたデータを、プラグイン導入後にロードするとどうなるでしょうか。
ニューゲーム開始時には空配列で初期化される _characterHoge
が、セーブデータロード時には未定義になってしまいます。
その配列に対して何らかのメソッドを呼ぶ、例えば .map
で何らかの処理を配列の要素全てに対して行うようなコードが書かれていた場合に、 Cannot read property 'map' of undefined
などというエラーが出てしまうのです。
こういったケースが存在することを知っていれば再現条件に想像は付きますが、プラグインの利用者はたいていこんなこと知りません。
再現の条件がわからないまま断片的な情報を作者に送りつけなければならなかったり、あるいはその時点でプラグインを捨てる選択を迫られるでしょう。
リリースしてから時間の経ったゲームの改修に使われてしまった場合は最悪です。
プレイヤーがバージョンアップしたゲームを取り込んだ後、これまでのセーブデータの続きからプレイできなくなってしまいます。
セーブデータに影響があるため、 Game_****
系のクラスにメンバを追加する場合は、ニューゲーム開始後とセーブデータロード後のどちらでもしっかりと値が初期化されるよう注意しましょう。
もちろん、セーブデータに含める必要のないメンバを追加すべきではありません。
カプセル化を意識している
2021/01/10追記: より平易な記事を用意したので、よくわからないという方はそちらも参照してみてください。
【RPGツクールMV/MZ】なぜアンダースコアで始まる変数にクラスの外からアクセスしてはならないのかカプセル化。RPGツクールの世界では、某クソコード動画さんのおかげで有名な言葉かもしれません。
噛み砕いて説明すると、クラスが内部状態や詳細な実装ではなく外部から必要な操作のみを公開するようにし、それぞれのクラスの独立性を担保して保守しやすくすることです。
言葉だけではピンとこないと思うので、RPGツクールMVのコアスクリプトのコードを一部見てみましょう。
Game_CharacterBase.prototype.moveStraight = function (d) { |
これは、 Game_CharacterBase
クラス(ゲーム中でマップに表示するためのキャラクターに共通の処理が書かれたクラス)に定義された moveStraight
メソッドです。
名前の通り、キャラクターを1歩移動させるメソッドですね。
1歩移動させるだけなら、向きに応じてキャラクターの _x
や _y
を操作すれば良いだけじゃないかと思うかもしれません。
実際はこのコードの通り、ある向きに対して移動が成功したかどうかの判定をしたり、マップがループ構造だった場合に端っこから逆側に出てくる処理があったりして、かなり複雑な処理になっています。
しかも、このメソッドを直接外部から呼ぶわけでもなく、更に外部向けに抽象化された moveRandom
や moveTowardCharacter
等といった同じクラス内2のメソッドから呼び出されるようになっています。Game_CharacterBase
を継承するクラスの一つである Game_Player
について、更に呼び出しを遡ると update
メソッドまで到達し、ようやく外部クラスである Scene_Map
クラスの updateMain
から呼び出されているとわかります。
マップ上のプレイヤーを実際に移動させる、という場合に、外からは update
だけ呼べば良い構造になっているわけです。
RPGツクールMVでは、 _ (アンダーバー)で始まる名前のシンボルは、そのクラスの内部に隠蔽しておきたいメンバとして扱う、という命名規則を採用しています。3 4
外部のクラスから _x
等のような、アンダーバーで始まる名前のメンバにアクセスしないようにコードを書くべき、ということです。_x
や _y
は Game_CharacterBase
の内部状態であり、それを直接操作するために必要な手続きは Game_CharacterBase
自身が持つべきです。
こうしてクラスごとの責務を明確にしたコードを書くと何が嬉しいかというと、プログラムが巨大になった場合でもどこにどんな処理があるかあたりをつけやすく、役割が明確なのでどこをどう書き換えれば望みの挙動を実現できるかすぐにわかったり、不具合があった場合にも原因となっている箇所が特定しやすくなります。
プラグインを書く場合にこれを完璧に守るのは難しい場合もありますが、プラグインの規模が大きくなればなるほど、これを意識できているかどうかが効いてきます。
前回の記事では散々苦言を呈したYEPやMOGですが、規模の大きいプラグインを書いているだけあってカプセル化という観点で見ればそれなりにまともなコードが多い印象を受けます。
高頻度で呼ばれる処理を避ける
クラスにもよりますが、 update
メソッドは、ゲームの処理の中で毎フレーム呼ばれていたりして、非常に呼び出し頻度が高いです。
毎フレーム呼び出されるメソッドの中に、重たい処理が混ざっていたらどうでしょうか。
これはゲーム全体のパフォーマンスに関わる問題で、何も考えずに重たい処理(画像や音声といった、各種メディアファイルのロード等)をいくつも突っ込んでしまうと、ゲームが処理落ちして遊んでいられなくなることにも繋がります。
プラグインによってどこに処理を追加すべきか、カプセル化の項目でも説明したプログラムの責務と合わせて、じっくり検討する必要があります。
おわりに
この記事では、セーブデータに関する話、カプセル化による保守性を意識したコードの話、そしてゲームのパフォーマンスに関わるような高頻度で呼ばれる処理の話を紹介しました。
ここで書いた話はゲームにとって必須ではないかもしれません。
プラグインとはあくまで従の存在であり、主たる存在はゲームそのものです。
しかしながら、従たるプラグインが良いものであることで、ゲームを作るための障害を減らしたり、効率よくゲームを作ることができるようになります。
前回以上に難しい内容になってしまいましたが、プラグインを読んだり書いたりする際に頭の片隅に今回の記事の内容を思い浮かべていただければ幸いです。
おまけ1:難読化
前回から通して、コードの読みやすさを重視してきましたが、逆にあえてコードを読みにくくすることもあります。
商業で書いたコード等の盗用を防止する為に、プログラム的に人間にほとんど読めないようなコードに変換するのです。
RPGツクールMVのプラグインにおいても、同様のことをしつつ有料でプラグインを販売している方がいらっしゃるようです。
ただ、有料であろうとなかろうと、難読化するということは元のコードを作者以外に読ませたくないということになります。
つまり、作者以外がそのプラグインについてサポートできなくなるということです。
自分一人で迅速にサポートをし続ける気概があるのでない限り、公開するプラグインを難読化するメリットは全くないと言って良いでしょう。
コードを圧縮することで難読化するため、ゲームにおいてプラグインが占める容量を減らすことも可能ではあります。
ですが、巨大なプラグインをいくつも入れていない限り誤差レベルですし、コメントに多く含まれるライセンス表記をまるごと消してしまうため、別途必要な表記をしておかなければライセンス違反になってしまいます。
より容量の大きい画像や音声ファイルについての軽量化を考えるほうがコストパフォーマンスは良いでしょう。
おまけ2: メモリ
プログラムの中でオブジェクトを生成すると、その分だけメモリを使用します。
使用できるメモリの量は有限であり、メモリがいっぱいになるとゲームが直ちに落ちてしまう……というわけでは、実はありません。
JavaScript処理系にはGarbage Collector(ガベージコレクタ、ゴミ集め)の機能が備わっており、不要になったメモリ領域を自動的に再利用してくれます。new Bitmap()
などとして確保したオブジェクトのためのメモリ領域が使わなくなった後5占有されっぱなしにならないのは、これのおかげです。
ゲームにおいてこのGCは曲者です。
メモリの再利用自体は必要なことですが、それをするコストもタダではないのです。
必要なものをしっかりとマーキングして、不要なものだけ再利用しなければなりません。
GCのせいでゲームがカクつく可能性がある、と言うと、アクションゲームなどを作っている方には深刻さが伝わるかもしれません。
ただし、ChromeやChromiumに搭載されたJavaScriptエンジンV8のGCは非常に優秀で、マイナーGC6は10ミリ秒のオーダーで完了します。7
もちろん、GCが頻発してしまうのは問題ですが、RPGツクールMVのプラグインでそんなに大きなオブジェクトを頻繁に作っては捨て、という処理を書くことはないでしょう。
小さなゴミが大量に出るのは経験上よく知られており、GCもそれに適した形にチューニングされています。
正確なことはゲームごとにメモリ使用量推移のデータを見てみないとわかりませんが、よほど雑にアロケーション8してしまったりオブジェクトを保持し続けない限り、GCの停止時間が問題になることはないでしょう。
GCのタイミングを制御したいほどリアルタイム性を重視するアクションゲームの場合は大掛かりな最適化を考えても良いかもしれませんが、そうでない場合は特別にGCを意識してコードの可読性を落としたりすべきではありません。9
- Game_Message,Game_Troop,Game_Enemy,Game_Tempは例外。
- あるいはGame_CharacterBaseを継承したクラスの内部。
- 最も広く受け入れられているスタイルの一つ Airbnb JavaScript Style Guide も同様の命名規則を採用している。
- ちなみに Google JavaScript Style Guide ではアンダーバーを末尾につける命名規則を採用している。プロダクトごとに命名規則は異なる。
- 正確にはもう二度と使われないとGCに判断された後。
- V8のGCは世代別GCで、ざっくり言うと簡単なお掃除と大掃除の2種類があり、マイナーGCは簡単なお掃除のほう。ちなみに大掃除のほうはメジャーGCと呼ぶ。
- オブジェクトプールを使った静的メモリ JavaScript参照。
- オブジェクトのためにメモリを確保すること。
- オブジェクトプールが有効なのはプログラム全体で必要なオブジェクト数上限がわかっている場合であって、単独のプラグインだけで意識してオブジェクトを抱え込んでも逆効果にすらなる場合がある。