見出し画像

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

前回前々回の課題を解決しました。これにより、マップ上で使用したスキルへのイベントの反応は一通り設定できたことになります。

これでプラグインを完成としてもいいのですが、まだ課題は残っています。第3回にて掲載した、最初に整理した課題を再掲してみます。

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

①は前回までの作業により、無事解決できました。今では個々のイベント内への条件分岐は不要であり、また複数のイベントに処理を容易に流用することが可能になりました。
②③④の課題は未解決であり、今でもこれらの問題が残っています。

実はこれらの課題をひとまとめに解決できる素晴らしい方法があります。それは「マップ上にスキル選択ウィンドウを直接表示して使用スキルを選択させる」というものです。
なぜこれにより上記残課題が解決できるのかということについては、長くなりますので今後実際の処理を組んでいく過程で改めてご説明することにします。

というわけで、今回からマップシーンに追加ウィンドウを表示させる処理を組んでいきたいと思います!

シーンとウィンドウ

マップ画面にウィンドウを表示させる処理を組むには、まずそもそもツクールではどのようにウィンドウを表示させているのかを知る必要があります。ウィンドウは「シーン」により作成・管理されます。シーンとは、文字通り場面のことです。
ツクールにはマップや戦闘、メニューやショップなど様々な場面がありますね。あれらは全て「シーン」です。今回ウィンドウを追加したいマップのシーンはScene_Mapです。ですのでScene_Mapにおいて表示されるウィンドウがどのように作成されているのかを模倣すれば、新しいウィンドウを追加することができそうな気がします。

ところで「マップ画面に表示されるウィンドウ」にはどのようなものがあるでしょうか? …そもそもツクール標準のマップ画面って、ウィンドウなんて表示されてましたっけ?

スクリーンショット 2021-06-21 22.21.56

何もウィンドウは表示されていないようですが…ちょっとおしゃべり君に話しかけてみましょうか。

スクリーンショット 2021-06-21 22.22.04

うわっ、それくらいで結構です!
そういえばマップにはメッセージウィンドウや、その話し手の名前ウィンドウが表示されていましたね! それ以外にも選択肢ウィンドウや所持金ウィンドウ、変わり種では大事なものなどを選択させるためのアイテムウィンドウなどもありました。
ここで重要なのは、これらのウィンドウは常に表示されているわけではなく、必要なときだけ開かれるようになっている、ということです。

今回作成する予定のスキル選択ウィンドウも必要な時にだけ開かれるようにしたいので、これらのウィンドウの処理が大いに参考になります。
ですのでScene_Mapの中でウィンドウがどのように作成されているのかを確認するために、早速Visual Studio Code(以下VSC)の全セクション検索にてScene_Mapで検索してみましょう。

スクリーンショット 2021-06-21 19.32.36

以前にもご紹介した通り、コアスクリプトではクラス定義の最初に必ずコメントがつけられています。ですのでこれを選択すればクラス定義箇所の最初にジャンプできます。

スクリーンショット 2021-06-21 21.15.40

rmmz_scenes.jsScene_Mapの定義箇所が開かれます。initializeはこのクラスの初期化メソッド(コンストラクタ)であり、インスタンスが作成された際に呼び出される関数です。

今探しているのはマップ画面でウィンドウを作成している箇所なので、このファイル内でwindowという単語で検索(command⌘/Ctrl+F)してみます。

スクリーンショット 2021-06-24 22.38.36

最初に出てきたのは_mapNameWindowというウィンドウで、このウィンドウのopen関数を開いているようです。これは場所移動した際にマップ名を表示するためのものなのですが、実はあれもウィンドウなのです。
とはいえこれ自体はウィンドウを作成する処理ではないようなので、それらしき単語が含まれていそうな他の箇所を探して見ることにします。

スクリーンショット 2021-06-24 22.45.35

検索によって893行目あたりまで来ると、createという単語が目に入りました。createAllWindows、つまりすべてのウィンドウの作成というまさにそのものズバリの名前がつけられた関数を呼び出しているではありませんか!
これは今回のカスタマイズにとって非常に重要そうなので、その下の903行目からの定義箇所を詳しくみてみましょう。

Scene_Map.prototype.createAllWindows = function() {
   this.createMapNameWindow();
   Scene_Message.prototype.createAllWindows.call(this);
};

この関数の最初では先ほども見たマップ名ウィンドウを作成するためのcreateMapNameWindow関数を呼び出しています。その次はScene_Message.prototype.createAllWindows.call(this)という処理をしているのですが、これはなんなのでしょうか。

callとは関数を呼び出すためのメソッドです。単に関数を呼び出すだけであればthis.createAllWindows()とでもすれば良さそうなものですが、なぜこのような複雑な書き方をしているのでしょうか。それは、Scene_MessageというScene_Mapのスーパークラスのメソッドを呼び出しているからです。

オブジェクト指向プログラミング言語であるJavaScriptでは「クラスの継承」という非常に便利な機能を使うことができます(厳密にはJavaScriptはプロトタイプベースではありますが)。ではそもそも、「オブジェクト指向プログラミング」とはなんなのでしょうか?

オブジェクト指向プログラミング

オブジェクト指向プログラミング(OOP)とはプログラミングにおける手法や考え方の一つです。OOPではその名の通り「オブジェクト」を主体としたプログラムの構築が基本になります。ちなみにプログラミング手法には他にも「手続き型プログラミング」や「関数型プログラミング」といった種類があり、OOPと対比されます。
OOPはその名の通り「オブジェクト」が主体であるとされますが、実際のプログラムでは「インスタンス」という名前で呼ばれることが多いです。インスタンスとは「クラス」の具体的な実体であることを強調するためのオブジェクトの別名とでもお考えください。OOPにおける重要な概念である「クラス」と「インスタンス」を理解するには、たい焼きの「型」と個々の「たい焼き」をイメージするとわかりやすいかと思います。

たい焼きの型は一つあれば、同じ形をしたたい焼きをいくつでも作ることができますよね。同じようなものを量産することこそが型の役割です。その型によって作られる一つひとつのたい焼きはどれも外見こそほぼ同じですが、中身が異なっていることもあるはずです。粒あんもあればこしあんもあり、カスタードやチョコクリームが入っている個体もありますよね。
ここでの「型」は「クラス」に、個々の「たい焼き」は「インスタンス」に相当します。

また、たい焼きという概念は一歩引いた目で見ると「おやつ」という概念の一種である、という捉え方もできます。というのも、たい焼きにはおやつの持つ以下の性質が全て備わっているからです。

・子供にも親しみやすい味付けがされている
・手頃な大きさである
・3時に食べる
・主食には向かない

この考えを進めるとおやつという「クラス」には、さらにもう一歩上位の概念として「食べ物」というクラスがあると言えるでしょう。おやつは食べ物の持つ以下の性質を全て受け継いでいるからです。

・経口摂取できる
・栄養がある
・お腹にたまる
・味覚を刺激する

このようにあるクラスは別のクラスの性質を受け継ぐことができます。これを「継承」といい、親となるクラスのことを「スーパークラス」といいます。なお子クラス(この例ではたい焼き)のことは「サブクラス」と呼ぶこともあります。

子クラスは、親クラスの持つ性質を全て受け継ぎます。その上で、その子特有の性質も併せ持ちます。例えば上記の例でいうと「たい焼き」は親クラスであるおやつの、そのまた親クラスである食べ物の性質を全て受け継いでいるという話でした。それに加えて、たい焼きには以下のような性質もあります。

・原材料:小麦粉、具の材料(あんこなら小豆)
・小麦粉で作られた生地を鯛を模した型に流し込んで焼く
・味は中の具による

おやつというクラスのサブクラスはたい焼きだけではありませんよね。例えば「ポテトチップス」には以下の性質が備わっているでしょう。

・原材料:じゃがいも
・薄切りにしたじゃがいもを揚げて作る
・主にしょっぱい味付けがしてある

たい焼きもポテトチップスも親クラスであるおやつや食べ物の性質をすべて継承しつつも、それぞれが持つ独自の性質によって区別されているわけです。

このような概念と具体事象との関係(クラスとインスタンス)、あるいは概念の親子関係(スーパークラスとサブクラス)というのは現実世界ではよくみられる関係です。例えば私たち個人だってホモサピエンスというクラスのインスタンスと言えましょう! また、ホモサピエンスというクラスは霊長目の、霊長目は哺乳類の、哺乳類は脊椎動物の、脊椎動物は脊索動物の…ずっとずっと遡れば生物というクラスのサブクラスであると言えるでしょう。
このような考え方は人にとって理解しやすく、現実の物事のほとんどをこれに当てはめることができます。

オブジェクト指向プログラミングとはざっくり言うと現実の「モノ(オブジェクト)」を模すことでより自然で分かりやすいプログラミングをしよう、それによってプログラムの生産性や保守性を高めよう、という手法です。

話をツクールに戻すと、コアスクリプトにはたくさんのクラスが存在します。例えばアクターを表すGame_Actorというクラスがあります。そしてこのクラスにはID 1番、2番、3番…を持つインスタンスが存在しますが、それらは基本的には同様に扱うことができます。どのアクターもパーティに加えたり、武器や防具を装備させたり、スキルを使ったりと、同じ操作ができますよね。ただし装備可能なアイテムの種類や使用可能なスキルの種類は異なっているわけですが、それは個々のインスタンスの持つパラメータ(プロパティ)が異なっているからです。
例えばID 1番のインスタンスのnameプロパティは「リード」であるのに対し、2番のアクターは「プリシア」、3番は「ゲイル」…といった具合です。
また、Game_ActorGame_Battlerのサブクラスです。ツクールで戦闘を行うことができるのはアクターだけではありませんよね。そうです、敵キャラ(Game_Enemy)だって戦闘に参加する資格を有しているわけです。Game_Battlerには戦闘を行うキャラクターに共通する機能が定義されています。例えばHPや攻撃力といったパラメータもそうですし、ステートにかかった際の処理などもそうです。そうした基本機能をスーパークラスであるGame_Battlerに定義した上で、それを継承するGame_Actorにはレベルアップや装備変更などアクター特有の機能が定義されています。もちろん、同じくGame_Battlerを継承するGame_Enemyにもドロップアイテムや戦闘行動パターンといった敵キャラ特有の機能が定義されています。

こうした概念はゲームでは大変よく見られるものですが、OOPであれば簡単に表現できます。もちろん、OOPはゲームに限らず非常に幅広い分野で使われています。

OOPは非常に奥が深く、クラスとインスタンスの関係だけが全てではありません。本記事ではここまでとしますが、ご興味がおありであればぜひ調べてみてください。

Scene_Mapのスーパークラス

話を戻しましょう。Scene_Mapというクラスは、Scene_Messageというクラスを継承しています。これは何かというと、メッセージの表示に関する処理をまとめたクラスです。ツクールにおいてメッセージを表示できるシーンはマップだけではありませんよね。そうです、戦闘中(Scene_Battle)にもメッセージを表示できます。これらのクラスにそれぞれメッセージ表示処理を組んでいたのでは二度手間になってしまいますから、その機能をScene_Messageにまとめた上でScene_MapにもScene_Battleにも継承させることで、シンプルに実装できているのです。

スクリーンショット 2021-06-27 12.35.51

スクリーンショット 2021-06-27 12.36.02

さらにいうとScene_MessageScene_Baseというクラスを継承しています。Scene_Baseはシーンの基本機能をまとめたクラスであり、全てのシーンクラスはこれを継承しています。

スクリーンショット 2021-06-27 12.39.25

つまりScene_Mapにはシーンとしての基本機能やメッセージの表示機能といった他のシーンにも共通する処理を記述せずに、マップに関する処理のみが記述されているのです。これによって非常にコードがスリムになり、可読性やメンテナンス性も高まっています。

というわけでようやくScene_MapcreateAllWindows関数に戻ります。この関数内にある以下のコードに注目していました。

Scene_Message.prototype.createAllWindows.call(this);

これはスーパークラスであるScene_MessagecreateAllWindows関数を呼び出して実行するための処理です。この書き方は、スーパークラスに存在するある関数にサブクラス特有の処理を追加したいときに大変よく用いられます。ひとまず、スーパークラスであるScene_MessagecreateAllWindows関数の定義を見てみましょう。

スクリーンショット 2021-06-27 13.22.55

何やらcreate○○Windowという関数がたくさん並んでいます。これらはすべてその名の通りウインドウを作成するための関数です。そのウィンドウとはいずれも、メッセージに関連したウィンドウなのです。
例えば一番上のcreateMessageWindowというのはわかりやすいですね。これはもちろん、メッセージウィンドウを作成するための関数です。その下のScrollTextはスクロールするタイプのメッセージウィンドウ(昔のCSツクールでいうところの「テロップ」ですね)、Goldは所持金、NameBoxはメッセージの名前枠…といった具合に、いずれもメッセージイベントにて表示できるウィンドウの作成関数です。これはScene_Messageクラスなので、メッセージ関連ウィンドウを作成する処理がまとめられているわけですね。

ではScene_MapcreateAllWindows関数はどうなっていたでしょうか。ここに再掲します。

Scene_Map.prototype.createAllWindows = function() {
   this.createMapNameWindow();
   Scene_Message.prototype.createAllWindows.call(this);
};

createMapNameWindowをまず実行し、その後スーパークラスの同名関数を呼び出す、という内容でしたね。マップ名ウィンドウはマップにのみ必要なのであって、戦闘中は表示する必要のないウィンドウです。ですのでScene_Map特有の処理としてこのように記述されているのです。もちろんScene_BattlecreateAllWindows関数にはその反対に、戦闘中にしか表示しないウィンドウの作成関数が定義されているはずです。

Scene_MapcreateAllWindows関数の、Scene_Messageの同名関数呼び出し部分を実際の内容に置き換えてみると以下のようになります。

Scene_Map.prototype.createAllWindows = function() {
   this.createMapNameWindow();
   // 以下は実際には、Scene_Message側に定義されている
   this.createMessageWindow();
   this.createScrollTextWindow();
   this.createGoldWindow();
   this.createNameBoxWindow();
   this.createChoiceListWindow();
   this.createNumberInputWindow();
   this.createEventItemWindow();
   this.associateWindows();
};

実に8種類ものウィンドウを作成しているわけです(最後の関数は作成ではなく、ウィンドウ間の関連付け処理です)。

Scene_Messageと共通する部分を毎回記述するのは二度手間なので、これを省くためにScene_Message.prototype.createAllWindows.call(this)という処理をしているのです。その上で、マップ特有のウィンドウ作成関数が追加されています。

というわけで、Scene_Mapにてウィンドウを作成している関数はこれで分かりました。では個別のウィンドウ作成関数はどのような処理をしているのでしょうか。上記のcreateMapNameWindow関数の定義を見てみましょう。

Scene_Map.prototype.createMapNameWindow = function() {
   const rect = this.mapNameWindowRect();
   this._mapNameWindow = new Window_MapName(rect);
   this.addWindow(this._mapNameWindow);
};

一行目のrectという定数には、mapNameWindowRectという関数の戻り値を代入しているようです。結論から言うと、これはRectangleというツクールの組み込みクラスのインスタンスを作成して定数に格納する、という処理です。ツクールにはこのように、コアスクリプトにて定義されているJavaScriptの拡張クラスがいくつか存在します。それは以下の公式APIドキュメントにて確認できます。

Rectangleというクラス自体は非常にシンプルで、X座標、Y座標、幅、高さをプロパティとして保持するだけのクラスです。Rectangleとは矩形、つまり四角形のことなので、四角形を生成するためのクラスということですね。

その四角形が代入されたrectという定数は、次の行にて使われているようです。

this._mapNameWindow = new Window_MapName(rect);

JavaScriptにてあるクラスのインスタンスを新しく作成するには、new演算子を使用します。構文は以下の通りです。

new クラス名(引数)

ここでの引数は、コンストラクターというインスタンス生成時に呼び出される特殊な関数の引数となります。
つまりrect定数を引数として、Window_MapNameクラスの新しいインスタンスを作成しているというわけです。

ではWindow_MapNameはどのようなクラスなのでしょうか。再び検索によって定義箇所へジャンプしてみましょう。

スクリーンショット 2021-06-27 16.19.54

rmmz_windows.jsファイルにWindow_MapNameが定義されています。ここに記述されているのは新しいクラスを定義するときに用いられる定型句のようなもので、コアスクリプトのクラス定義箇所はほとんがこのようになっています。
ここで重要なのは以下の二つです。

・5273行目にて、Window_MapNameのスーパークラスとしてWindow_Baseを指定している
・5270行目および5274行目にて、initializeという名前の関数をコンストラクターとして指定している

Window_Baseはウィンドウの基本機能を定義しているクラスであり、すべてのウィンドウクラスがこれを継承しています。シーンクラスにとってのScene_Baseと同じなので、名前もわかりやすいですね。

initializeという関数自体は5276行目から定義されています。この関数は引数を一つ取るのですが、これが上記のnew演算子によってインスタンスが作成される際に指定される引数に対応します。
なおコンストラクターに指定する関数の名前(識別子)は自由につけられるのですが、コアスクリプトではinitializeで統一されています。ツクールがXP〜VXAce時代に採用していたプログラミング言語Rubyではコンストラクターの名前は必ずinitializeにする必要があるのですが、その名残りであると思われます。

Scene_MapcreateMapNameWindowに戻ると、つまりrect定数を引数としてWindow_MapNameのコンストラクターメソッドinitializeを呼び出してインスタンスを新しく作成する、という処理を行なっていたということになります。
ウィンドウはどこに表示するのかという座標と、どのくらいの大きさかという寸法(幅・高さ)情報を必要とします。そのためインスタンス作成時にその情報をrect定数(Rectangleのインスタンス)によって渡している、ということなのです。

これでScene_Mapがどのようにウィンドウを作成しているのかが大体わかりました。ここからは実際にウィンドウを作成することを考えていきましょう。

スキルウィンドウ

作成するといってもツクールに定義されている非常に多くのウィンドウクラスのうち、一体どのウィンドウを作ればいいのでしょうか。
今回作ろうとしているのはマップ上でスキルを呼び出すためのウィンドウです。ということは、ツクールでスキルを選択するシーンに表示されているウィンドウが何であるかが分かれば、それを作成すればOKな感じがします。

ツクールでスキルを選択するシーンは2つありますよね。メニューでスキルを使用する場面と、戦闘です。メニューの方がわかりやすいのと、そもそも今まではメニューからスキルを使用していたということもあり、メニューの方を見てみましょう。メニュー上でスキルを使用するシーンのクラスはScene_Skillといいます。例によって検索で定義箇所にジャンプしましょう。

スクリーンショット 2021-06-27 16.52.13

rmmz_scenes.jsScene_Skill定義箇所です。このシーンのスーパークラスであるScene_ItemBaseというクラスは、「アイテム類(スキルや使用アイテム全般のことを指します)」の使用に関する共通機能を定義しています。スキルやアイテムの使用場面は使いたいスキル/アイテムを選択し、使用する対象を選択し、その効果が発動する、という流れが非常に似ていますよね。それは、これらのシーンクラスがどちらもScene_ItemBaseのサブクラスであるからなのです。

今探しているのはウィンドウの作成処理なのですが、1656行目にあるcreate関数がそれらしきことをしていそうですね。1659行目のcreateSkillTypeWindow関数が非常にそれっぽい雰囲気を醸し出しているので、その定義を見てみましょう。

スクリーンショット 2021-06-27 17.01.05

細かい処理はあまり問題ではなく、重要なのは作成しているウィンドウクラスの名前です。これはWindow_SkillTypeというそうです。
結論から言ってしまいますが、このウィンドウはスキルのタイプ、つまり「必殺技」や「魔法」を選択するためのウィンドウです。つまりコレです。

スクリーンショット 2021-06-27 17.06.23

これはこれで後で使うかもしれませんので、覚えておきましょう。ではその下の、スキル一覧を選択するウィンドウはどこで作成されているのでしょうか。結論から言ってしまうとcreateItemWindowにて作成されるウィンドウがそれになります。

スクリーンショット 2021-06-27 17.09.07

1703行目にあるこれがその関数の定義なのですが、作成されるウィンドウのクラス名はWindow_SkillListになります。まさにこれしかない、といった感じの名前ですね!

これでマップ上にウィンドウを作成する準備が整いました。早速その処理を組んでいきましょう!

マップ上にウィンドウを作成

ではどのようにマップ上にWindow_SkillListを表示させればいいのでしょうか。基本的にはすでにあるものを模倣することから始めた方が早いことが多いです。というわけで、まずはScene_Skillの処理をScene_Mapにコピーすることから始めてみましょう。

Scene_SkillcreateItemWindowの定義をコピーしてFieldAction.jsに貼り付けます。

スクリーンショット 2021-06-27 17.32.05

現時点ではまだScene_Skillの関数のままです。今やろうとしているのはマップ上にこのウィンドウを表示させることなので、この関数のクラス名をScene_Mapに置き換えます。

Scene_Map.prototype.createItemWindow // クラス名のみ変更

たったこれだけです。続いては、この関数を実際に呼び出す処理を組みます。Scene_Mapでウィンドウを作成しているのは、createAllWindows関数でしたね。これをコピーしてプラグインに貼り付けます。

スクリーンショット 2021-06-27 17.36.22

この関数の中で先ほどのcreateItemWindow関数を呼び出そう、というわけです。以下のようになるでしょう。

Scene_Map.prototype.createAllWindows = function() {
    this.createMapNameWindow();
    Scene_Message.prototype.createAllWindows.call(this);
    this.createItemWindow(); // 追加処理
};

基本的にはこれで正常に動作自体はするはずです。ただし、この方法はプラグインとしてはあまり好ましくありません。というのも、この関数の定義を完全に上書きしてしまっているからです。これは、プラグイン競合の原因になりがちなのです。

例えばあるプラグインが、マップ上にステータス画面を表示するという機能を持っていたとします。そしてそれは、このcreateAllWindows関数の中でウィンドウ作成関数を呼び出すことで実装しているとします。すると、どちらのプラグインも有効にしていた場合スキルリストウィンドウとステータスウィンドウのどちらか片方しか表示されなくなってしまうのです。
これを回避するためには、元々のcreateAllWindows関数の処理を逃してやる必要があります。具体的には、以下のようにします。

const _Scene_Map_prototype_createAllWindows = Scene_Map.prototype.createAllWindows;
Scene_Map.prototype.createAllWindows = function() {
    _Scene_Map_prototype_createAllWindows.call(this); // 元の処理の呼び出し
    this.createItemWindow(); // 追加処理
};

これは、_Scene_Map_prototype_createAllWindowsという名前の定数に元々のcreateAllWindows関数を代入した上で、それをcallメソッドで呼び出す、ということをしています。こうすることで元の処理を残しつつも、新しい処理を加えることができるのです。なお元々あった以下のコードはこの元処理呼び出しにより実行されますので、不要になります。うっかり残してしまうと同じ処理を2回実行することになってしまい、予期せぬ不具合の原因になりますので注意しましょう。

this.createMapNameWindow();
Scene_Message.prototype.createAllWindows.call(this);

このような、コアスクリプトに元々定義されている関数に新しい処理を追加するために別名定数に逃すというのはプラグイン制作において非常にポピュラーなテクニックであり、公式プラグイン講座でも紹介されています(以下のリンクの「既存メソッドの再定義」をご覧ください)。


このときにつける定数の名前は、以下のような命名規則に従ってつけられることが非常に多いです。

_クラス名_prototype_関数名

これはあくまでも慣習であり、まったく別の名前をつけたとしても問題なく動作します。ただしコードを解読する人の負担を減らしたり、自分にとっても命名に迷わなくて済むといった利点を考えると、十分に従う価値のある慣習であるといえます。本シリーズでは今後もこの命名規則に従うことにします。

ここまででスキルリストウィンドウをマップ上に表示させるための処理は追加できたと思います。早速テストしてみましょう。

スクリーンショット 2021-06-27 17.59.22

おや、エラーになってしまいましたね。どうもitemWindowRectという関数がないことが原因のようです。改めて、先ほど貼り付けたcreateItemWindow関数を見てみましょう。

Scene_Map.prototype.createItemWindow = function() {
     const rect = this.itemWindowRect();
     this._itemWindow = new Window_SkillList(rect);
     this._itemWindow.setHelpWindow(this._helpWindow);
     this._itemWindow.setHandler("ok", this.onItemOk.bind(this));
     this._itemWindow.setHandler("cancel", this.onItemCancel.bind(this));
     this._skillTypeWindow.setSkillWindow(this._itemWindow);
     this.addWindow(this._itemWindow);
};

最初の行でitemWindowRect関数を呼び出しています。これは上述のmapNameWindowRect関数と非常によく似た関数でありスキルリストウィンドウ用のRectangleインスタンスを返すためのものなのですが、この関数が定義されているのはScene_Skillです。createItemWindowScene_Mapにも定義したのはいいのですが、これを正常に動かすためにはitemWindowRect関数も一緒にコピーする必要があったのです。早速貼り付けましょう。

スクリーンショット 2021-06-27 18.28.35

そしてこの関数のクラス名もScene_Mapに書き換えます。

Scene_Map.prototype.itemWindowRect  // クラス名のみ変更

​再度テストしてみましょう。

スクリーンショット 2021-06-27 18.31.04

またエラーですね…今度はyというプロパティがない、と怒られてしまいました。

コンソールには以下のように出ています。

スクリーンショット 2021-06-27 18.34.11

どうもitemWindowRectの中で起きているエラーのようです。この関数をよくみてみましょう。

Scene_Map.prototype.itemWindowRect = function() {
    const wx = 0;
    const wy = this._statusWindow.y + this._statusWindow.height;
    const ww = Graphics.boxWidth;
    const wh = this.mainAreaHeight() - this._statusWindow.height;
    return new Rectangle(wx, wy, ww, wh);
};

wyという定数に代入するためにthis._statusWindow.y + this._statusWindow.heightという計算をしているのですが、このstatusWindowとはScene_Skillで使用されているウィンドウなのです。このウィンドウはScene_Mapにはありませんので、結果存在しないプロパティのY座標を呼び出そうとしてエラーとなった、ということなのです。

そもそもこの関数はスキルリストウィンドウの座標と大きさを決定するための関数なのですが、細かい調整は後で行うことにします。ですので今は、エラーをなくすことを優先することにしてstatusWindowを参照している箇所を適当な値に置き換えてしまいます。

Scene_Map.prototype.itemWindowRect = function() {
     const wx = 0;
     // const wy = this._statusWindow.y + this._statusWindow.height;
     const wy = 0; // 暫定値
     const ww = Graphics.boxWidth;
     // const wh = this.mainAreaHeight() - this._statusWindow.height;
     const wh = 300; // 暫定値
     return new Rectangle(wx, wy, ww, wh);
};

ご覧のようにY座標を0に、高さを300にしました。元の処理も念のためにコメントアウトして残してあります。
再テストしてみましょう。

スクリーンショット 2021-06-27 18.52.40

またエラーです!
今度はどこですかー!?

スクリーンショット 2021-06-27 18.53.53

今度はcreateItemWindowの中のようです。

Scene_Map.prototype.createItemWindow = function() {
    const rect = this.itemWindowRect();
    this._itemWindow = new Window_SkillList(rect);
    this._itemWindow.setHelpWindow(this._helpWindow);
    this._itemWindow.setHandler("ok", this.onItemOk.bind(this));
    this._itemWindow.setHandler("cancel", this.onItemCancel.bind(this));
    this._skillTypeWindow.setSkillWindow(this._itemWindow);
    this.addWindow(this._itemWindow);
};

どうもこの中のbindという関数が悪さしているようです。結論から言うと、とりあえず以下のようにこの関数内の一部をコメントアウトしてしまいましょう。

スクリーンショット 2021-06-27 18.56.38

これらも先ほどのstatusWindowのようにScene_Skillには存在するが、Scene_Mapにはまだ存在しない関数等を参照しようとしてエラーになってしまう処理です。これらは単に動かすだけならなくても問題ありませんので、とりあえず暫定措置としてコメントアウトしてしまいます。

さあもう一度テストです、今度こそうまくいくでしょうか!?

やりました、今度こそマップ上にスキルリストウィンドウを表示することに成功しましたね!
今は単に表示しているだけで具体的な機能は何もありませんが、それについては次回以降作り込んでいくことにします。

まとめ

というわけで今回はマップ上に新しいウィンドウを表示するための処理を作成しました。お疲れ様でした。

その過程でオブジェクト指向プログラミングについてごく簡単にではありますがご紹介したり、エラーが起きたときの対処法などについても触れました。
これらのトピックは今後も重要な基盤になりますので、ぜひマスターしていただきたいと思います。

次回はこのウィンドウに実際にスキル一覧を表示させる処理を組んでいきたいと思います。

それではまた次回お会いしましょう!

今回までの最終コード

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

/*:
* @target MZ
* @plugindesc 
* @author 
*
* @help 
*
* @param skillReactionPatterns
* @text Skill Reaction Patterns
* @desc Contains skill reaction patterns as many as necessary.
* @type struct<switchPatternList>[]
* 
* @command toggleSwitchesByFacingSkillTargets
* @text Turn Switches on by Events Faced by Player
* @desc Turns on switches whose IDs correspond to <skillReactionId: > note of events faced by the player.
*
* @command requestAnimationAtFacingSkillTargets
* @text Show Animation at Facing Events
* @desc Show a specified animation at events faced by the player.
* 
* @arg animationId
* @text Animation ID
* @desc ID of the animation to be shown.
* @type animation
* 
*/

/*~struct~switchPatternList:
*
* @param identifier
* @text Identifier
* @desc The identifier for skill reaction patterns.
* @type string
* 
* @param patterns
* @text Skill Reaction Pattern List
* @desc The list for skill reaction patterns.
* @type struct<switchPattern>[]
* 
*/

/*~struct~switchPattern:
*
* @param commonEventId
* @text Common Event ID
* @desc The common event ID which affects the event.
* @type common_event
* 
* @param selfSwitchCh
* @text Self Switch Character
* @desc The self switch character which will change.
* @default A
* @type select
* @option A
* @option B
* @option C
* @option D
* 
* @param selfSwitchValue
* @text Self Switch Value
* @desc The changed Value of the self switch.
* @type boolean
* @default true
* 
* @param selfSwitchCondition
* @text Self Switch Condition
* @desc The self switch character which needs to be on for enabling this reaction.
* @default null
* @type select
* @option none
* @value null
* @option A
* @option B
* @option C
* @option D
* 
*/

/*:ja
* @target MZ
* @plugindesc 
* @author 
*
* @help 
*
* @param skillReactionPatterns
* @text スキル反応パターン
* @desc スキルに対する反応パターンを、反応の種類だけ定義します。
* @type struct<switchPatternList>[]
* 
* @command toggleSwitchesByFacingSkillTargets
* @text 直前イベントスイッチオン
* @desc プレイヤーの目の前のイベントのメタタグに設定されているスイッチをオンにします。
*
* @command requestAnimationAtFacingSkillTargets
* @text 直前イベントアニメーション表示
* @desc プレイヤーの目の前のイベントに指定したIDのアニメーションを表示します。
* 
* @arg animationId
* @text 表示アニメーション番号
* @desc 表示するアニメーションの番号です。
* @type animation
* 
* 
*/

/*~struct~switchPatternList:ja
*
* @param identifier
* @text 識別子
* @desc スキル反応パターンの識別子です。
* @type string
* 
* @param patterns
* @text スキル反応パターンリスト
* @desc スキル反応パターンのリストです。
* @type struct<switchPattern>[]
* 
*/

/*~struct~switchPattern:ja
*
* @param commonEventId
* @text コモンイベントID
* @desc イベントに作用するコモンイベントのIDです。
* @type common_event
* 
* @param selfSwitchCh
* @text セルフスイッチ記号
* @desc 変化するセルフスイッチの記号です。
* @default A
* @type select
* @option A
* @option B
* @option C
* @option D
* 
* @param selfSwitchValue
* @text 変化後スイッチ
* @desc 反応後に変化するセルフスイッチの値です。
* @type boolean
* @default true
* 
* @param selfSwitchCondition
* @text セルフスイッチ条件
* @desc この反応を有効にするためにオンになっている必要があるセルフスイッチの記号です。
* @default null
* @type select
* @option なし
* @value null
* @option A
* @option B
* @option C
* @option D
* 
*/

(() => {
   'use strict';


   const PLUGIN_NAME = "FieldAction";


   const skillReactionPatterns = JSON.parse(PluginManager.parameters(PLUGIN_NAME).skillReactionPatterns)
       .map(str => JSON.parse(str));
   for (let i=0; i<skillReactionPatterns.length; i++) {
       const str = skillReactionPatterns[i].patterns;
       const ary = JSON.parse(str).map(s => JSON.parse(s));
       for (let j=0; j<ary.length; j++) {
           for (const key of Object.keys(ary[j])) {
               const value = ary[j][key];
               if (!["A","B","C","D"].includes(value)) {
                   ary[j][key] = JSON.parse(value);
               }
           }
       }
       skillReactionPatterns[i].patterns = ary;
   }
   const SKILL_REACTION_PATTERNS = {};
   for (const obj of skillReactionPatterns) {
       SKILL_REACTION_PATTERNS[obj.identifier] = obj.patterns;
   }
   
   PluginManager.registerCommand(PLUGIN_NAME, "toggleSwitchesByFacingSkillTargets", args => {
       $gamePlayer.toggleSwitchesByFacingSkillTargets();
   });
   
   PluginManager.registerCommand(PLUGIN_NAME, "requestAnimationAtFacingSkillTargets", args => {
       const targets = $gamePlayer.facingSkillTargets();
       const animationId = Number(args.animationId);
       $gameTemp.requestAnimation(targets, animationId);
   });


   Game_Player.prototype.facingSkillTargets = function() {
       const direction = this.direction();
       const x1 = this.x;
       const y1 = this.y;
       const x2 = $gameMap.roundXWithDirection(x1, direction);
       const y2 = $gameMap.roundYWithDirection(y1, direction);
       const events = $gameMap.eventsXy(x2, y2);
       return events;
   };
   
   Game_Player.prototype.isFacingSkillTargets = function() {
       const events = this.facingSkillTargets();
       return events.length > 0;
   };
   
   Game_Player.prototype.toggleSwitchesByFacingSkillTargets = function() {
       const events = this.facingSkillTargets();
       for (const event of events) {
           const patterns = SKILL_REACTION_PATTERNS[event.skillReactionId()];
           if (patterns) {
               const commonEventId = $gameTemp.currentCommonEventId();
               const struct = patterns.find(obj => obj.commonEventId === commonEventId);
               if (struct) {
                   const selfSwitchCondition = struct.selfSwitchCondition;
                   const mapId = $gameMap.mapId();
                   const eventId = event.eventId();
                   let ok = false;
                   if (!selfSwitchCondition) {
                       ok = ["A","B","C","D"].every(ch => !$gameSelfSwitches.value([mapId, eventId, ch]));
                   } else {
                       ok = $gameSelfSwitches.value([mapId, eventId, selfSwitchCondition]);
                   }
                   if (ok) $gameSelfSwitches.setValue([mapId, eventId, struct.selfSwitchCh], struct.selfSwitchValue);
               }
           }
       }
   };
   
   
   Game_Event.prototype.skillReactionId = function() {
       return this.event().meta.skillReactionId || "";
   };


   Game_Temp.prototype.retrieveCommonEvent = function() {
       const commonEventId = this._commonEventQueue.shift();
       this._currentCommonEventId = commonEventId;
       return $dataCommonEvents[commonEventId];
   };
   
   Game_Temp.prototype.currentCommonEventId = function() {
       return this._currentCommonEventId;
   };

   
   Scene_Map.prototype.createItemWindow = function() {
       const rect = this.itemWindowRect();
       this._itemWindow = new Window_SkillList(rect);
       // this._itemWindow.setHelpWindow(this._helpWindow);
       // this._itemWindow.setHandler("ok", this.onItemOk.bind(this));
       // this._itemWindow.setHandler("cancel", this.onItemCancel.bind(this));
       // this._skillTypeWindow.setSkillWindow(this._itemWindow);
       this.addWindow(this._itemWindow);
   };
   
   const _Scene_Map_prototype_createAllWindows = Scene_Map.prototype.createAllWindows;
   Scene_Map.prototype.createAllWindows = function() {
       _Scene_Map_prototype_createAllWindows.call(this);
       this.createItemWindow();
   };
   
   Scene_Map.prototype.itemWindowRect = function() {
       const wx = 0;
       // const wy = this._statusWindow.y + this._statusWindow.height;
       const wy = 0; // 暫定値
       const ww = Graphics.boxWidth;
       // const wh = this.mainAreaHeight() - this._statusWindow.height;
       const wh = 300; // 暫定値
       return new Rectangle(wx, wy, ww, wh);
   };

})();

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