2012/03/28
KAICHO: s_naray[at]yahoo[dot]co[dot]jp
※SPAM防止のため捻ってある

吉里吉里/KAGプラグインの作り方

■はじめに

本文書は、吉里吉里/KAGを拡張するいくつかの方法のうち、 KAGプラグインの作り方について述べる。

KAGプラグインは、(KAGって名前が付いてるけど)実際にはTJSスクリプトだ。 data/system/Plugin.tjs の中に定義されている KAGPluginクラスを継承して、 自前でクラスを書き、それを使うKAGマクロを用意して、それらをKAGに登録する ことで、KAGを拡張することができる。

…なーんて書いても全然ピンと来ないだろうことはわかる。モノスゴよくわかる。 なんせ我輩もそうだったから。というか、解説文が少なすぎるんだよね。本家ですら ここの末尾にわずかに一行書いてあるだけだし。

まぁ…言えばTJSに挑戦!の 「第4章 KAG プラグイン」が一番詳しいと思う。詳しく知りたければ、そっち 見た方がいい。ただ、会話形式で縦長なのでちょい読みにくいというのがあって…… ええい、自己満足のために自分でも書いてみたかったんじゃーい!(本音)

■何故KAGプラグインなのか

吉里吉里/KAGを使い込んでいくと、TJSを使って自分用の何かを作りこむように なってくる。その時、例えば、「ユーザがセーブロードする時に、同時に この変数の値を保存・回復したいなー」とか、「画面をトランジションする時に 一緒に自作レイヤをXXしたいなー」とか、そういった『システムの動作にあわせて なにかしたい』欲求が出てくるだろう。これを可能にするのがKAGプラグインである。

逆に、こういう『システムの動作にあわせて何かしたい』のでなければ、あえて KAGプラグインを書く必要はない。なんか「もにょっ」としたTJSスクリプトを 「もにょっ」と追加しても十分な場合も多々ある。拙作ならCtrlSkipプラグインとか 実はそうなんだよね。

■KAGプラグインの実際

もう少し具体的に書くと、KAGプラグインは、KAGが動く時に、その動作に合わせて 呼ばれる自作メソッドを含むKAGPluginクラスの派生クラス(のインスタンス)だ。 以下のように定義する。

class 自作クラス extends KAGPlugin {
	(何か自作クラスで必要な処理)
};

global.自作クラスインスタンス = new 自作クラス();	// インスタンスを作成
kag.addPlugin(global.自作クラスインスタンス);		// KAGに登録

「自作クラスインスタンス」は、どこに定義してもいいのだが、まぁ普通は global. 以下に定義する。注意すべきは、global. 以下の値は、(明示的に 書き換えない限り)データをロードしようが何しようが吉里吉里を再起動するまで 変わらないこと。

で、実際KAGPluginクラス中で以下の7つのメソッドが定義されていて、それぞれ 以下のような機能を持っている。これに従い、KAGPluginを派生させた 自作クラスでそれらのメソッドをオーバーライドして定義すれば、それぞれの 動作を自作クラス上から制御可能になる。必要ないものは単純にオーバーライド しなければよい。そうすればそのメソッドについては何もしない。

以上!ホントにこれだけ。で、ここまで分かったら、data/system/Plugin.tjsを 見てみよう。上で書いた機能そのままに、KAGPluginクラスにメソッドが定義 してあるのがわかるだろう(中身は空だけど)。 また、元々中には何も定義されていないから、 オーバーライドしたメソッドからスーパークラスのメソッドを呼び出す必要がない ことがわかる。これは今後変更になるかもしれないけれど。

■KAGプラグインが登録された後の動作

実際、KAGPlugin関係がKAGシステムにどう実装されているかということを、例でもって 説明してみる。 例えば、[backlay]時に呼ばれるメソッドonCopyLayer()がどう呼ばれるかを例に示すと こんなカンジ。

  1. 事前にKAGPluginクラスを継承した自作クラスのインスタンスをkag.addPlugin()で登録しておく
  2. [backlay]を実行する
  3. data/system/MainWindow.tjsのKAGWindow.backupLayer()が呼ばれる
  4. この中で、登録済みの各KAGPluginインスタンスのonCopyLayer()が順次呼ばれる
わざわざリストアップすることもなかったよ!是非data/system/MainWindow.tjsの 該当箇所を読んで欲しい。そして、他のメソッドも同じように呼ばれることも 確認して欲しい。

■KAGプラグイン作るうえで考えねばならないこと

ずばり、主にセーブ・ロード。
吉里吉里を拡張する上で最大の難点は、セーブ・ロードの処理を自前で用意しなければ ならないことだ。他のシステムではこんなのシステム側でテキトーにやってくれる ものなのだが、吉里吉里はこれをTJS経由で自前で処理する必要がある。 実に面倒くさい。 とはいえ、だからこそKAGPluginには必要性があるわけだ。単純にKAGシステムに 機能追加しただけだとセーブ・ロードでその機能が上手く実行されなくなるから、 ここでonStore()やonRestore()などの前述のメソッドが有用だということが分かる… だろうか?

保存する値を全部 f.* や sf.* などのグローバル変数に直接書いとけばいいやん、 という話もあるにはある。 もちろん、それでなんとかなるならそういう方法で逃げちゃうのもアリだ。ただまぁ、 あんまグローバル変数使うっちうのもねぇ…ということがあったりなかったり、また 変数への書き込みタイミングが必要な場合(全部が同時に書き込まれないといけない とか)などにも対応するためには、やっぱり自前でクラス書くことが綺麗なことの方が 多い。と思う。んじゃないかな。まチョト覚悟はしておけ。

■KAGプラグインを作ってみる

というわけで、ここからは実際にKAGプラグインを自前で書いてみることにする。 なんかありそうだしKAGプラグインの例としてはうってつけなので、ここでは 『画面上に指定した文字列を表示し続ける』という頭が悪いプラグインを 作ってみよう。仕様は以下の通り。

●ファイル先頭

StaticText.ksの先頭から説明する。[return ...] となっているのは、二重定義を 防ぐため。前述のように、global.* の定義は消えることはないため、 『global.StaticText が存在すれば(=既にStaticTextクラスが定義されていれば)、 二度とは定義しない』ようにすればいい。

; 既に定義されていれば二重定義を防ぐためにすぐreturnする
[return cond="typeof(global.StaticText) != 'undefined'"]

; ここからTJSとする
[iscript]
(続く)

●クラス定義先頭

定義するクラスは StaticText、これはKAGプラグインとして作成するので、KAGPlugin クラスを継承しておく。クラス内で保持するデータとしては、文字列を表示する 前景レイヤと背景レイヤを一つづつ、それと、表示する文字列だろう。文字列は 表と裏とで違う値を設定することを考え、こちらも一つづつ別に定義する。

(続き)
// StaticTextクラス定義。KAGプラグインなので、KAGPluginクラスを継承すること
class StaticText extends KAGPlugin {
	// 変数定義
	// 文字を表示するレイヤ(fore_layerとback_layer)
	var flayer, blayer;
	// 表示文字列
	var fstring = "", bstring = "";
(続く)

●StaticTextクラスのコンストラクタとデストラクタ

StaticTextクラスのコンストラクタでは、以下を実施する。

  1. 渡されたkagw(=global.kag, KAGウィンドウのインスタンス)上に前景・背景レイヤを作成
  2. 前景・背景レイヤを初期化(サイズ・表示位置・表示優先度調整)
あわせてフォントサイズもここで初期化しておく。これで、レイヤが定義され、 (デフォルトでvisible=trueなので)画面上に表示される(ただしデフォルトでは透明)。 しかし、まだ文字列は表示されないことに注意。

正直、デストラクタを綺麗に定義する意味はあまりない。KAGPluginとして登録 しているなら、インスタンスが破棄されるのはKAGPluginから登録抹消する時だけで、 通常それは吉里吉里を終了した時以外ないからだ。でもまぁ、綺麗にプログラムを 組む、ということで、ここでは「ちゃんと」デストラクタを定義しておく。

(続き)
	// StaticTextクラスのコンストラクタ
	function StaticText(kagw)
	{
		// レイヤを定義
		flayer = new Layer(kagw, kagw.fore.base);
		blayer = new Layer(kagw, kagw.back.base);
		// レイヤサイズを指定
		flayer.setSize(100,40);
		blayer.setSize(100,40);
		// 表示優先順位 を設定(ヒストリレイヤの一つ下)
		flayer.absolute = blayer.absolute = 2000000-1;
		// 表示する
		flayer.visible = blayer.visible = true;
		// フォントサイズを指定
		flayer.font.height = blayer.font.height = 24;
	}

	// StaticTextクラスのデストラクタ
	function finalize()
	{
		// 定義する意味はあまりないが、一応両方削除しておく
		invalidate flayer;
		invalidate blayer;
	}
(続く)

●画面に文字列を表示する

では、画面に文字列を表示するためのメソッド dispString() を追加しよう。 ここでは簡単に、指定した文字列を保存し、あわせて指定したレイヤ (fore/back/both)に文字列を書くようにしている。書き込み前に必ず 該当レイヤをクリアするので、書いた文字列が重なることはない。空文字列を 指定すると、レイヤをクリアして何も書かない。

個人的には'both'という指定が可能なのが好き。fore/back両方に一気に書き込む、 という指定。通常のKAGには無いが、こういうのがあるとスクリプタの作業量を 減らすことができるから。

(続き)
	// 文字列を指定レイヤに表示する
	function dispString(str, page = 'both')
	{
		if (page == 'fore' || page == 'both') {
			// 前景に表示する文字列に設定
			fstring = str;
			// まずレイヤをクリアして
			flayer.fillRect(0,0,flayer.width,flayer.height,0);
			// 文字列書き込み
			if (fstring != "")
				flayer.drawText(0, 0, fstring, 0xffffff);
		}
		if (page == 'back' || page == 'both') {
			// 背景に表示する文字列に設定
			bstring = str;
			// まずレイヤをクリアして
			blayer.fillRect(0,0,flayer.width,flayer.height,0);
			// 文字列書き込み
			if (bstring != "")
				blayer.drawText(0, 0, bstring, 0xffffff);
		}
	}
(続く)

●レイヤコピーに対応する

簡単に言えば[backlay]の時にどう動作するかを定義する。レイヤをコピーしたら、 その内容が同一にならなければならないので、表示文字列をあわせ、 実際に表示しておく。ここで blayer = flayer のようにしてレイヤをコピー してはいけない。それだと、インスタンスが一つになってしまうし、それまで 使用していたblayerを開放することなく非参照としているからだ。 まぁ一番簡単な方法はassignImage()だろうと思ったのでこれを使う。ただし、 assignImage()ではレイヤのvisibleメンバがコピーされないので、これを コピーすることを忘れずに。この後ろでvisibleメンバを使用するため必要になる。

(続き)
	// [backlay]または[forelay]の時に、レイヤをコピーする
	function onCopyLayer(toback)
	{
		if (toback) {
			// fore → back のコピーの時
			blayer.assignImages(flayer);
			blayer.visible = flayer.visible;
			bstring = fstring;
		} else {
			// back → forek のコピーの時
			flayer.assignImages(blayer);
			flayer.visible = blayer.visible;
			fstring = bstring;
		}
	}
(続く)

●トランジションに対応する

トランジション時、レイヤは表と裏を入れ替える必要がある。 今回の場合はそう。トランジションが完了すると、今まで flayer だったものは 背景レイヤに、blayer は前景レイヤに移動している(system/KAGPlugin.tjsの説明を 読むこと)。だから、このプラグインでも交換しておく必要がある。当然、 表示する文字列も交換する。

TJSでは型制限がないので、ここでは同じtmp変数で文字列とレイヤ両方を交換 している。わかりにくいと思ったら、適当に書き変えてもらって構わない。

(続き)
	// トランジションが完了したとき、表と裏を入れ替える
	function onExchangeForeBack()
	{
		var tmp;
		tmp = fstring;
		fstring = bstring;
		bstring = tmp;

		tmp = flayer;
		flayer = blayer;
		blayer = tmp;
	}
(続く)

●右クリックでメッセージレイヤが隠された時に隠す

メッセージレイヤが隠された・表示された時に、あわせてこの文字列表示レイヤも 表示・非表示を設定することにする。onMessageHiddenStateChange()メソッドを 追加するだけでよい。

(続き)
	// メッセージレイヤが隠された時、表示された時に呼ばれる。
	// hidden = 隠された:true、表示された:false
	function onMessageHiddenStateChanged(hidden)
	{
		if (hidden) {
			// 隠された
			flayer.visible = blayer.visible = false;
		} else {
			// 表示された
			flayer.visible = blayer.visible = true;
		}
	}

(続く)

●セーブ・ロードに対応する

セーブはonStore()、ロードはonRestore()メソッドを追加することで対応できる。 引数の意味は下を見るか、data/system/Plugin.tjsを参照すること。

さて、今まで定義してきたクラスの中で、セーブしなければならないものは なんだろう。少し考えてみて欲しい。「セーブしなければならないもの」は すなわち「ロード時に復旧するために必要なもの」だ。

…考えた?マヂで?ホントに考えてなければこの下を読まないよーに。

正解は、メンバ変数 fstring と bstring 、それと、flayer/blayer の visible メンバだ。そのほかのレイヤデータ(メンバ)は固定的で誰も 変更しないから、このプラグインが初期化した状態で十分復旧できている。 復旧時はセーブしておいた fstring と bstring を元に文字列を表示し、 visible状態を設定するだけでセーブした状態に復旧できる。 逆に言えば、レイヤデータ(メンバ)をユーザが任意に指定できるようにしているなら、 それらのデータも自前でセーブし、ロード時に復旧しなければならない。 例えば表示座標をユーザがいじれるならそれを、文字色をユーザが変更できるなら それを、のように。今回はそれらが全部固定なのであまり気にしなくてよいだけ。

で、セーブするデータは、普通は f.プラグイン名.変数名 にセーブする。 こうしておけば、他のプラグインと(名前が違えば)バッティングすることが ないから。ただ、一つのプラグイン(のクラス)で複数のインスタンスを 定義するなら、もう少し捻る必要があるだろう。ここでは単純に、 f.StaticText.{fstring|bstring|fvisible|bvisible} に、それぞれの変数を セーブすることにした。

(続き)
	// セーブ時(栞に保存するとき)に呼ばれる
	// f = 保存先の栞データ ( Dictionary クラスのオブジェクト )
	// elm = tempsave 時のオプション(通常はvoid)
	function onStore(f, elm)
	{
		// セーブデータを初期化
		f.StaticText = %[];
		// fstring/bstring をセーブデータ上に書き込む
		f.StaticText.fstring = fstring;
		f.StaticText.bstring = bstring;
		// レイヤの表示状態(visible)をセーブデータ上に書き込む
		f.StaticText.fvisible = flayer.visible;
		f.StaticText.bvisible = blayer.visible;
	}

	// ロード時(栞から読み出すとき)に呼ばれる
	// f = 読み込む栞データ ( Dictionary クラスのオブジェクト )
	// clear = メッセージレイヤをクリアするか (tempload 時のみ false)
	// elm = tempload 時のオプション (通常はvoid, tempload 時は dic)
	function onRestore(f, clear, elm)
	{
		if (f.StaticText === void)
			return;	 // 一応エラーチェックしておく
		// fstring/bstring を復活する
		dispString(f.StaticText.fstring, 'fore');
		dispString(f.StaticText.bstring, 'back');
		// visible を復活する
		flayer.visible = f.StaticText.fvisible;
		blayer.visible = f.StaticText.bvisible;
	}
}; 
// ここまででStaticTextクラス定義完了
(続く)

なお、onSaveSystemVariables()は普通は使わない。あんまKAGプラグインで sf(=kag.scflags)にデータをセーブすることがないからだ。もしsfにデータを 書き込む必要があるのならonSaveSystemVariables()を使う。 上のonStore()/onRestore()の中でsf.*に値を書き込んではいけないことに注意。 その場合、f.*の値とsf.*の値がセーブされるタイミングは異なるため、 sf.* の内容が同期的にセーブデータ中に書き込まれるかどうかの 保証がないからだ。

●インスタンスを定義し、KAGプラグインとしてkagに登録する

本当は、インスタンスの定義・KAGプラグインとしてkagへ登録、という作業は、 プラグイン定義ファイル中でやるべきではない。上で定義したクラスを元に 複数のインスタンスをユーザが定義して使う可能性もあるからだ。ただ、実際には 多くの場合、定義したクラスから複数インスタンスを作ることはない。ので、 ユーザの利便性を考えて、プラグインファイル中でインスタンスを一つ作り、 kagへ登録してしまうのが一般的だ。

登録方法は上で述べたとおり。インスタンスはクラス定義とは異なるので、 ここでは global.StaticText_instance としてインスタンスを定義した。

(続き)
// インスタンスを定義して、
global.StaticText_instance = new StaticText(kag);
// それをプラグインとしてkagに登録
kag.addPlugin(global.StaticText_instance);

// ここでTJSスクリプト完了
[endscript]
(続く)

●KAGから使いやすいマクロを用意する

せっかくここまで作ったら、TJSからだけ使えるんだぜー、ではなくて、KAGからも 使用できるようにマクロを作っておくのが親切だ。今回の場合、文字列が変更 できるだけなので、[set_statictext page=fore|back|both string=文字列] という マクロを定義しておこう。上で作ったインスタンスのdispString()を呼び出すだけだ。

(続き)
[macro name=set_statictext]
[eval exp="global.StaticText_instance.dispString(mp.string, mp.page)"]
[endmacro]


[return]

で、最後に [return] してこのプラグインファイルは完了する。

せっかく作ったので、コピペなりでもいいので是非コピーして、動作を確認してみて 欲しい。そして、少しずつ変更したりして、一体どうなるのかを確認してみて欲しい。

■おわりに

このように、KAGプラグインを作るのは、思いの他面倒だ。色々考えなきゃいけない ことも多いし、使い方にもちょっとしたコツが必要だったりもする。 しかし、TJSベースでKAGをうまいこと拡張できるという意味では、これほど 便利なものも他にない。「これ、プラグイン化したらみんな喜ぶんじゃないか?」と 思ったら、是非プラグイン化して、みんなが喜ぶようにしていただければ みんなが喜ぶ自分もハッピー、正にワールドビジネスサテライト!なので、 是非諸兄にあってはKAGプラグインをもりもり作って頂きたいのココロ。

…もちろん、ちゃんとメンテしなきゃダメなんだけども。