選択ウィンドウを作ってみる

 yamachanさんのショップ販売のカテゴリ化プラグインを作成してみるや SigureyaさんのWindow_Selectableの解説 - Qiitaを見つつ、選択ウィンドウを追っていこうと思います。

addCommand あるのは Window_Command ですけどね

残りのウィンドウ

 今回は残ったウィンドウを探っていきます。
 Window_Selectable の直下には たくさんクラスがありますが、メニューらしいメニューは Window_Command を基本としてつくられているので、 Window_SelectableWindow_Command を中心に見ていこうと思います。

 Window_SelectableWindow_Command をじっくりやって、その下のやつはまた必要になったら調べるという感じで、今回は流しちゃおうと思います。

Window_Selectable

 まずは今回調べるクラスの大元である Window_Selectable を使ってみましょう。
 前回と同様に、Scene_Title.prototype.create の後ろの方に間借りします。


	// ↓ ここから追加部分
	const selectWindow = new Window_Selectable( 100, 20, 400, 200 );
	this.addChild( selectWindow );
};

 表示されましたが、選択肢もないし、当然選択した後の処理も何もありません。
 Window_Selectable を読んでみたら drawItem() というコマンド項目を描くメソッドはあるんですが中身は空です!
 データの保持と描画は継承して新しいクラスで実装してね、ということみたいです。
 ごく基本的なテキストコマンドは実行できると思ってましたが、意外。
 文字や画像の表示に関しては今まで追ってきたように Window_Base が持っているので、それを継承したWindow_Selectable でも当然使えます。
 活用して drawItem() の中身を実装すれば良いというわけです。

 それに入力関係の処理やレイアウトの処理などは一通り Window_Selectable に揃っています。

Window_Command

 Window_Command は、いわゆるコマンドメニューを実現するクラスです。
 このクラスの中身を調べておけば、自分で Window_Selectable を拡張してクラスを作る場合もスムースに実現できるはず。

 Window_Selectable は選択項目のデータを持っていません。
 そこで Window_Command_list という配列プロパティを用意して、そこにコマンドに必要な情報を格納しています。
 そして選択項目の最大数を返す maxItems() メソッドを、このリストの長さを返すものに変えてあります。

 この全体の数さえ判明すれば、その他の部分はいい感じに処理してくれます。drawItem( index ) は引数の項目番号をもとに位置を決めて描画すればOK。
 位置決めのためのメソッドも用意してあって、itemRectForText( index ) に項目番号を渡せば矩形範囲( Rectangle )を返してくれます。
 この矩形範囲に沿って drawText() を使えば文字を描けます。
 なお、選択項目が文字列でない場合は itemRect( index ) がいい感じに選択範囲を返してくれます。

 しかし、ここまでの変更ではウィンドウしか表示されません。
 Window_Commandinitialize()refresh() を実行して、コマンド文字の描画を行っています。
 それでも、選択肢は表示されるもののカーソルの操作ができません。これはウィンドウにフォーカスが合ってないからです。
 なので続けて activate() を実行してウィンドウにフォーカスを合わせます。
 そしてまだカーソルが表示されません。初期状態では項目が選択されていないからです。
なので続けて、select( 0 ) として最初の項目を選択しています。

以上がおおよその Window_Selectable を拡張してコマンド選択ウィンドウにする手順です。
んが! まだコマンドリストは表示できないんですよ!!
コマンドリストの登録は makeCommandList() で行うように設計されていますが、 Window_Command では空のメソッドなんです。

Window_Command を継承する

 Window_Command のコードを読むと、initialize()refresh() が呼ばれて、そこからさらに makeCommandList() 呼ばれてます。
 だから普通に追加しただけでは空のウィンドウができてしまいます。
 後から makeCommandList() を書き換えてコマンドを追加する機能をつける手もありますが、ここはもう新たにクラスを作りましょう。
 1・2・3を選べるものにするので、名前は Choice_123 にしましょう。
 RPGツクールMVもずっと Window_ じゃなくて接頭辞変えた方がわかりやすいと思うんですけどね。


	// ↓ ここから追加部分
	const choice = new Choice_123( 100, 20 );
	this.addChild( choice );
};

class Choice_123 extends Window_Command {
	constructor( x, y ) {
		super( x, y );
	}
	makeCommandList() {
		this.addCommand( '1', 'command1' );
		this.addCommand( '2', 'ok' );
		this.addCommand( '3', 'cancel' );
	}
}

 class は最近の JavaScript 環境だったらだいたい動く書式で、RPGツクールMVで採用されている書き方ではありませんが非常にわかりやすいので使っていきます。
 ここでは class について解説するのは本題ではないので避けます。
 makeCommandList() の中で addCommand() というこれも Window_Command のメソッドを呼んでいます。
 だいたい予想できる通りの機能ですね。実際に画面に表示されるラベル('1')と、スクリプトの操作で使うシンボル('command1')を設定してます。
 ラベルとシンボルが分かれているのは多言語対応を見越した仕組みだと思いますが、RPGツクールMVそのものがイマイチ多言語対応機能に積極的でないので、宝の持ち腐れというかただ面倒臭いだけになってる感じがあります。

 さて、それはそれとして、これで一見コマンド選択ウィンドウができたように見えますが、決定しても何も起きません。
 選択後の処理を設定する必要があります。面倒くさいですね〜(笑)

Window_Command にハンドラを設定

 選択後の処理は setHandler() メソッドで実行する関数( ハンドラ ) を指定していきます。
 addCommand() でいっぺんにやりたい感じもしますが処理を分けておくことで、表示まではウィンドウで行い入力への対処はシーンで行うことが可能になります。

 方向キーによる移動やタッチでの選択なんかは Window_Selectable がいい感じにやってくれるので、実際ハンドラを設定するのは、決定・キャンセルの処理です。
 これらは Window_Selectableで設定されています。決定は callOkHandler() 、キャンセルは callCancelHandler () です。
 中身はそれぞれ this.callHandler( 'ok' );this.callHandler( 'cancel' ); を呼んでるだけの単純なものです。
 Window_Command では callOkHandler()上書きされていて(オーバーライド)addCommand() で設定されたシンボルが呼ばれています。

 オブジェクトはそのままで、追加部分を書き換えます。


	// ↓ ここから追加部分
	const choice = new Choice_123( 100, 20 );
	this.addChild( choice );
	choice.setHandler( 'command1', () => {
		alert( 'ダイアログに出力' );
		choice.activate();
	} );
	choice.setHandler( 'ok', () => {
		console.log( 'コンソールへ出力!' );
		choice.activate();
	} );
	choice.setHandler( 'cancel', () => {
		choice.deactivate();
		choice.close();
		this._commandWindow.activate();
	} );
};

 choice.activate(); しているのは、連続で決定やキャンセルが実行されないように deactivate(); されてるんで、それを戻す処理です。
 実際、2 は決定キーを押している間連続して発生します。
 2 のシンボルに ok を指定してますが、全ての選択肢の決定で呼ばれるのではなく、設定した選択肢で発生します。
 3 はシンボルの方に cancel を指定しているので、3 を選んだ場合もキャンセルキーを押した場合も実行されます。
 実行内容は、新規に作ったウィンドウを閉じて選択ウィンドウを作ってみるコマンドへ選択権を渡すようにしてます。

 なんかもう選択ウィンドウは十分作れるような気がしてきました!!

Window_Selectable を継承する

 以上を踏まえて、Window_Selectable を継承したクラスも作ってみました。
 もう一通り解説してるんで、改めて解説するところはないと思います。


	// ↓ ここから追加部分
	const choice = new Selectable_123( 100, 20 );
	this.addChild( choice );
	choice.setHandler( 'ok', () => {
		console.log( `選択肢番号: ${choice.index()}` );
		choice.activate();
	} );
	choice.setHandler( 'cancel', () => {
		choice.deactivate();
		choice.close();
		this._commandWindow.activate();
	} );
};

class Selectable_123 extends Window_Selectable {
	constructor( x, y ) {
		super( x, y );
		this.refresh();
		this.activate();
		this.select( 0 );
	}
	initialize( x, y ) {
		this._data = [ 'いち', 'にー', 'さん' ];
		super.initialize( x, y, 200, this.fittingHeight( this._data.length ) );
	}
	maxItems() {
		return this._data.length;
	}
	drawItem( index ) {
		const rect = this.itemRectForText( index );
		this.drawText( this._data[ index ], rect.x, rect.y, rect.width );
	}
}

まとめ

 というわけで、基本的なウィンドウの仕組みは分かった(ような気がする)ので、ガシガシとメニューを書き換えていけると思います。
 Window_BattleLog など、全然よく分かってないやつもチラホラありますが、つどつど必要になったら調べていこうと思います。
 というか、必要なんだけどよく分かってないんで調べてみました記事が書けないんだけどね。

 そこで結論。