⚔️

『RPGツクールMZ』シーンのウィンドウ生成を調べてみた

2021/06/09に公開

『RPGツクールMZ』関連記事 目次

いままではウィンドウ中心にコードを追ってきましたが、今回は階層を一つ上がって、シーンがウィンドウを生成するところを追ってみたいと思います。

例によって適宜『RPGツクールMZ』非公式JavaScriptリファレンスのページにクラスなどリンクしていきます。

そう言えば、どのバージョンのスクリプト読んでるか書いてなかった気がしますが、1.0.2 のコアスクリプトです。
クラスごとに分割したファイル作ってそれ読んでるんですが、新しいの分割するの面倒くさくて、はっはっは。
アップデートしてないのかよって話ですが、まぁあんまり変わってませんし!

ウィンドウ生成の基礎

急がば回れ、まずは基礎的なウィンドウ生成の挙動を復習します(書いたつもりで書いてないかもしれないし)

ウィンドウはシーンで生成され WindowLayerAddWindow() で追加されるのが基本的な作りです。
この際にコンストラクタは new Window( rect ) という感じに矩形範囲(位置と大きさ)を持った Rectangleデータを渡されます。
この各ウィンドウの Rectangleを生成するメソッドは 〇〇WindowRect() という形でシーンが持っています。
ここでシーンの役目はいったん終わりです。

そして Window 側の初期化処理になります。
コンストラクタから initialize() さらに _createAllParts() が呼ばれ、各種部品が準備されます。
この時点では、部品は用意されるものの中身は空っぽです。

windowskin プロパティが設定されると _onWindowskinLoad() ハンドラが設定され、ウィンドウ画像の読み込み待ちとなります。
が、 Windowクラスには windowskin を設定している箇所はありません。
なので Window_Base のコードを見てみます。
initialize() にある loadWindowskin()windowskinが設定されてます。
ここまでがウィンドウ側の初期化処理です。

そして、またシーンに戻って、ウィンドウに追加でプロパティを設定したりします。
ここで追加されるプロパティは、関連オブジェクトの情報への参照であったり、描画内容だったり色々です。

初期設定が終わって他の諸々の処理をしているうちにウィンドウの画像が読み込まれます。
すると先ほど設定して追いた Windowクラスの _onWindowskinLoad() が呼び出され、_refreshAllParts() が実行されます。
ここでやっと、ウィンドウの描画がなされます。
大抵はウィンドウ枠の画像はキャッシュされている状態なので、ほぼノータイムで実行されるんですけどね。

この _refreshAllParts() なんですけど、何かと呼ばれるんですよね。
具体的には width, height, padding, marginのプロパティおよび move()メソッドで width, heightいずれかが更新された時。
各種プロパティが定まってない状態で描画メソッドが呼ばれることも多くて、プロパティに 0 とか undefinednull みたいなのが入ってる場合の回避コード仕込まなきゃいけなかったりします。面倒臭い。

以上、ウィンドウ生成時の基本的な動作でした。

シーンの生成

さっきのウィンドウ生成の流れでは「ウィンドウはシーンで生成され」といきなり書いてますが、その前の部分を見ていきます。
まず、シーンはどこで生成されるのか。

これはズバリ SceneManager です。
SceneManager は静的クラスで、スクリプトのどこからでも直接利用できます。
そして、ほとんどの画面周りのオブジェクトは SceneManager を起点として制御されています。

ちなみに処理の起点となる SceneManagerupdate()メソッドは Graphics から呼ばれていますが、こっちはそんなに気にしなくてもいいと思います。
「なんか知らんけど SceneManagerupdate()は毎フレーム実行される」ぐらいの理解でいいかと。

さて今回はSceneManagerは詳しくみるのが目的ではないので、シーンがウィンドウを生成する create() が呼ばれているところを探します。
すると changeScene()メソッドの中に this._scene.create(); と書いた行があり、確かに呼んでいます。
ちなみにこの _scene というプロパティに現在のシーンが入っていて、前述の通りシーンを起点としてオブジェクトが制御されるのですから、かなり重要なプロパティです。
デベロッパーツールの[Console]からだと SceneManager._scene でアクセスできます。

メッセージウィンドウ編

生成するものはシーンごとに異なるので、代表的なものとしてまずは Scene_Message を見ていきます。
これまで Window_Message を調べてきたので、実家のような安心感ありますからね。

実際に使われているのは Scene_Message を継承した Scene_MapScene_Battle になるのですが、今回は Scene_Map を中心に見ていくことにします。

メッセージウィンドウの準備

Scene_MessageSceneManager から呼ばれる create() は持っていません。
ちなみに、Scene_Message のスーパークラスである Scene_Basecreate() はメソッド自体存在するものの、中身は空です。
コメント記号しかないですが…せっかくだからなんか説明書けばいいのに。

Scene_Base.prototype.create = function() {
    //
};

次に Scene_Mapcreate() です。

Scene_Map.prototype.create = function() {
    Scene_Message.prototype.create.call( this );
    this._transfer = $gamePlayer.isTransferring();
    this._lastMapWasNull = !$dataMap;
    if( this._transfer ) {
        DataManager.loadMapData( $gamePlayer.newMapId() );
        this.onTransfer();
    } else if( !$dataMap || $dataMap.id !== $gameMap.mapId() ) {
        DataManager.loadMapData( $gameMap.mapId() );
    }
};

ちゃんと中身があります。んが、ホッとしたのもつかの間。
それっぽい名前の createAllWindows() は呼ばれていません。

コールスタックによるコードの追い方

こういったコードが追えなくなった場合、挫折しちゃう人も多いんじゃないでしょうか。
ひたすらコードの検索を繰り返すとか console.log() で情報を出力する手もありますが、他にもいい方法があります。

F8で開くデベロッパーツールで[Sources]タブを選択して、左のファイルブラウザから目的のファイル(rmmz_scenes.js)を選びます。
createAllWindows() の適当な行の行番号の前をクリックしてブレークポイントを置きます。
ゲームウィンドウに戻ると、ブレークポイントをつけた行を実行した時にプログラムの実行が停止します。
この時、デベロッパーツール右側の[Call Stack] パネルをみると、どこから呼び出されているか経路が一発で分かります。

『RPGツクールMV』のデベロッパーツールは使えない機能が多かったのでありがたいですね。
ちょっとしたことならこのデベロッパーツールでも十分いけます。

ちなみに、このデベロッパーツールの文字が小さすぎる場合は[⌘^]で大きくなります。
なんか[ショートカット]メニューには[⇧+]とか書いてあるんですが、[⌘^]で行けます。
(Windowsがどうなってるかは知りません)

この辺りのデバッグ機能は、コードエディタを利用すれば、さらに使いやすくできます。
Visual Studio Codeの環境構築については、『RPGツクールMZ』のJavaScript開発環境構築 を参考にしてください。

メッセージウィンドウの準備つづき

さて、コールスタックを見てみると createAllWindows()onMapLoaded() から createDisplayObjects()を通して呼ばれてます。
onMapLoaded()はマップデータ(data/MapXXX.json)を読み込み終えたタイミングで呼ばれるんですけど、その読み込み自体は create()DataManager.loadMapData() で設定してあります。

そしてスーパークラスの Scene_MessagecreateAllWindows() から createMessageWindow() が呼ばれ、ついに今回の調査対象である Window_Message が生成されます。
これで Scene_Message(Scene_Map) 側でのメッセージウィンドウの生成は完了です。

しょーじき、なんでマップデータの読み込み待ってからウィンドウの生成しなきゃいけないのか、よく分かってません。
その辺調べるとまた時間かかりそうなので、今回は放っておきます。

メッセージウィンドウの内部の準備

Scene_Message 側での動きはわかったので、今度はWindow_Message の生成を見ていきましょう。
例によって、最初は initialize() から。
this.openness = 0 とやって close状態にしてますね。

そこから呼ばれてる initMembers() で付属ウィンドウへの参照の初期化をしているのだけど、それらのウィンドウの生成はしてないし、参照にウィンドウを割り当てるメソッドはあるけど、それらのメソッドの呼び出し箇所は Window_Message の中にはないのです。
どっから呼ばれているかというと Scene_MessageassociateWindows() メソッドです。
この処理は後述します。とりあえず、この付属ウィンドウたちは放っておきましょう。

ともかくメッセージウィンドウは生成された後、閉じた状態で待機してます。
ここまでがウィンドウ生成時の準備動作。

シーン側でのメッセージウィンドウの準備

そしてまたシーンに戻ってきます。
まず生成して初期化まで終わった Window_MessageScene_Message の子オブジェクトとして登録されます。
ここで、メッセージウィンドウを生成する createMessageWindow()メソッドのコードにコメントをつけたものを見ておきましょう。

Scene_Message.prototype.createMessageWindow = function() {
    const rect = this.messageWindowRect();  // ウィンドウの位置・大きさの Rectangleデータを得る
    this._messageWindow = new Window_Message( rect );   // メッセージウィンドウの生成
    this.addWindow( this._messageWindow );  // メッセージウィンドウをシーンに登録
};

これで一応、メッセージウィンドウが使えるようになったのですが、もうちょっとやることがあります。
createAllWindows() の最後に associateWindows() というメソッドがあります。
さっきちょっとだけ出てきましたね。

Scene_Message.prototype.associateWindows = function() {
    const messageWindow = this._messageWindow;
    messageWindow.setGoldWindow( this._goldWindow );
    messageWindow.setNameBoxWindow( this._nameBoxWindow );
    messageWindow.setChoiceListWindow( this._choiceListWindow );
    messageWindow.setNumberInputWindow( this._numberInputWindow );
    messageWindow.setEventItemWindow( this._eventItemWindow );
    this._nameBoxWindow.setMessageWindow( messageWindow );
    this._choiceListWindow.setMessageWindow( messageWindow );
    this._numberInputWindow.setMessageWindow( messageWindow );
    this._eventItemWindow.setMessageWindow( messageWindow );
};

これ何やってるかというと、メッセージウィンドウとその付属ウィンドウのプロパティに互いの参照をセットしてるんです。
どーも、Window_Message に役割が多過ぎて、無駄に他のクラスとのやりとりが発生している感があります。
個人的には Window_Message は単体で存在させて現在の付属ウィンドウ群は Scene_Message 側で管理してほしいんだけどね。…なんか前にも書いた気がするなこのこと。

まともかく、これでメッセージウィンドウの準備は整いました。
後の処理は『RPGツクールMZ』メッセージ表示の仕組みを調べてみたに書いてます。

コマンドウィンドウ編

今度はコマンドウィンドウの生成部分を調べてみます。
前に 選択ウィンドウを作ってみるタイトルとメニューのウィンドウを調べてみた という記事を書いて『RPGツクールMV』について調べたことはあるのですが、『RPGツクールMZ』でウィンドウ周りが結構変わったので再度チャレンジです。
コマンドウィンドウを生成するシーンはたくさんあるのですが、今回は Scene_Title を調べることにします。
起動したら最初に表示されるシーンですし、ウィンドウはひとつしかないし、テストにぴったりです。

コマンドウィンドウの生成

基本のところは Scene_Map と同じなので、create()メソッドから見ていきましょう。

Scene_Title.prototype.create = function() {
    Scene_Base.prototype.create.call( this );
    this.createBackground();
    this.createForeground();
    this.createWindowLayer();
    this.createCommandWindow();
};

Scene_Map と違って特に読み込みを待つデータはないので create() から直に createCommandWindow() が呼ばれてます。
簡単でいいですね!

Rectangleデータを作ってウィンドウを生成するのはメッセージウィンドウと一緒です。
ここでは Window_TitleCommand が生成されています。

コマンドウィンドウの設定

ウィンドウが生成されたら初期化が動きます。
ウィンドウにコマンドのラベルを設定しなければなりません。

intialize() から素直にたどっていくと、makeCommandList() にたどり着きます。
名前からあたりをつけて、コールスタックを見てもいいですね。

さてこのmakeCommandList() を見てみると。

Window_TitleCommand.prototype.makeCommandList = function() {
   const continueEnabled = this.isContinueEnabled();
   this.addCommand( TextManager.newGame, "newGame" );
   this.addCommand( TextManager.continue_, "continue", continueEnabled );
   this.addCommand( TextManager.options, "options" );
};

ここで3連発されている addCommand()Window_Command が持ってるメソッドです。
ラベルを TextManager から取り出して、シンボルと結びつけます。
上から[ニューゲーム][コンティニュー][オプション]ですね。

ラベルとシンボルはどっちもコマンド名なんですが、
実際に画面に表示されるのをラベル、内部的に使うものをシンボルと言います。
別にラベルとシンボル一緒で良くない?って思う人もいるでしょうが、普通のアプリケーションだと別にしておけば、例えば言語が日本語から英語に変わってもシンボルは同じなので、ローカライズが容易になる利点があります。
でも『RPGツクールMZ』は言語切り替えを標準でサポートしてないので、特に意味のない仕組みになってる感はあります。

なお TextManager が持っているラベルの値は [データベース]-[用語]で設定した単語です。

第3引数に渡しているのは、最初に選択しておくか、という真偽値です。
ここではコンティニュー可能かどうかで、選択しておくかを決めてます。

シーン側でのコマンドウィンドウの準備

ウィンドウ側での初期化が終わって、シーンに戻ってきます。

Scene_Title.prototype.createCommandWindow = function() {
    const background = $dataSystem.titleCommandWindow.background;
    const rect = this.commandWindowRect();
    this._commandWindow = new Window_TitleCommand( rect );
    this._commandWindow.setBackgroundType( background );
    this._commandWindow.setHandler( "newGame", this.commandNewGame.bind( this ) );
    this._commandWindow.setHandler( "continue", this.commandContinue.bind( this ) );
    this._commandWindow.setHandler( "options", this.commandOptions.bind( this ) );
    this.addWindow( this._commandWindow );
};

コードを見るとここで、$dataSystem.titleCommandWindow.background のデータで背景タイプを設定してます。
この背景の設定はJSONデータ RPG.System.titleCommandWindow にあります。
あるけど、そもそもそんなの『RPGツクールMZ』エディタの[データベース]にあったっけ?

……あー、座標設定と一緒のダイアログに入ってますね。
このUIはちょっと微妙というか、これじゃ背景の設定見落としちゃいますね。
そもそも背景と座標が一緒に入ったダイアログなんか開かずに、[システム1]-[タイトル画面]のところに直においてあるべきです。
安直にダイアログを開くのは典型的ダメUIです。とにかくツクールは……くどくど……ということで、しっかりUIの専門家をつけて操作系全般を刷新してほしい!!

次に3連発されてる setHandler()Window_Selectable が持っているメソッドで、コマンドのシンボルとハンドラを登録します。

ハンドラというのはコマンドが選択された時に実行されるメソッドです。
.bind( this ) というのをつけておくとハンドラとなるメソッド内での this が現在の this つまり Scene_Titleインスタンスになります。
つけてないと実行されたその場のオブジェクトが this となるので Window_Selectable_handlersthis になります。
Scene_Titleのメソッドの thisScene_Title 以外だとややこしいので、bind() をしているというわけです。
このあたり、JavaScript の分かりにくさとしてあげられるものの筆頭ですが、「まぁそんなもんだ」の精神、「ツクールのコードがそうしているからそうしとこう」の精神で、乗り切っていきましょう!!

addCommand()setHandler()「バラバラにやらずに一気にやってくれよ!!」という気持ちになりますが、画面表示周りと処理本体は極力分離しておく方が、何かとあとあと都合が良いので、そんなもんだと思って諦めましょう。

最後に this.addWindow( this._commandWindow ); でコマンドウィンドウをシーンに登録。
以上で、コマンドウィンドウの準備は完了です。

まとめ

この辺の仕組み、なかなかピンとこない人も多いと思います。
実際僕も addCommand()setHandler()どっちがどっちだったっけ?ってなりがちです。

しばらく カスタムメニュー作成プラグインを調べてみた で紹介した『カスタムメニュー作成プラグイン』を使って慣れてから、再度読んで見ると理解が進むんじゃないかと思います。
『カスタムメニュー作成プラグイン』にすっかり慣れて、「別に自分でクラス作る必要なんかないな」という結論になるかもしれません。

とはいえどのウィンドウでも、このふたつを定型として理解していけば、初期化の部分はだいたい分かるんじゃないでしょうか。

エンジョイ! ツクールライフ!!

Discussion