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

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

■はじめに

本文書は、吉里吉里/KAGを拡張する方法の一つ、TJSプラグインを 紹介する。

吉里吉里/KAGを構成するTJSは、実はちょっとびっくりするような方法で コ汚い(けれども便利な)拡張が可能である。その泥臭さがとっても我輩好み なので、ここで紹介する所存。

なお、本書は、TJSがある程度組める人を対象にしているのであしからず。 ちうかTJSってTiny Java Scriptの略じゃないのん?見かけそうだけど。

■TJSはどのように記録されているか?

イキナリ本書のキモ。吉里吉里のTJSが、実際どのようにメモリ上に 記録されているかの概要を知っておこう。とはいえ、実はモノスゴ簡単。

以上!いやマヂで。これさえ覚えておけば、本書の九割は理解したといっても過 言ではないくらい非常に大事な概念だ。クラスも、インスタンスも、メンバ関数も、 プロパティも、辞書配列も、変数も、全部が全部、global以下に登録されている。 ウソだと思うなら、KAGの何か立ち上げてコンソールで確認してみればいい。 例えば以下のように。 殆どの場合、頭の global. を省略してもいいので別々に見えるが、 実は全部 global. という辞書配列に繋がっているのであった。 ただし、クラス・インスタンス・関数などは既に中間言語(?)に変換されているので、 変数を参照するだけでは人間がその内容を知ることはできないことに注意。

■クラスやらインスタンスやらを書き換えてみる

さて、ここからが本題。 あるクラスにメンバ関数を一つだけ追加したい場合、普通は以下のように、 元クラスを派生させた新しいクラスをつくり、メンバ関数を追加するだろう。 この例では Layer クラスに新しいメンバ関数 garbagePrint() を追加する クラス MyLayer を定義している。内容についてはまぁ…聞くな!

class MyLayer extends Layer {
	function garbagePrint()
	{
		dm('ゴミ!');
	}
}

しかし、だ。上記の二つの特徴、 「全てglobal.以下にある」「それらは上書き可能」を鑑みると、 「だったら直接書き換えちゃえばいいんじゃない?」ということを容易に思いつく。 こんな風に。

global.Layer.garbagePrint = function() {
	dm('ゴミ!');
};

クラスとインスタンスは別ツリーに定義されており、クラス定義を変更しても、 クラス変更前までに定義されたそのクラスのインスタンスは変更されないことに 注意。 従って、上の garbagePrint() メンバ関数を、全ての Layer クラスのインスタンス (例えば kag.fore.layers[0])にも追加したければ、上の定義は override.tjs (KAGがインスタンス定義を始める前に実行されるスクリプト)の中に 記述する必要がある。

同様に、インスタンスも書き換え可能だ。kagの背景レイヤに loadImages() すると 常に白い画像が表示されるようなイタズラを実装するには、以下のように定義する。

kagの背景レイヤには、常に白い画像"white.jpg"を読み込む
global.kag.fore.base.loadImages = function () {
	global.KAGLayer.loadImages("white.jpg");
} incontextof global.kag.fore.base;

global.kag.back.base.loadImages = function () {
	global.KAGLayer.loadImages("white.jpg");
} incontextof global.kag.back.base;

incontextof でそのインスタンス中のものであると明示している。 この例では明示しなくてもよいが、まぁ念のため。

これにより、kag.fore.base と kag.back.base だけ、 どんな画像を読んでも真っ白になる。 override.tjs中でLayerクラスを書き換えたときと違って、 kag.fore.layers[1]などは無関係であることに注意。

■で、それの何が嬉しいのん?

ここまでゆってまだわからんか!キィッ!

普通、「綺麗な」オブジェクト指向言語では、こういう拡張はLayerクラスの 定義部分を書き換えることによって実施する。あるいは、上の前者の例のように、 Layerクラスを継承した新しいクラスでメンバ関数を追加するだろう。 しかし、吉里吉里では Layerクラスの定義はTJSで書かれていないので書き変えられない。 また、Layerクラスを継承してMyLayerクラスを作ったとして、 それを全てのインスタンスが使用するように KAG(=data/system/*)全体を書き換えるのはかなりキツい。 こういう使い勝手の悪さは、オブジェクト指向型言語の最大の弱点だと思う。

そこで上の後者の(global.以下を直接書き換える)方法だ。メリットは二点。

簡単に言えば「パッチをパッチのまま追加できる」。このメリットは頗る大きい。

もう少し吉里吉里/KAGでの一般的な話をしよう。
KAGシステムは、(主にdata/system/以下の)TJSスクリプトの集合体だ。だもんで、 このTJSスクリプトを直接書き換えてしまえば、自分が欲しい機能を 好き勝手にひょいひょい追加実装することは可能である。 それで必要十分な場合が多いのも認めよう。

しかし、だ。時が経つと、吉里吉里/KAGの新しいバージョンがリリースされました、 今まで使ってた「独自機能」「独自パッチ」はどうしよう?ということが必ず起こる。 よっぽどのスーパーエンジニアか、作った機能それぞれに 詳細なドキュメントを残していない限り、それを新しいバージョンに移植する 作業には、新規作成と同等以上の労力が必要になる。

一方、上のような形を採っていれば、吉里吉里/KAG本体が更新されても、 そこに手を加える必要は全くない。追加で変更した部分だけを新しい吉里吉里/KAGに あわせて更新すれば、 最低限の労力で新しい吉里吉里/KAGに対応可能だ。Wowなんて便利!

まだわかんない人は、 100以上の自作機能をKAGシステムに間違いなく追加しなおす作業を 思い浮かべてみればいい。 簡単だと思うか?もし思うなら、アナタは何も知らないアリスチャンか 本当のスーパーエンジニヤなので、この後の文書読む必要まるでなし。 しかし、我輩のような凡人以下の人は、可能な限り労力を削減しないと なんかあるたびに発狂してしまうのであるよ。

だから、我輩は、独自機能追加のためにも、吉里吉里/KAG本体は極力 書き換えないことを強くお勧めしている(書き換えないとダメなこともあるにはある)。 幸いにして、TJSでは、オブジェクト指向言語にあるまじき)びっくりするような方法で 既存の機能を上書き・更新、新機能を追加することが可能だ。 これを上手に使って、最低限の労力で、最大限の効果を得られるように尽力すべし。

我輩は超エンタープライズ分野で働いてるので、こういう「元をつつかなくても なんとかパッチを当てることができるコ汚い仕組み」が好き好き大好き。 英語で言えばジュッテーンム。アザブジュバーン。ムセンボージュ。

……というところが嬉しいのだよ!この嬉しさが判るか!?キィッ!

■以降、これを(勝手に)TJSプラグインと呼称する!

TJSプラグインという言葉は造語だ。ググル先生もご存知ないくらいの造語。 「TJSアドオン」や「TJSパラサイト」とかと迷ったけど、 KAGプラグインと吉里吉里プラグインがあるんだから、TJSプラグインでいいでしょ、 と安直に着地。なんか実は正式名称があるのぜー、 というのをご存知だったら、誰か根拠と共に教えて下さい是非。

■例題:boxBlurをかけたレイヤをセーブ・ロードで復旧せよ

吉里吉里のLayerクラスには doBoxBlur() というメソッドが存在する。これを使うと、 レイヤに表示している画像にブラーをかけることができる。効果としては かなり有用なのに、実際に使用されている例が少ないのは、そうして作った 画像が、シナリオセーブして再びロードした時に復旧できないからだ。 何故って、今のKAGにはdoBoxBlur()をロード後に復旧する機能がないから。

無ければ作れ、って死んだばっちゃがゆってた。ので作ってみましょうよ。 以下のような方針で臨む。

というわけで、これを実現する小さなTJSスクリプトが以下の通り。 前述の通り、これはAnimationLayerクラスを書き換えるため、Override.tjsの中などで 定義しておく必要があることに注意。

AnimationLayerクラスを拡張し、doBoxBlur()のかかった画像のセーブ・ロードに対応する
if (typeof(.AnimationLayer.doBoxBlur_boxblur_org) == 'undefined') {
	// doBoxBlur()をフック
	.AnimationLayer.doBoxBlur_boxblur_org = .AnimationLayer.doBoxBlur;
	.AnimationLayer.doBoxBlur = function (xblur=1, yblur=1) {
		doBoxBlur_boxblur_org(...);
		if (typeof(Anim_loadParams) == 'undefined' ||
		    Anim_loadParams === void)
			Anim_loadParams = %[];
		Anim_loadParams.boxblur = %[xblur:xblur, yblur:yblur];
	};
	// restore 処理を変更。
	.AnimationLayer.restore_boxblur_org = .AnimationLayer.restore;
	.AnimationLayer.restore = function (dic) {
		restore_boxblur_org(...);
		if (dic.loadParams !== void &&
		    dic.loadParams.boxblur !== void) {
			var arg = dic.loadParams.boxblur;
			doBoxBlur(arg.xblur, arg.yblur);
		}
	};
}

上の説明見ながら、 一体どういうことをやってるのかバッチリ理解してくれたまえよキミィ! たったこれだけで、KAG上からkag.fore.layers[0].doBoxBlur(5,5)とかやって 放置しておいても、ロードする時はブラーかかった画像がフツーに復活する奇跡。

※ただし、上の例は万能ではない。実際にはAnimationLayerには この他にも画像を変更する機能があったりして、こういうのと組み合わせて使う場合、 どっちをどういう順番に実行するか、が最終的な画像に影響したりする。 例としては、画像をブラーした後に[pimage]で画像を貼り付けた場合など、[pimage] 部分は鮮明になってるはずだが、上の例ではその状態でセーブ→ロードすると [pimage]部分にもブラーがかかる。 こういうのに完全に対応するには、画像に加えられた処理の順番を記憶し、 ロード時にはそれをなぞる必要がある。…まぁ、現実問題としては 複数のそういう処理を重複してかけることは少ないので、 そうそう困るもんじゃないけれども。

■注意:invalidateの順番

たとえばゲーム終了時、定義してたインスタンスは順次invalidateされるが、 吉里吉里は依存関係を考えてinvalidateしない。円環参照してたら両方とも invalidateできない、ということを防ぐためなんだろうけど、TJSプラグインを 書く上で時々困ることがあるのでご紹介。

それは、たとえばKAGを書き換えるようなTJSプラグインを作った時。せっかく 「プラグイン」なんだから、元に戻せるように作ろう、とすると、以下のような 実装になるだろう。これは、KAGの[ch]タグを書き換えて処理を追加する時に 書くようなTJSプラグインのサンプル。

class ABC {
	var w;
	var ch_org;
	function ABC(window)
	{
		w = window;
		ch_org = w.tagHandlers.ch;
		ch = ch_custom;
	}

	function finalize()
	{
		w.tagHandlers.ch = ch_org;
	}

	function ch_cus(elm)
	{
		ch_org(...);
		(何か処理)
	}
}

global.ABC_obj = new ABC(kag);

普通はコレで困らない。でも、厳密にはコレだとダメなのだ。何がダメって、 吉里吉里の起動オプション -debug を付けた場合に、 吉里吉里を終了する時、 finalize()で、以下のように警告されてしまうところ。

警告: ABC.tjs(12)[(function) finalize]: 削除中のオブジェクト 0x031275B8[instance of class ABC] 上でコードが実行されています。
このオブジェクトの作成時の呼び出し履歴は以下の通りです:
                     ABC.tjs(21)[global.ABC_obj = new ABC(kag)]

だもんで、こういう「元に戻す可能性があるもの」については、仕方ないから KAGPluginにしちゃう方がいい。そうすれば、kagのfinalize()のときに登録されてる KAGPluginが順次invalidateされるから、このエラーは出なくなる。
※もちろん、男らしく元に戻さないというのであればこんなのどうでもいいんだけど。

class ABC extends KAGPlugin {
	var w;
	var ch_org;
	function ABC(window)
	{
		w = window;
		ch_org = w.tagHandlers.ch;
		ch = ch_custom;
	}

	function finalize()
	{
		w.tagHandlers.ch = ch_org;
	}

	function ch_cus(elm)
	{
		ch_org(...);
		(何か処理)
	}
}

.kag.addPlugin(new ABC(kag));

余談だけど、KAGWindowのfinalize()で登録されてるKAGPluginをinvalidateしてる ところ、正順にinvalidateしてるけど逆順の方がよくないか…?だってaddPlugin()で 登録する時が正順なんだからさ。上のような「書き換えて終わるだけ」なら特に問題は 起こんないけど。

■おわりに

なんてコ汚いと思ったろうか。その通りだよ!

我輩はこういう「パッチ当て」の方が、最終的にはユーザに優しいものが できると思う。 クラス型言語にはこういう「パッチあて」の方法が無いために、 システム変更に伴う影響範囲が極端に大きくなる。 実際、我輩それで何度も酷い目に遭った。 Javaとかなんとか、言語屋は多分「一から作った時と 既に存在するライブラリを利用する時の開発効率」だけを考え、 それらをちまちま修正しながら巨大なシステムを維持管理するという運用実務を 考慮してないから、そういう実務に即さないものを作ってしまうんだろうな。 運用屋からすると、その意味で Java はダメのダメダメで、TJSは大変便利だ。 運用上は、美しい実装が必ずしも有効ではない、ということ。

ただし、この方法は、一歩間違えばあっという間にスパゲッティプログラムを 量産してしまう。メンテ性が低下し、バグ発生率も飛躍的に向上する。 万能の方法ではないことを、ゆめゆめ忘るることなかれ。