本文書は、吉里吉里/KAGを拡張するいくつかの方法のうち、 KAGプラグインの作り方について述べる。
KAGプラグインは、(KAGって名前が付いてるけど)実際にはTJSスクリプトだ。 data/system/Plugin.tjs の中に定義されている KAGPluginクラスを継承して、 自前でクラスを書き、それを使うKAGマクロを用意して、それらをKAGに登録する ことで、KAGを拡張することができる。
…なーんて書いても全然ピンと来ないだろうことはわかる。モノスゴよくわかる。 なんせ我輩もそうだったから。というか、解説文が少なすぎるんだよね。本家ですら ここの末尾にわずかに一行書いてあるだけだし。
まぁ…言えばTJSに挑戦!の 「第4章 KAG プラグイン」が一番詳しいと思う。詳しく知りたければ、そっち 見た方がいい。ただ、会話形式で縦長なのでちょい読みにくいというのがあって…… ええい、自己満足のために自分でも書いてみたかったんじゃーい!(本音)
吉里吉里/KAGを使い込んでいくと、TJSを使って自分用の何かを作りこむように なってくる。その時、例えば、「ユーザがセーブロードする時に、同時に この変数の値を保存・回復したいなー」とか、「画面をトランジションする時に 一緒に自作レイヤをXXしたいなー」とか、そういった『システムの動作にあわせて なにかしたい』欲求が出てくるだろう。これを可能にするのがKAGプラグインである。
逆に、こういう『システムの動作にあわせて何かしたい』のでなければ、あえて KAGプラグインを書く必要はない。なんか「もにょっ」としたTJSスクリプトを 「もにょっ」と追加しても十分な場合も多々ある。拙作ならCtrlSkipプラグインとか 実はそうなんだよね。
もう少し具体的に書くと、KAGプラグインは、KAGが動く時に、その動作に合わせて 呼ばれる自作メソッドを含むKAGPluginクラスの派生クラス(のインスタンス)だ。 以下のように定義する。
class 自作クラス extends KAGPlugin { (何か自作クラスで必要な処理) }; global.自作クラスインスタンス = new 自作クラス(); // インスタンスを作成 kag.addPlugin(global.自作クラスインスタンス); // KAGに登録 |
「自作クラスインスタンス」は、どこに定義してもいいのだが、まぁ普通は global. 以下に定義する。注意すべきは、global. 以下の値は、(明示的に 書き換えない限り)データをロードしようが何しようが吉里吉里を再起動するまで 変わらないこと。
で、実際KAGPluginクラス中で以下の7つのメソッドが定義されていて、それぞれ 以下のような機能を持っている。これに従い、KAGPluginを派生させた 自作クラスでそれらのメソッドをオーバーライドして定義すれば、それぞれの 動作を自作クラス上から制御可能になる。必要ないものは単純にオーバーライド しなければよい。そうすればそのメソッドについては何もしない。
実際、KAGPlugin関係がKAGシステムにどう実装されているかということを、例でもって 説明してみる。 例えば、[backlay]時に呼ばれるメソッドonCopyLayer()がどう呼ばれるかを例に示すと こんなカンジ。
ずばり、主にセーブ・ロード。
吉里吉里を拡張する上で最大の難点は、セーブ・ロードの処理を自前で用意しなければ
ならないことだ。他のシステムではこんなのシステム側でテキトーにやってくれる
ものなのだが、吉里吉里はこれをTJS経由で自前で処理する必要がある。
実に面倒くさい。
とはいえ、だからこそKAGPluginには必要性があるわけだ。単純にKAGシステムに
機能追加しただけだとセーブ・ロードでその機能が上手く実行されなくなるから、
ここでonStore()やonRestore()などの前述のメソッドが有用だということが分かる…
だろうか?
保存する値を全部 f.* や sf.* などのグローバル変数に直接書いとけばいいやん、 という話もあるにはある。 もちろん、それでなんとかなるならそういう方法で逃げちゃうのもアリだ。ただまぁ、 あんまグローバル変数使うっちうのもねぇ…ということがあったりなかったり、また 変数への書き込みタイミングが必要な場合(全部が同時に書き込まれないといけない とか)などにも対応するためには、やっぱり自前でクラス書くことが綺麗なことの方が 多い。と思う。んじゃないかな。まチョト覚悟はしておけ。
というわけで、ここからは実際に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クラスのコンストラクタでは、以下を実施する。
正直、デストラクタを綺麗に定義する意味はあまりない。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へ登録してしまうのが一般的だ。
登録方法は上で述べたとおり。インスタンスはクラス定義とは異なるので、 ここでは global.StaticText_instance としてインスタンスを定義した。
(続き) // インスタンスを定義して、 global.StaticText_instance = new StaticText(kag); // それをプラグインとしてkagに登録 kag.addPlugin(global.StaticText_instance); // ここでTJSスクリプト完了 [endscript] (続く) |
せっかくここまで作ったら、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プラグインをもりもり作って頂きたいのココロ。
…もちろん、ちゃんとメンテしなきゃダメなんだけども。