本文書は、吉里吉里/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プラグインをもりもり作って頂きたいのココロ。
…もちろん、ちゃんとメンテしなきゃダメなんだけども。