見出し画像

RPGツクールプラグイン制作過程紹介 第3回

前回は、ツクール標準機能を使用してプラグインの仕様を検討しました。第3回となる今回はいよいよ実際のコードの解読や記述に入っていきます。

前回のおさらい

前回のケース2は概ね良好な結果でした。ですので、基本的にはその設定を踏襲します。ただしそのまま使用するにはいくつかの課題(以下に採番しなおしたものを再掲)があったわけですが、プラグインにてこれらを解決できれば理想的な機能にできるのではないかと思います。
本記事では、①に取り組んでいきます。

①マップ上に複数のたきぎが存在する場合、その数だけ条件分岐を用意する必要があるので面倒。
②特徴のMP消費率が設定されたアクターや装備、ステートが複数存在する場合、条件分岐が面倒。
③スキル不発時、一瞬だがMPの減少が描画されてしまう。
④メニューを開く必要がある。開いている間、イベントの視認不可。

イベントの座標判定

前回はイベントコマンドだけで実装しようとしたために「プレイヤーの目の前にイベントが存在するかどうか」をイベントごとに判定しなければなりませんでした。前回はイベントが一つしかなかったのであまり問題視していなかったのですが、よく考えてみたらイベントが複数存在する場合はもちろん、マップだって通常は複数あるわけですから、一つのコモンイベントに全ての条件分岐を詰め込もうと思ったら膨大な労力が必要になってしまいます。ですのでこの処理はスクリプト化が事実上必須と言えるでしょう。

ところで前回のケース1を覚えていらっしゃるでしょうか? 条件分岐が複雑すぎるということで早々に検討をやめた方式でしたが、こちらはケース2と違い「プレイヤーの目の前にイベントが存在するかどうか」を判定する必要がありませんでしたね。なぜでしょうか?
それはもちろんプレイヤーがイベントを「調べる(=イベントの前で決定ボタンを押す)」ことがイベントのトリガーであるので、このイベントが実行されているということは「プレイヤーがイベントの前にいて、イベントの方を向いている」ことが自明であるからですね。
ということは、ツクールにはマップイベントを起動する際に「あるイベントがプレイヤーの目に前に存在するかどうか」を判定する仕組み(関数)が用意されているのではないか、と推測できます。もしそうならその仕組みを流用すれば、上記のような個別の座標判定が不要になるのではないでしょうか。

それではその関数をどうやって調べたらいいのか?ということになるのですが、結論から言ってしまうとGame_Playerというクラスを見てみましょう。「イベントがプレイヤーの目の前にあるかどうか」の判定ということで、プレイヤーに関わるクラスに定義されている可能性が高そうだ、という手がかりはあるわけです。そしてツクールにおいてプレイヤーに関わる処理はGame_Playerというクラスに定義されています。このクラスは今後よく使うことになるので、覚えておいて損はありません。なおツクールにどのようなクラスが存在するのかを調べる方法については後述します。

というわけで、まずはVisual Studio Code(以下VSC)の全セクション検索(左上の虫メガネアイコン)でGame_Playerと入力してみます。

スクリーンショット 2021-03-20 13.06.33

するとGame_Playerという文字列が含まれる箇所が全てヒットします。とても数が多いのでどこを見ればいいのか分かりにくいですが、まずはここを選択してみます。

スクリーンショット 2021-03-20 13.09.51

すると以下のような表示になるはずです。

スクリーンショット 2021-03-20 13.12.22

これはrmmz_objects.jsというファイル内のGame_Playerクラスが定義されているセクションです(rmmz_objects.js用に新しいタブが開かれます)。
rmmz_objects.jsとは、主にゲームオブジェクトの定義が記述されているJavaScriptファイルです。「ゲームオブジェクト」とは何なのかについては本記事では触れませんが、ご興味がおありなら公式プラグイン講座のこちらをご覧ください。

検索でヒットしたのは定義の一番上の、英語でコメントしてある箇所です。コアスクリプトでは原則としてこのように、あるクラスの最初にそのクラスの説明コメントが記述されています。ですのでクラス名で検索したとき、先ほどのようなコメントアウト行(//で始まる行)を選択することでそのクラスの定義の先頭にジャンプできます。なおこれは単に私がよくやるテクニックというだけで、目的の箇所にたどり着けさえすればこの手順を踏む必要はありません。

関数の探し方

さてGame_Playerクラスにたどり着けましたので、早速「あるイベントがプレイヤーの目に前に存在するかどうか」の判定関数を探しましょう。私はざっくりコードを上から順に眺めていって、それっぽい関数名や処理を「カン」で探しています。今回は以下の関数を見つけました。

スクリーンショット 2021-03-20 13.38.23

startMapEventという名前の関数なのですが、いかにもマップイベントの起動に関わっていそうなオーラを放っています。これを見ていくことにします。
ところで私はこのように「カンで探す」というバッドプラクティスの権化のようなことをやってしまうのですが、ツクールのコアスクリプトに定義されているクラス・関数の一覧に関してはとんび@鳶嶋工房さんが素晴らしいリファレンスを作ってくださっています。

RPGツクールMZ 非公式JavaScriptリファレンス

こちらを参照すれば、ほとんどの関数に解説コメントが用意されているので目的の関数が見つけやすくなるかと思います。
例えば今回でいえば、Game_Playerのページを開けばこのクラスの全関数が確認できます。
このような素晴らしい資料を作ってくださったとんび@鳶嶋工房さんに心より感謝申し上げます。

Game_Playerの関数

本題に戻りましょう。
まず、startMapEventという関数が本当にマップイベント起動に関わっているのかを確認します。まずはこの関数を第1回で作成したFieldAction.jsにコピーします。

スクリーンショット 2021-03-20 14.09.13

以前用意した「枠」の中で、use strictよりも下に貼り付けます。
その後保存して、テストプレイを起動します。特に問題なく動作していれば正常です。
この時点ではまだstartMapEvent関数に何も変更を加えていないので、ツクール標準と何も動作が変わらないことこそが正常な状態です。
エラーが出ないことを確認したら、この関数に以下のような記述を加えます。

Game_Player.prototype.startMapEvent = function(x, y, triggers, normal) {
       console.log("hello");  // ←足した記述
       if (!$gameMap.isEventRunning()) {
           for (const event of $gameMap.eventsXy(x, y)) {
               if (
                   event.isTriggerIn(triggers) &&
                   event.isNormalPriority() === normal
               ) {
                   event.start();
               }
           }
       }
   };

これは、この関数が呼び出された際コンソールにhelloという文字列を出力させることを意図したコードです。console.log以外はそのままにしておきます。
ところで本シリーズではJavaScriptそのものについての解説は行いませんので、それについては公式のプラグイン講座をご覧ください。

ツクール上では、マップ上に適当なイベントを設置します。イベントのトリガーは決定ボタンとします。

できたらテストプレイを起動してみましょう。第1回で行った設定のおかげで、VSC上でもF5キーで起動できるはずです。もちろん、ツクール上から起動しても問題ありません。

スクリーンショット 2021-03-20 15.51.23

VSC下部にDEBUG CONSOLEが表示されています。もしこの枠自体が表示されていなければView > Debug Consoleで表示されると思います。あるいはツクールからのテストプレイ起動であれば、F8キーで同様のコンソールを呼び出せます。

さて、このコンソールを見てみるとhelloという文字列がきちんと出力されています。とりあえずconsole.logに関しては正常に機能しているといってよいでしょう。
ただし、一つ問題があります。それはhelloが何度も出力されていることです(helloの左の数字が回数)。実は、今回のテストプレイではプレイヤーが歩くだけでもhelloが出力されていたのです。
今回必要なのは、あくまでもプレイヤーの目の前にイベントが存在するかどうかの判定関数です。つまり決定ボタンをトリガーとしたイベントの起動判定に関わる関数を探しています。なので、こんな実験もしてみます。

スクリーンショット 2021-03-20 17.06.42

これは、目の前に全くイベントが存在しない状態で決定ボタンを押した状況です。helloが2回出力されていますね。
どうも今回のstartMapEventは、イベントの起動に関して全般的に呼び出されているようです。つまりトリガーが決定ボタンなのかイベントとの接触なのかを問わず、プレイヤーが何か入力するたびに起動すべきイベントが存在するかどうかをチェックし、存在すれば起動する、という処理を行う関数なのです。
ということでこれ自体は「あるイベントがプレイヤーの目に前に存在するかどうか」を判定している関数ではありませんが、その判定を行った後に呼び出されている関数ではないか、と推測できます。

ということはこの関数を呼び出している処理を探せば目的の関数を見つけられそうですね。なのでstartMapEventで検索してみましょう。

スクリーンショット 2021-03-20 17.17.48

さっきよりはグッと少なくなりましたね。これなら総当たりで見ていってもそこまで手間ではないでしょう。まずは一番上から…と言いたいところですが一番上は先ほどコピーした、この関数を定義している箇所です。ですので2番目を見てみましょう。

スクリーンショット 2021-03-20 17.25.21

なんとジャンプ先に、検索結果の残り全部が集中して記述されています。これはラクですね。上から見ていきましょう。

Game_Player.prototype.checkEventTriggerHere = function(triggers) {
   if (this.canStartLocalEvents()) {
       this.startMapEvent(this.x, this.y, triggers, false);
   }
};

このcheckEventTriggerHere関数は、canStartLocalEventsという関数の戻り値が真であれば先ほどのstartMapEventを呼び出す、という処理をしています。ではcanStartLocalEventsという関数は一体どのような処理なのでしょう。今度はこの関数名で検索してみます。

スクリーンショット 2021-03-20 17.47.14

4つの検索結果が表示されましたが、見るべき箇所は一つだけです。
今知りたいのはcanStartLocalEventsという関数がどんな処理を行なっているかということなので、この関数の定義元を見ればいいわけです。コアスクリプトにおいて、あるクラスの関数(メソッド)を定義する構文はほとんど以下のようになっています。

クラス名.prototype.関数名 = function() {
     // 処理
};

というわけで、見るべきは4番目の検索結果です。

スクリーンショット 2021-03-20 17.54.01

ジャンプしてみてびっくり、何とこの関数の定義文はさっきまで見ていた関数のすぐ近くにあったのですね!

さてこの関数の内容を見てみましょう。

Game_Player.prototype.canStartLocalEvents = function() {
   return !this.isInAirship();
};

thisとは一言で言うとプレイヤーのことです(厳密には「Game_Playerクラスのインスタンス」と表現すべきかもしれませんが、このクラスのインスタンスは$gamePlayerのみなので、実用上は「Game_Playerのメソッド内に登場するthisは常にプレイヤーキャラクターを参照する」という理解で差し支えないと思われます)。
つまりプレイヤーのisInAirship関数を呼び出し、その戻り値を真偽反転させて返す、という処理ですね。ではこの関数は一体どのような処理を行なっているのでしょう。
もちろんこれまで行なってきたようにこの関数も定義箇所を検索すればそれを確認できます。ですが私は普段、関数名を見てその処理内容を当て推量してしまうこともあります。
isInAirshipなんて名前、『飛行船に乗っているかどうか』の判定関数以外ありえへんやろがいっ!」ってな具合です。ツクールでは飛行船に乗っているとき原則としてイベントを起動できないわけですが、その処理がこれなんだろうなー、と推測で判断してしまってもこの時点では問題ないでしょう。いちいち一つひとつの関数の内容を調べていたら時間がいくらあっても足りません。問題が起きたらそのときに調べればいい、くらいの心構えでいいんじゃないでしょうか。ありがたいことにコアスクリプトには原則として可読性の高い関数名や変数名が使用されていますので、こうした場面でその恩恵にあずかることで時短を図るのもひとつのテクニックだと思います。

canStartLocalEventsについては理解できたので改めてcheckEventTriggerHereを見てみると、startMapEventの引数にthis.xthis.yが入っています(第3引数と第4引数については、ここでは気にしなくて問題ありません)。つまりプレイヤーのX・Y座標ですね。

this.startMapEvent(this.x, this.y, triggers, false);

まとめると、checkEventTriggerHereは「プレイヤーが飛行船に乗っていなければ、プレイヤーのXY座標を代入してstartMapEvent関数を呼び出す」という処理を行なっている関数である、ということになるでしょう。

ここでピンときた方もいらっしゃるでしょう。これはプレイヤーと「同一」座標に存在するイベントの起動に関わる関数なのです。イベントの設定によってはプレイヤーと重なるケースもあります。そうした場面でそのイベントを起動すべきかどうか、を判定しているわけですね。

プラグインの制約

そう考えると、今回のプラグインについても仕様を考える必要がありますね。例えばプライオリティが「通常キャラの下」に設定された落ち葉がプレイヤーの同一座標と目の前の座標の両方に落ちていたとします。そこで《ファイアⅠ》を使用した場合、どちらの落ち葉に火がつくべきなのでしょうか? このような問いにおいては、常にプレイヤーにとっての分かりやすさを優先すべきである、と私は考えます。この場合、プレイヤーの足元にある落ち葉は「見えにくい」ですよね? なのでもしこのプラグインにおいて目の前よりも足元の方が優先される仕様である場合、プレイヤーが足元にあるとは気づいていないイベントに(本当は目の前にあるイベントに使用するつもりで)スキルを使用してしまうというケースが発生しやすくなるのではないでしょうか。その結果MPの無駄消費につながってしまったら、そのゲームに対する印象が悪くなってしまうことが懸念されます。
これを防ぐために、以下のような制約を設けることにします。

・マップ上で使用するスキルの対象は常に「プレイヤーの直前のイベント」とする

文字にしてしまうと簡単なことですね。重要なのは、この制約はこのプラグインの利用者にも知らせる必要がありますし、またその利用者(=ゲーム制作者)が制作するゲームのプレイヤーにも知らせる必要がある、ということでしょう(もちろん、後者はゲーム制作者がゲーム内で知らせることになるでしょう)。

さて、上記のような制約を設けたということもあり、今回確認したcheckEventTriggerHere関数は探している関数とは直接的には関係なさそうです。ですのでお次はすぐ下にあるcheckEventTriggerThereを見てみましょう!
…と言いたいところですが、さすがに長くなり過ぎましたのでここで一旦終わりとします!

まとめ

というわけで今回は「プレイヤーの直前にイベントが存在するかどうかを判定する関数」を探してみました。お疲れ様でした。

今回は解読ばかりで実際のコードはほとんど書いていませんが、次回は冒頭の①を解決するべくどんどんコードを書いていきたいですね。というわけでまた次回お会いしましょう!

今回までの最終コード

//=============================================================================
// RPG Maker MZ - 
//=============================================================================

/*:
* @target MZ
* @plugindesc 
* @author 
*
* @help 
*
* 
*
* 
*/

/*:ja
* @target MZ
* @plugindesc 
* @author 
*
* @help 
*
* 
*
* 
*/

(() => {
   'use strict';
   
   Game_Player.prototype.startMapEvent = function(x, y, triggers, normal) {
       console.log("hello");  // ←足した記述
       if (!$gameMap.isEventRunning()) {
           for (const event of $gameMap.eventsXy(x, y)) {
               if (
                   event.isTriggerIn(triggers) &&
                   event.isNormalPriority() === normal
               ) {
                   event.start();
               }
           }
       }
   };
   
})();

この記事が気に入ったらサポートをしてみませんか?