KAICHO Mail:s_naray[at]yahoo[dot]co[dot]jp ※SPAM防止中

吉里吉里/KAG小技集

本文書では、吉里吉里/KAGの小技をつらつらと書き連ねる所存ナリ。 他人のためというか、自分の備忘録のために。こんな場合にどうなる?みたいな 質問があれば、上のメールアドレスに投げて頂けると、調べて追加しちゃうかも しれない(期待薄)。


日本語が使える

案外ご存じない方が多いようなので書いてみる。吉里吉里/KAGでは、 ラベル、マクロ名、変数名などに、普通に日本語が使える。こんなカンジ。

変数名に日本語を使うマクロ名と引数名に日本語を使う
[eval exp="f.ゲームバージョン = 1.01"]
[macro name="画像表示"]
[image storage=%画像 layer=%layer|base page=back top=%top left=%left]
[trans method=%method|crossfade rule=%rule time=%time|1000]
[wt canskip=true]
[endmacro]

[画像表示 画像=学校]

この機能を知らないのか使いたくないのか、 マクロ名にやたらめったら記号的なアルファベットを使っている例が多い。 我輩は、第三者が後から見て理解が難しいスクリプトは書くべきではないと思う。 ソフトウェア工学の基礎基本だからだ。ゲームは違う、という人が居るかも しれないが、それでも、「ちゃんと」作ることは心がけるべきだと思う。 というワケで我輩は日本語推奨派。英単語でも、ちゃんと洗練されたのを 書ければそれでいいんだけども。
例えば [akdspcntr] って書かれてて、ぱっと見てこれが何を意味するか分かる人が なんか居ないと思う。正解は akira_display_center、すなわち、 「アキラを中央に表示」…らしい。[アキラ 中央] とかの方が100倍マシだと思う。


マクロ内ローカル変数

あるマクロの中では、ローカル変数として mp.xxx が使用できる。mp.xxx は基本的に マクロへの引数を参照するために使うが、実際には辞書配列としてマクロに 与えられたものであり、マクロ内で新規に作成することができる。また、 作成した mp.xxx は、マクロ引数のように KAG 上から %xxx で参照することができる。

; トランジションでキャラを消去(裏画面に透明な画像を読み込む)
[macro name="キャラ消去"]
[eval exp="mp.w = kag.fore.layers[mp.layer].width"]
[eval exp="mp.h = kag.fore.layers[mp.layer].height"]
[eval exp="mp.t = kag.fore.layers[mp.layer].top"]
[eval exp="mp.l = kag.fore.layers[mp.layer].left"]
; mp.storage = 透明画像ファイル名 = "blank###x###"
[eval exp="mp.storage = 'blank' + mp.w + 'x' + mp.h"]
; 作成した mp.storage を %storage で参照
[image storage=%storage layer=%layer page=back top=%t left=%l]
[trans method=%method|crossfade rule=%rule time=%time|300]
[wt canskip=true]
[endmacro]

[キャラ消去 layer=1]

マクロ内で一時的に変数を使う場合、tf.xxx よりも mp.xxx を使った方がいい。 tf.xxx はゲーム中で共通であり、たとえば tf.tmp なんてありがちなのを使って しまうと、入れ子になったほかのマクロ中でこの tf.xxx を使うことが できなくなるからからだ。 変わりにマクロ中で mp.tmp を使えば、これはほかのマクロでも独立して使用 可能である。

なお、アニメーション定義ファイル .asd の中のマクロでは、このような用途に mpは使えない。これは、mpがInitialize.tjsの中で以下のように定義されているため。

property mp
{
	getter { return kag.conductor.macroParams; }
}

ずーっと解決策は無かったのであるが、吉里吉里rev4942以降でこのあたりが 大きく修正(いろんなものがグローバルじゃなくて該当オブジェクトのコンテキストで 動作するようになったとのこと)され、.asdの中のマクロでもmpが使えるようになった そうな。スッゲー助かるけど、それって互換性に問題ありそうで怖い。


マクロ引数のtrue or false

吉里吉里 2.28 くらいからだったか、マクロの引数に値を指定しなかった場合は true になるようになった。これを使うと、マクロの引数そのものをパラメータの ように使え、マクロの可読性を上げたり 引数のタイプ量を減らしたりすることができる。

[macro name="メッセージ枠"]
[if exp="mp.表示"]
	; メッセージ枠を表示
	[backlay]
	[position layer=message0 page=back visible=true frame="メッセージ枠" left=24 top=427 width=580 height=171 marginl=59 margint=12 marginr=42 marginb=16]
	[trans method=crossfade time=200]
	[wt canskip=true]
	[current layer=message0 page=fore]
[elsif exp="mp.消去"]
	; メッセージ枠を消去
	[backlay]
	[position layer=message0 page=back frame="" color=0 opacity=0]
	[trans method=crossfade time=200]
	[wt canskip=true]
[endif]
[endmacro]


[メッセージ枠 表示]

[メッセージ枠 消去]


マクロ中でループしたい

残念ながら、KAGにループのための命令は無い。でも、例えば表示されている キャラクタの数だけループしてキャラクタを消去したい、なんてことは 時々起こる。そんな時は、サブルーチンを上手に使う。ただし、 サブルーチンはシナリオから直接実行されないように工夫しなければ ならない。具体的には、
  1. サブルーチンは、あるファイルに独立して存在するか、
  2. あるファイルの末尾に存在しており、定義前に [return] がある
などの工夫が少し必要となる。

; このファイル macro.ks はマクロしか定義されていないと思いねぇ。

[macro name="全キャラ消去"]
; 必ずストレージ名↓を指定すること
[call storage=macro.ks target=*全キャラ消去_sub]
[endmacro]

[return]
; この return のおかげで、[call storage="macro.ks"] されてもこれ以降は
; 実行されない

; ここからループのためのサブルーチン定義
*全キャラ消去_sub
[eval exp="mp.layerary = [ 1, 2, 3 ]"]
*全キャラ消去_loop
[return cond="mp.layerary.count <= 0"]
[eval exp="mp.layer = mp.layerary.pop()"]
[キャラ消去 layer=%layer]
[jump target=*全キャラ消去_loop]

実は、マクロから呼んだサブルーチン中では、mp.xxx がそのまま使用でき、 サブルーチン内ローカル変数としても用いることが可能である。上の例では mp.layerary や mp.layer をそのように使用している。 これも小技といえば小技。

で、KAGにループの命令がないのはアレだなぁ、と思って作ってみたのがこちら、 ExtKAGParser(声:大山のぶ代)。 コレ使えばマクロ中だろうがなんだろうがサブルーチン使わずにループできるように なるから、我輩は今後はこっちを使うということで。


指定されなかったマクロ引数のマクロ内での扱い

こんなの書いてどうなるというワケでもないけれど一応。マクロの引数として 指定されなかったものを、マクロ中で %xxx とかで参照した場合、その引数は void として扱われる。平たく言うと、存在しないものとして扱われる。例えば こんな(上の方にも似たようなのあるけどな)。

[macro name=transition]
[trans method=%method|crossfade rule=%rule time=%time|1000]
[endmacro]

; ↓universal なのでルール画像を指定する
[transition method=universal rule=abc]
; ↓crossfade だとルール画像は指定しない
[transition time=200]

crossfade の時はトランジションルール rule を指定する必要が無いが、 その場合、[transition] 中で rule=%rule と書いていても、[image] には rule が指定されなかったものとして認識される。 分かりづらいですかそうですね。自分でマクロを書くようになれば分かるさ!


&の後はTJS式が書ける

マクロの引数なんかで & を使うことがあるが、この後にはTJS式を書くことができる。 ということは、かなり複雑なことでもムリヤリここに押し込めてしまえば 何とでもなるということだ。

; キャラを表示キャラ数に合わせて適当な位置に移動する
[キャラ移動 left=&"kag.scWidth/(f.表示キャラ数+1)*f.表示index - kag.fore.layers[f.layer].width/2"]

…まぁコレはムリヤリすぎる例ではあるけれど。


全指定したマクロの引数を上書きする

マクロの引数を次のマクロに渡すとき、* を指定する。この時、全部をそのまま 渡すのではなくて、一部は固定値で指定したいことがある。そういうときは、 * の「後」に改めて指定してやればよい。

[macro name=テスト]
[別マクロ * top=&void attr=左]
[endmacro]

[テスト layer=1 top=10 left=20 attr=中央]
; これだと [別マクロ]には「layer=1 left=20 attr=左」が渡される

ちなみに、* の「前」に指定すると、指定されなかったことになるらしい。 なんでやのん。 本当に全てのVerのKAGで使えるのかどうかは考えたことが無いので、 モノスゴ裏技なのかもしんない。今まで使ってきて不都合が出たことは無いけれど。


一時変数 tf.xxx の保存範囲

一時変数 tf.xxx がどこまで有効かをご存知な方が案外少ないので書いておく。 正解は、「ゲームが起動されて終了するまで、変更されるまで有効」。すなわち、 一度作成した後はずーっとゲーム中で使える。しかし、 「セーブしても保存されず、ロードされても以前のものは復活しないし 現在のものは消えない」ことに注意。 これはつまり、tf.xxx を作って参照するまでの間に、 セーブポイントは存在してはならないことを意味する。これを忘れると、 「ゲーム再開したら、参照できない変数がある、と時々怒られる」ゲームが 完成してしまう。そういう意味では、よっぽどの理由が無い限り tf.xxx は 使うべきではないと思う。


効果音が鳴っているかどうかを調べる

効果音は kag.se[バッファ] で管理されている。この status メンバが、 そのバッファの使用状況を表している。"play" だと演奏中、"stop" だと 演奏停止中(ほかにどんなステータスがあるかは調べてないので知らない)。 従って、演奏中かどうかを調査するなら、以下のようなTJS式が使用できる。

[if exp="kag.se[string(mp.bufnum)].status == 'play'"]
	; 演奏中だった
[endif]

ちなみに、このバッファ添字も、数値っぽく指定するが文字列じゃないと ダメらしいので、一応 string(num) とかで明示的に文字列に変換した方が、 後からハマることがなくなると思う。こういう細かい努力って大事。


ファイルが存在するかどうか調べる

ストレージ(KAGから利用できるファイル群)中に、指定のファイルが存在するか どうかは、Storage.iesExistentStorage()で確認できる。例えば、 .ogg/.mp3/.wavのサウンドファイルが存在するかどうかをチェックして、 存在しなければその旨をログに記録するけどエラーにはしない、という [se_play]マクロは以下のように書く。

[iscript]
function isExistSoundStorage(storage)
{
	if (Storages.isExistentStorage(storage) ||
	    Storages.isExistentStorage(storage + '.ogg') ||
	    Storages.isExistentStorage(storage + '.mp3') ||
	    Storages.isExistentStorage(storage + '.wav'))
		return 1;
	return 0;
}
[endscript]

;●se_play ... seを再生する
; 	buf=			効果音バッファ
;	storage=		効果音ファイル(拡張子不要)
;	loop=true|false(def)    ループ再生するかどうか
; 				loop=trueなら無限ループ再生。[ws]せずに戻る
;	wait=true|false(def)    再生待ちをするかどうか
; 	volume= (def=100または登録済なら以前のまま)
;	gvolume= (def=100または登録済みなら以前のまま)
; 	pan= (def=以前のまま)
[macro name="se_play"]
[se_opt *]
[se_stop buf=%buf]
[if exp="isExistSoundStorage(mp.storage)"]
	[playse storage=%storage buf=%buf loop=%loop|false]
[else]
	[eval exp="dm('エラー!存在しない音声ファイル: ' + mp.storage)"]
[endif]
[ws buf=%buf canskip=true cond="mp.wait && !mp.loop"]
[endmacro]


ストレージディレクトリを追加する

KAGが参照するストレージディレクトリを追加するには、Storage.addAutoPath() を 使用する。しかし、 このメソッドを scenario/ ディレクトリ中で使ってはならない。正解は、 system/Initialize.tjs の真ん中より上あたりで、システムデフォルト値が 定義されている部分に追加することだ。

(snip)
function useArchiveIfExists(name)
{
	// name が存在していたらそのアーカイブを使う
	var arcname;
	if(Storages.isExistentStorage(arcname = System.exePath + name))
		Storages.addAutoPath(arcname + ">");
}

Storages.addAutoPath(System.exePath + "video/"); // exePath 以下の video/
Storages.addAutoPath("video/"); // video フォルダ
Storages.addAutoPath("others/"); // その他
Storages.addAutoPath("rule/"); // ルール画像フォルダ
Storages.addAutoPath("sound/"); // 効果音フォルダ
Storages.addAutoPath("bgm/"); // BGM フォルダ
Storages.addAutoPath("fgimage/"); // 前景画像フォルダ
Storages.addAutoPath("bgimage/"); // 背景画像フォルダ
Storages.addAutoPath("scenario/"); // シナリオフォルダ
Storages.addAutoPath("image/"); // そのほかの画像フォルダ
Storages.addAutoPath("system/"); // システムフォルダ
←ここに追加

// パッチアーカイブの検索と使用
// もしこれらの名前を持ったアーカイブが実行可能ファイルと
// 同じ場所にあった場合、それを優先して使う
useArchiveIfExists("video.xp3");
useArchiveIfExists("others.xp3");
useArchiveIfExists("rule.xp3");
useArchiveIfExists("sound.xp3");
useArchiveIfExists("bgm.xp3");
useArchiveIfExists("fgimage.xp3");
useArchiveIfExists("bgimage.xp3");
useArchiveIfExists("scenario.xp3");
useArchiveIfExists("image.xp3");
useArchiveIfExists("system.xp3");

useArchiveIfExists("patch.xp3");
(snip)

なぜか。それは、パッチを当てる時に問題になるためだ。
吉里吉里/KAGでは、ストレージの検索優先順位が、後から指定する方が強くなる。 上の例でも分かるように、デフォルトディレクトリ(例えば scenario/)よりも patch.xp3 の方が後に指定されているため、patch.xp3 に含まれるファイルで scenario/ の中のファイルを上書きし、結果的にパッチを当てることができる。
例えば first.ks 内で

[eval exp="Storages.addAutoPath('eventcg/')"]
と指定した場合、first.ks は Initialized.ks よりも後に読み込まれるため、 patch.xp3 よりも eventcg/ ディレクトリの方が優先されて読み込まれることになる。 つまり、パッチを当てられなくなってしまうのだ。だから、Initialize.tjs 内を 変更すること。

ちなみに、もう既に first.ks 中で Storages.addAutoPath() を指定してしまって、 パッチが当てられない状態になってしまった場合は、一応回避策がある。 first.ks のその指定の直後に、もう一度 patch.xp3 を登録してやればいい。

パッチ前の first.ksパッチ後の first.ks
[eval exp="Storage.addAutoPath('eventcg/')"]
[eval exp="Storage.addAutoPath('eventcg/')"]
; patch.xp3 を再登録↓
[eval exp="Storage.removeAutoPath(System.exePath + 'patch.xp3>')"]
[eval exp="Storage.addAutoPath(System.exePath + 'patch.xp3>')"]

かなりのコ汚さであることは重々承知するが、これで一応 patch.xp3 の優先度が もっとも高くなるため、問題は回避できる。この後に patch2.xp3 とかを追加する 場合は、更にそれらを再登録しなければならないが、まぁ回避できなくもない、と いうことで。


レイヤに表示されている画像ファイル名を得る

画像レイヤに表示されている画像ファイル名は、アニメーションレイヤ クラスの Anim_loadParams.storage に格納されている。以下のように取得できる。

[eval exp="tf.filename =  kag.back.layers['1'].Anim_loadParams.storage"]

実は Layer クラスにはこういうのが無い、というのが世界の不思議。

ちなみに、テキストのフレームファイル名は、frameGraphic に格納されている。 messageレイヤ3番の表画面に読み込まれているフレームファイル名は、 kag.fore.messages[3].frameGraphic で参照できる。


KAGを終了する時にエラーが出る場合

KAGを終了する時に kag.close() を使った場合、「スクリプトで例外が発生しました」 「オブジェクトはすでに無効化されています」のようなエラーが発生することがある。 発生のメカニズムを説明すれば長くなるので割愛。簡単に言えば、kag.close() の 代わりに kag.closeByScript(%[ask:true]) を使えば回避できる。

変更前変更後
[current layer=message0 page=fore] 
[locate x=100 y=100] 
[button graphic=開始ボタン.png storage=シナリオ1.ks target="*ゲーム開始"] 
[locate x=100 y=150] 
[button graphic=設定ボタン.png storage=設定.ks target="*設定開始"] 
[locate x=100 y=200] 
[button graphic=終了ボタン.png exp="kag.close()"] 
[s]
[current layer=message0 page=fore] 
[locate x=100 y=100] 
[button graphic=開始ボタン.png storage=シナリオ1.ks target="*ゲーム開始"] 
[locate x=100 y=150] 
[button graphic=設定ボタン.png storage=設定.ks target="*設定開始"] 
[locate x=100 y=200] 
[button graphic=終了ボタン.png exp="kag.closeByScript(%[ask:true])"] 
[s]


「最初に戻る」で「タグ・マクロ XX は存在しません」エラー発生

タイトルのまま。これが出ると、よく対象マクロが定義されてるプラグインの バグじゃないかとか疑われたりするのだが、実はこれ、[startanchor] タグの 使い方の問題だったりする。よーくマニュアルを読んでみよう「このタグはセーブ可能なラベルの直後に書いてください」と書いてある。 さて、キミのスクリプトで、[startanchor]の前に ちゃんとセーブ可能なラベルがあるか確認してみて欲しい。 意外にラベルを書いてないことが多い。もともと[startanchor]そのものが 『最初に戻る』なんだから、その前にラベルが必要だなんて思わないだろうしね。 [startanchor]内でやたらと長いラベルを自前で埋め込めばよかったのにね。

修正前修正後
;ファイル先頭

[startanchor]
(以下略)
;ファイル先頭
*start|開始
[startanchor]
(以下略)


TJS:あるクラス中の任意のスーパークラスのメンバを参照するには

通常は、A→(派生)→B→(派生)→C のようにクラスが派生したとして、Cから Bのメンバ member を参照するには「super.member」のように指定する。しかし、では CからAのメンバ member を参照するにはどうする?実は「global.A.member」と書く。 つまり、「global.目的のクラス名.メンバ名」と書くのが正しい。なぜ、と言われて も困るので、こう書く、とだけ知っておいてもらえればよし。

たとえば、ButtonLayerクラスを派生させて自作レイヤAnimButtonLayerを作成 した場合、ButtonLayerのheightは実はpropertyで宣言されていて色々副次的な 効果を持っているため、それをスキップして直接heightに値を設定したいなぁ、 と思った時なんかには、AnimButtonLayerクラス中で以下のようにheightを オーバーライドすればよい。

property height
{
    setter(x) {
        global.KAGLayer.height = x; // ButtonLayerのheightを回避する
    }
    getter {
        return global.KAGLayer.height;
    }
}

これ知らないとハマる。


最初からデバッグコンソールを表示するには

吉里吉里でデバッグする時に system/config.tjs で「debugMenu.visible = true と 書くのはアタリマエだけれど、最初からデバッグコンソールが表示されてれば いいのに、と思うことがなくもない。そんな時は first.ks の先頭とかで 以下のように定義。

[eval exp="Debug.console.visible = true"]    デバッグコンソールを表示
[eval exp="Debug.controller.visible = true"]    デバッグコントローラを表示


ヒストリ(履歴)をクリアするには

なんでこんなことがKAGからできないのかよくわかんないけれど、実際できないので ここに方法を記す所存ナリ。

[eval exp="kag.historyLayer.clear()"]

たったこれだけなんだけれどもね。


実行速度を気にするな(誤解を招く書き方ではあるが)

「XXをOOするとき、##という書き方と**という書き方どっちが速いですか?」という 質問をよく受けるのだが、それにはもう「そんなの気にするのは無意味だから もっと別のことに力を注ぎなさい」と言い放つことにしている。例えば以下の ようなTJSのループ。確かに後者の方が高速ではあるが、じゃぁどのくらい違うのか というと、実測で10万回ループして0.2秒違うくらい。…で、キミのスクリプトは 10万回もループするものなのかね?(それは作り方間違ってるし)。

/* 遅い書き方? */
for(var i = 0; i < 100000; i++) {}

/* 速い書き方? */
for(var i = 100000-1; i >= 0; i--) {}

たとえ5年前のPC使ってたって、たかだかスクリプトごときの書き方一つで 劇的に速度が変わることは「ない」。 断言してもいい。こんなの気にせず、いいから手を動かして 目の前のスクリプトを書き上げたまえよ実際。

万一ある動作が遅くなった場合、その原因は書き方一つでどうこうというもの ではなく、殆どの場合、アナタが途方もない無駄を作りこんだからだ。 レイヤ100枚表示したまま別々にトランジションしてた、とか、 継承しまくったクラスの変数の書換えを、継承ごとに全部実施してた、とか、 グローバル変数(sとか)の保存をものすごい回数実行してた、とか。 こういう時でも、「書き方」を気にする前に、もっと論理的に、現実の問題を調査し、 解決した方がいいのだよ。「遅い」なら、その時に怪しげな箇所に System.getTickCount()使って ちゃんと時間計って、どこでどのくらい時間食ってるのか調べるのだよ。 そういう、自分で手を動かすことなしに「どっちが速い?」とか聞くのは、 教えてクン以外の何者でもない。もしどうしても知りたかったら自分で手を動かせ!


ゲームの起動がやたらめったら遅いことがある

長いゲームに多いのだが、暫く遊んでいると、そのうちゲームの起動にモノスゴ 時間がかかるようになってくる。ウインドウも出ず、延々となんかCPUがぐるぐる 回っているカンジ。30秒や一分はアタリマエ、ちょっと古い PC だと二分以上 かかったりすることがある。

色々調べてやっとわかった、原因は、KAGWindowのコンストラクタでシステム変数を 初期化するところ、もっと言えば loadSystemVariables() 関数のズバリここ。

var fn = saveDataLocation + "/" + dataName +
	"su.ksd";
if(Storages.isExistentStorage(fn))
{
	sflags = Scripts.evalStorage(fn);  ★これが遅い
	sflags = %[] if sflags === void;

これ、セーブデータ(例えば savedata/savesu.ksd)を、kag.sflags(ゲーム中ではsfで 参照可能)に読み込む処理なんだが、データ量が多いと、ここにモノスゴ時間がかかる。 データ量が多い時とは、典型的にはどこでもセーブプラグインみたいなものを 使っているとき。このとき、sf上には「trail_ファイル名_ラベル名 = 数値」の ようなデータが山ほどできる。長いゲームだと数万行に及ぶ。この読み込みが 遅いのだ。どのくらい時間がかかるかというと、手元の Core2Duo(1.83GHz)で、 74000行のsavedata/savesu.ksd(圧縮してあるので440KB、非圧縮で5.5MB)の 読み込みに、かっきり30秒かかる。 一秒あたり2467行しか読み込めてない。データは圧縮してあってもしてなくても、 暗号化してあってもしてなくても、読み込み時間は殆ど変わらない。

どこでもセーブプラグインを使っていなくても、結局データ量が増えれば 同じことが起こるので、画面ごとにセーブ可能なゲームでは、本編が長ければ 長いほど同じ状態になりうる。画面ごとにセーブしてなくても、sfが増えていけば 結局同じ。

回避策は無かったのであるが、「■吉里吉里/KAG/TJS雑談質問スレ■その23」 の963〜971の方々の検証により、既読ラベルのセーブデータを階層的にすれば 回避できることがわかったので、highSpeedLabeler (文書) (TJSソース) を作ってみた。確かにこれを使うと起動時間は30倍くらい高速になった。

根本原因は、要素数が多い(10万とか)辞書配列に新しいのを 追加するのに時間がかかっていること。 だから、本当は、この部分のコードをもう少し賢く すればいいんだけど…。それKAGPluginレベルでどうこうできる話じゃないし…。

新しい開発版吉里吉里や吉里吉里Zでは、'b'(バイナリモード)が指定可能。 これだとロードが異様に高速になる。これも検討してもいいかも。詳細は こちらで。


セーブがやたらめったら遅いことがある

なんでじゃーい!セーブしよう思うたら、セーブボタン押した後に10秒くらい止まる んじゃーい!ということがある。色々さがしたんだけど、結局これも saveBookMarkToFile()の中のsaveStruct()が遅いのである。とても遅いのである。 びっくりするくらい遅いのである。ゲーム開始直後でセーブデータなんて 殆どないのに、CoreDuo1.2GHzのマシンで6秒とか止まる。なんでやねーん!

原因は不明だけど、回避策はある。system/Config.tjsのsaveDataModeを"z"に すること。これで上環境では全く同じデータを一秒以内にセーブすることが できるようになる。しかし、テストの時は"z"付けてないからねぇ…。 どうしたものやら。

新しい開発版吉里吉里や吉里吉里Zでは、'b'(バイナリモード)が指定可能。 これだとセーブが倍くらい高速になる。これも検討してもいいかも。詳細は こちらで。


バイナリモードでのセーブ・ロードは本当に高速なのか?

今currentの吉里吉里2.32では使えないんだけど、開発版の最新吉里吉里や 吉里吉里Zでは、 system/Config.tjs の saveDataMode に 'b' を指定する(= システム変数辞書の Dictonary.saveStruct()の第二引数に 'b' を指定する)ことで、セーブデータを バイナリモードにすることができる。コレをすると巨大な辞書でもセーブ・ロードが 速いらしいので、じゃぁ一体どんだけ速いのよ、ということで、 吉里吉里Z(2013/12/31版)で確認してみた。確認スクリプトは以下の通り。 要素数100000の辞書を作って、単純にsaveStruct()でモード (t=テキスト、c=暗号化、z=圧縮、b=バイナリ)ごとにセーブ、 evaStorage()でそれをロードしているだけ。

[eval exp="tf.NUM = 100000"]

[macro name=hashsave]
hashセーブ中([emb exp=mp.mode]) ... 
[eval exp="mp.now = System.getTickCount()"]
[eval exp="(Dictionary.saveStruct incontextof hash)('hash'+mp.mode, mp.mode)"]
[eval exp="mp.time = (System.getTickCount() - mp.now)"]
[emb exp=mp.time] ms[r]
[endmacro]

[macro name=hashload]
hashロード中([emb exp=mp.mode]) ... 
[eval exp="mp.now = System.getTickCount()"]
[eval exp="mp.hash = Scripts.evalStorage('hash'+mp.mode)"]
[eval exp="mp.time = (System.getTickCount() - mp.now)"]
[emb exp=mp.time] ms[r]
[endmacro]


辞書作成中 ... 
[iscript]
var hash = %[];
var now = System.getTickCount();
for (var i = 0; i < tf.NUM; i++) {
	var elm = "hash_number_is_" + "%06d".sprintf(i);
	hash[elm] = 1;
}
tf.time = (System.getTickCount() - now);
[endscript]
[emb exp=tf.time] ms[r]
[r]

; hashの再構築待ち
[wait time=10000]

;テキストセーブ ... 本当は't'は無視されるだけ
[hashsave mode=t]
;暗号化セーブ
[hashsave mode=c]
;圧縮セーブ
[hashsave mode=z]
;バイナリセーブ ... 吉里吉里2.32には存在しない
[hashsave mode=b cond="System.versionInformation.substr(11,1) == 'Z'"]
[r]
;テキストロード ... 本当は't'は無視されるだけ
[hashload mode=t]
;暗号化ロード
[hashload mode=c]
;圧縮ロード
[hashload mode=z]
;バイナリロード ... 吉里吉里2.32には存在しない
[hashload mode=b cond="System.versionInformation.substr(11,1) == 'Z'"]

[s]

データサイズはこんなカンジ。かなり大きいでスね。 普通はこんなサイズのデータを残すことはそうそうないだろうけど。 同じテキストの繰り返しだから、圧縮すると小さくなるなぁ。

-rwx------+ 1 xxx xxx 4400013 5月  30 12:45 data/hashb*
-rwx------+ 1 xxx xxx 6400029 5月  30 12:46 data/hashc*
-rwx------+ 1 xxx xxx 6400026 5月  30 12:45 data/hasht*
-rwx------+ 1 xxx xxx  330808 5月  30 12:46 data/hashz*

そして結果。Win7Pro[Core i5-2520M(2.5GHz), 8GB memory]上で確認。 今回はデータをSSD上に配置したので、実際のデータ読み書き時間はスゴい短い、はず。

辞書作成中 ... 11163 ms

hashセーブ中(t) ... 7252 ms
hashセーブ中(c) ... 7094 ms
hashセーブ中(z) ... 337 ms
hashセーブ中(b) ... 3635 ms

hashロード中(t) ... 10334 ms
hashロード中(c) ... 10625 ms
hashロード中(z) ... 10951 ms
hashロード中(b) ... 68 ms

速ッ!速いよバイナリモード!セーブは半分くらいの時間だけど、ロードが 異様に速い。なんでそんなに?と思ってソース見たら、なるほど、バイナリモードで セーブしたら配列や辞書の要素数がセーブデータ中に書かれるので、 ロード時は最初からそれだけ確保しといてぽいぽいデータを放り込むのですな。 セーブデータ中に型も書かれてて型チェックも省けるから、これは確かに高速だなぁ。 使えるならコレを使った方がいい、ということはよくわかった。

ただ、書き込み時間はそんなに高速ではないから、ラベル通過(=セーブ)の度に これだけ時間がかかることを考えると、現実的には 'z' + 拙作highSpeedLabelerの方がいいのかもしれない。 それならデフォルト吉里吉里でも使えるしね。

…全く関係ないけど、「何故Dictionary.saveStruct()を拡張したのか」がモノスゴ 気になる…。綺麗に拡張するなら、TJSCreateTextStreamForWriteの方を拡張 すべきじゃなかったのかなぁ、と。クラス名がTJSCreateTextStreamForWrite() だから、バイナリデータは書かせちゃダメだろ、という判断かしら。 だったら'z'とか'c'とかの意味は…とか謎は深まるばかりであった。 うんどうでもいい話なんですけどね。

更に余談。同じスクリプトを吉里吉里2.32で動かすと、hashロードの時間が 全体的に半分くらいになる(=2倍高速)。吉里吉里Z優秀!(なのか?)

更に更に余談。スクリプト中の[wait time=10000]を除くと、セーブ時間は 変わらないのにロードが6倍くらい高速になるという世界の不思議。オカルトだ!


1クリックでオートモードを抜けたい

え、どゆこと?と思う人は困ってない人だろうからここ見なくてよし。 吉里吉里/KAGでは、オート(自動読み進み)モード中にクリックすると、 「オートモードをキャンセルする関数が呼び出される」だけだ。ということは、 例えばオートモード中の長い文章表示中にクリックすると、「オートモードは 解除されるが文章は表示が続く」ことになる。しかし、オートモード中で なければ、その文章は最後のクリック待ちまでスキップされる。そういう、 オートモードと通常時の動作の違いがなんかイヤだ、という方のための話題。 前置き長いですかそうですね。

オートモード中にクリックを受けてオートモードキャンセルするのは、 system/MainWindow.tjs中のonPrimaryClick()関数の中の以下の部分。

function onPrimaryClick()
{
(snip)
	if(st != stopst && autoMode)
	{
		// 自動読みすすみの場合
		cancelAutoMode(); ★ここ
	}

確かに、cancelAutoMode()しか実行していない。 ということは、この部分を細工すればいいわけだ。 色々試行錯誤してみたが、こんな風にするといいみたい。 click待ちのものがあればそれをキャンセルし、キャンセルされなかったら、 次のクリック待ちまでスキップするというそんな。 これで、例えば文末で[ws]とかで音声待ちをしている場合にも、その音声を 中止してすぐにクリック待ちになる、はず。二回クリックとかカッコ悪いしね。

function onPrimaryClick()
{
(snip)
	if(st != stopst && autoMode)
	{
		// 自動読みすすみの場合
		if (!kag.conductor.trigger('click'))
			if (st == runst && kag.clickSkipEnabled && kag.skipMode == 0)
				kag.skipToClick();
		cancelAutoMode();
	}


セーブデータの保存場所を変えるには

確か吉里吉里3.30以降だったと思うが、krkrconf.exeで[システム全般]→ [データ保存場所]を変更することでセーブデータの保存場所を変更することができる ようになった。しかし、実はコレ、ゲーム名.cfファイルに設定を書き出すだけ。 なので、.cfファイル書き換えられて --debug 付けられたりすると色々アレに なったりする。むぅ。いや、確かに、セーブデータの場所の変更とかが必要な 場合はこういうのアリだけれども。

やっぱり前のように、system/Config.tjsのfunction KAGWindow_config()の中で、 saveDataLocation変数を定義した方がいいんじゃなかろうか。Windows Vistaや7の UAC(User Access Control)の仕組みを考えると、%APPDATA%\ゲーム名 以外に セーブデータをおくことはヤバそうだし。なので、我輩オススメはConfig.tjsで 以下のように定義すること。

//-------------------------------------------- ウィンドウや動作の設定 -----
(snip)
function KAGWindow_config()
{

saveDataLocation = System.appDataPath + 'ゲーム名';

(snip)

System.appDataPathはOSごとに内容が異なるため、詳細は このページを参照のこと。

余談だが、現在の吉里吉里(のsystem/MainWindow.tjs)では、saveDataLocationに 定義されたものがフルパスならそれに従い、相対パスならゲームの実行ファイルの 位置からの相対位置にセーブデータを書き出す。上の例は絶対パスなので、 ゲームをどこにインストールしても、必ず指定された場所にセーブデータが 書き出される。


「一つ前の選択肢に戻る」を実装する

結構簡単に作れるのにあんま実装されていない機能。これ作っとくとユーザだけでなく デバッグの時にも非常に便利なので、是非皆様には実装して頂きたい。具体的には、 [record]タグを使う。

  1. system/Config.tjs を変更する
    変更するのは二つ、recordHistoryOfStoreとmaxHistoryOfStore。後者は数を 変更しなくていいならデフォルト(=5)のままでもよい。前者は、0 にすることを オススメするが、[s]が存在する箇所が選択肢の時だけであることが確認できて いるのなら、2 にしてもよい。

    (snip)
    
    // ◆ 通過記録
    // 通過記録を自動的に行うかどうかを指定します。
    // ラベル記録とは違います。
    // 通過記録を行うと、「システム - 前に戻る」で、直前に
    // 通過記録を行った箇所に戻ることができます。
    // 0 を指定すると自動的には通過記録を行いません ( record タグで手動で通過記
    //   録を行うことはできます )
    // 1 を指定すると 保存可能なラベルを通過するごとに自動的に通過記録を行いま
    //   す。
    // 2 を指定すると選択肢 ( s タグで停止 ) ごとに通過記録を行います。
    ;recordHistoryOfStore = 0;
    
    
    // ◆ 通過記録の最大数
    // 通過記録の最大数を指定します。最大、ここで指定した回数、前に戻る事ができ
    // ます。大きくするとセーブデータも大きくなります。
    ;maxHistoryOfStore = 5;
    
    (snip)
    

  2. メニューバーの文字列を変更する
    メニューバーに表示される文字列を「前に戻る」から 「一つ前の選択肢に戻る」に変更しておいたほうがいいだろう。 このためには、system/Menus.tjsのgoBackMenuItemのメッセージを書き換える。

            systemMenu.add(this.goBackMenuItem = new KAGMenuItem(this, "一つ前の選択肢に戻る(&B)", 0,
                    onBackStartMenuItemClick, false));
    

  3. シナリオの選択肢部分に[record]タグを挿入する
    recordHistoryOfStore = 0 としたのなら、[record]タグを適当な位置に挿入する。 我輩のオススメは、選択肢を表示した直後の[s]の直前に[record]を挿入すること。 注意すべきは、[record]は、この直前の「セーブ可能ラベル」の位置の情報を 記録するため、選択肢を表示する画面の頭にセーブ可能ラベルを配置する必要が あること。

    *選択肢表示画面|選択肢表示画面
    [link target=*選択肢1を選択]選択肢1[endlink]
    [link target=*選択肢2を選択]選択肢2[endlink]
    [record]
    [s]
    *選択肢1を選択|選択肢1
    (snip)
    
    *選択肢2を選択|選択肢2
    (snip)
    


トランジションをスキップするとエラーが出る

ゲームテスト中にモノスゴいイキオイでスキップさせていると、時々以下のような エラーが出ることがある。

 00:13:49 ==== An exception occured at kaglayer.tjs(232)[(function) beginTransition], VM ip = 303 ==== 
 00:23:32 -- Disassembled VM code -- 
 00:23:32 #(232)   super.beginTransition(method, withchildren, src, elm); 
 00:23:32 00000303 calld %0, %2.*31(%-5, %-6, %-4, %-3) // *31 = (string)"beginTransition" 
 00:23:32 -- Register dump -- 
 00:23:32 %-6=(int)1  %-5=(string)"mosaic"  %-4=(object)(object 0x01B30F88:0x01B30F88) 
 00:23:32 %-3=(object)(object 0x01B14E4C:0x01B14E4C)  %-2=(object)(object 0x0013E0DC:0x00000000) 
 00:23:32 %-1=(object)(object 0x00BF9C6C:0x00BF9C6C)  %0=(void) 
 00:23:32 %1=(object)(object 0x00B67F24:0x00000000)  %2=(object)(object 0x00B6B588:0x00000000) 
 00:23:32 %3=(object)(object 0x00B95D4C:0x00B95D4C) 
 00:23:32 -------------------------------------------------------------------------------------------- 
 00:23:32 trace : graphiclayer.tjs(124)[(function) beginTransition] <-- override.tjs(72)[(function expression) (anonymous)] 
  <-- mainwindow.tjs(5303)[(function expression) (anonymous)] <-- conductor.tjs(440)[(function) onTag] <-- conductor.tjs(104)[(function) timerCallback] 
 00:23:32 エラーが発生しました 
 ファイル : first.ks   行 : 237
 タグ : trans ( ← エラーの発生した前後のタグを示している場合もあります ) 
 現在のトランジションを停止させてから新しいトランジションを開始してください。同じレイヤに対して複数のトランジションを同時に実行しようとするとこのエラーが発生します 

判ってる人にはなんてことないんだけど、知らないと原因を掴むのが難しいかも しれない。原因は、first.ks の 237 行で、[trans]を実行しているが、それが 以前の [trans] の終了前に実行されたため。文字通り、「複数のトランジションを 同時に実行しようとした」ためにこのエラーが発生した。

で、なんとかするにはどうしたらいいかというと、対応も簡単、全ての[trans]の前に 必ず[stoptrans]をくっつければよい。全ての[trans]を[wt]で待つようにしても いいが、きっと[stoptrans]を入れた方が確実だろう。[stoptrans]はトランジション 中でなければ単純に無視される。

...
[stoptrans]
[backlay]
[image storage="graph0" layer=0 page=back]
[trans method=crossfade time=2000]
...
[stoptrans]
[backlay]
[image storage="graph1" layer=0 page=back]
[trans method=crossfade time=300]
...

もちろん、こういうのはちゃんとマクロ化して抜け漏れのないように作れば 更に完璧。…でもさ、こういうのってシステム側でなんとかすべきじゃないかしらと 思わなくもない。


なぜゲームの動作が「もっさり」するのか(改ページ編)

なんか…遅いなぁ…。スキップさせても文字が表示されんのが遅い。むむぅ、 なんでこんなにもっさり動くのだ。そう思ったら、マメにいろんなところに System.getTickCount()をつっこんで、実際その処理にかかっている時間を 計ってどこが遅いのか探すのが吉。で、スキップさせる文字が遅いなぁ、と いうののもっともありがちな理由は、改ページ部分に要らん処理が入って いること。代表的なものを以下に列挙しておく。

  1. 改ページで必ずセーブするようにラベルが書いてある
    どこでもセーブプラグインを使っていても同じ。ラベルがあると、変数のセーブが 必要になるので、そこでちょっと遅くなる。どのくらいかというと、 Core2Duo(1.83GHz)で20ms〜30ms程度。そんなに遅くないじゃん、と思うかも しれないが、長いとおよそ1/30秒だと考えると案外無視できない。改ページ以外の、 一画面分の文字表示が大体20〜30msなので、単純に倍の時間がかかることになる。 …とはいえ、これを削るのはなかなか難しい。いっそあきらめて、セーブポイントを 複数ページに一度にしちゃう、とかしてもいいかもしれないが、まずはその他の部分に 無駄がないか探してみよう。

  2. 改ページの時に顔画像を必ず消去するようにしている
    顔画像(メッセージウィンドウの左側に表示するアレ)の表示・消去も結構重い。 空画像を読み込んだり、fillRect()とかでレイヤをクリアしてたりすると、 更に遅さ倍増。これも作り方によっては30ms(Core2Duo 1.83GHzで)くらいかかるので、 少なくとも、「表示されてない時にも消去する」なんてことをしないように作ろう。 そういう努力、大事。かのLittleWitchのシュガーコートフリークスでも ちゃんとそういう努力してたよ(だから顔画像が表示されると 途端にスキップが遅くなったりする)。

  3. 立ち絵の消去で無駄処理サクレツしている
    顔画像と同じく、立ち絵を消去する時に、空画像を読んでいる、とか、 fillRect()で消去してる、とかすると、立ち絵が大きいとかなり遅くなる。 下手すると50ms以上かかっちゃうことがある。じゃぁどうするか。 レイヤーのvisible=falseだよ!これが一番早いんだよ!というわけで、消去の時は、 可能な限りvisible=falseにしましょうよ。ええもう。

試せばわかるけど、テキストのフォントが影付きとか縁取りとかされてても、 そんなに遅くないんだよね。とにかく画像をどうこうするのはテキメンに遅いので、 そのあたりをなんとかしよう。さすればもっと快適に。


なぜゲームの動作が「もっさり」するのか(セーブ編)

画面サイズをフルスクリーンにしたり戻したり、栞をセーブしたり、テキストの 読み進み速度を設定したりすると、なんか…吉里吉里が止まるんですけど数秒くらい。 この原因はsystem/MainWindow.tjsのSaveSystemVariables()にあることが多い。

SaveSystemVariables()は、システム変数(kag.scflagsとkag.sflags(=sf.*))を savedata/datasc.ksdとsavedata/datasu.ksdにセーブする関数。なんだが、 これが遅い。なぜか遅い。やたら遅い。もちろんセーブする変数の数が多いと 遅いのはなんとなくわかるのだが、そうでもない時でも1秒とか2秒とか かかることがある。いやソレかかりすぎでしょう!と思うこともしばしば。 原因は主に以下の四つ。

  1. セーブ先のストレージが遅い(特にUSBメモリとかSDカードだったりすると)
  2. system/Config.tjsで栞の保存モード(saveDataMode)を"z"にしていない(=セーブ量が多くなる)
  3. KAGPluginをいっぱい使ってて、それぞれのonSaveSystemVariables()が重い
  4. SaveSystemValiables()がいろんなところから頻繁に呼ばれている

1.と2.以外はちょっと修正できないのでおいといて、1.と2.くらいは対策しても いいんじゃなかろうか。特に2.はほぼ必須だと思う。


なぜゲームの動作が「もっさり」するのか(dbstyle)

フルスクリーン←→ウィンドウモードの変更、画面の拡大縮小とか、そういう ちょっと派手な操作をしよっかなー、と思うと、一瞬画面が止まることがある。 0.6秒くらい。なにこれ?でも一度起こると二度は起こらない。なにこれ?

答えはdbstyle。 こちら「コマンドラインオプション」の、「グラフィック関連のオプション」の 「-dbstyle (ダブルバッファリング方式)」項を参照。

(前略)画像を表示するときにダブルバッファリングを行う際、 どの方式を用いるかの設定です。
 設定可能な値は 'auto' (自動), 'gdi' (GDIを用いる), 'ddraw' (DirectDrawを用いる), 'd3d' (Direct3Dを用いる) のいずれかで、このオプションを指定しないと 'auto' が指定されたものと見なされます。(中略)
 'auto' が選択された場合、DirectDraw と Direct3D のどちらかを使うかを決めるために、0.6秒ほどの時間をつかってベンチマークを行い、高速な方を選択します。(後略)

なんで動かしてる最中にベンチマークするのん。せめて起動時に一回やってその結果を 保存してくれてればこんなこと起こんないんだけどなぁ…。それだと正確な値が 得られないよというのはわかってるけれども。

回避策は、「-dbstyle=ddraw」などを起動オプションに付加することくらいしか 思いつかない。しかしこれだとうまいこと動かなかったりちょっと遅くなったり することもあるわけで…。 スクリプト中で、操作の前に setArguments("-dbstyle", "ddraw") とかやってみたけど 変わらなかったので、なんとかするいい方法はいまのところなさそう。「我慢する」 というのがいい方法だというならそれで。


なぜゲームの動作が「もっさり」するのか(デバッグメッセージ編)

ほんのちょっと遅い…もう少し早く動いてほしいのに…。という時、実はデバッグ メッセージが大量に出力されていて、それが原因であることがある。複雑なマクロや 深いサブルーチンを作りこんだ、凝ったシステムで多い。

吉里吉里2/KAG3のデフォルトではデバッグレベルがちょい高いので、 コンソールを表示して、流れていくメッセージがヤケに多いなぁ、 ということに気づいたら、抑制を考えてみてもいいかもしれない。

簡単には、AfterInit.tjsに以下の行を追加する。

kag.mainConductor.debugLevel = 0; // ゲーム時のデバッグメッセージ抑制 kag.extraConductor.debugLevel = 0; // 右クリックルーチンのデバッグメッセージ抑制

当然、こうするとデバッグメッセージは表示されなくなる。デバッグ時には 逆に = 2(最大) とかにしといた方がいいかもしれない(def = 1)し、 製品でもかなり「バグはない」と自信がないと、いざ問題が起こった時に どうしようもなくなって困るかもしれないことは覚悟しとくこと。

また、debugLevel=0 でも有る程度のメッセージは出力されるので、それも 抑制するために、first.ksの頭とかに以下を挿入しとくのはありかもしれない。

[eval exp="dm = function() {}"]

こうすると、本当に何も表示されなくなる。よっぽど自分のスクリプトに自信の あるヤツじゃない限りはすべきじゃないけどね。


TJSはループを使わない方が早い

TJSはスクリプト言語なので(=CPUキャッシュに乗るかどうかとかをあんま考える必要が ないので)、直感的に「こっちの方が速いんじゃないか?」と思う方法が大体速い。 たとえば、イニシエの懐かし技、ループ展開。以下のように、20の要素の中から voidでないものを加算するというプログラムは上より下の方が2倍高速。 Corei3 2120T(2.6GHz)で実測すると、一万回繰り返して上は176ms、下は89ms。

var a = %[];
(snip)
var sum = 0;
for (var i = 0; i < 20; i++)
	sum += +a['aaa'+i] if (a['aaa'+i] !== void);

var a = %[];
(snip)
var sum = 0;
sum += +a['aaa'+0] if (a['aaa'+0] !== void);
sum += +a['aaa'+1] if (a['aaa'+1] !== void);
sum += +a['aaa'+2] if (a['aaa'+2] !== void);
sum += +a['aaa'+3] if (a['aaa'+3] !== void);
sum += +a['aaa'+4] if (a['aaa'+4] !== void);
sum += +a['aaa'+5] if (a['aaa'+5] !== void);
sum += +a['aaa'+6] if (a['aaa'+6] !== void);
sum += +a['aaa'+7] if (a['aaa'+7] !== void);
sum += +a['aaa'+8] if (a['aaa'+8] !== void);
sum += +a['aaa'+9] if (a['aaa'+9] !== void);

sum += +a['aaa'+10] if (a['aaa'+10] !== void);
sum += +a['aaa'+11] if (a['aaa'+11] !== void);
sum += +a['aaa'+12] if (a['aaa'+12] !== void);
sum += +a['aaa'+13] if (a['aaa'+13] !== void);
sum += +a['aaa'+14] if (a['aaa'+14] !== void);
sum += +a['aaa'+15] if (a['aaa'+15] !== void);
sum += +a['aaa'+16] if (a['aaa'+16] !== void);
sum += +a['aaa'+17] if (a['aaa'+17] !== void);
sum += +a['aaa'+18] if (a['aaa'+18] !== void);
sum += +a['aaa'+19] if (a['aaa'+19] !== void);

色々試してみたら、ループ処理そのものはそんなに重くないことがわかった。 それよりも影響がデカいのは、変数参照とかそこいらへんみたい。下のインデックスを 'aaa'+0じゃなくて'aaa'+iの形に書き換えるととたんに上と同じくらいになる。また、 こんな話もあるので、 a['aaa'+0] の部分を a.aaa0 に変更すると、下は更に10%ほどスピードアップする。

言えば、こんなのが問題になることはそうそうない。んだけど、継承を繰り返した 巨大なクラスのコピーとかで、大量にメンバ変数をごそごそする必要があったり する時に、ふいっと遅くなったりすることがあって、そういう時に調べてみると、 こんなのが原因だったりする、という例までに。

Z80世代としては、ループ展開で速くなるちうのは、なんか嬉しい。 68030で256byte(※KとかMとかついてない)しかないキャッシュに収まるように 最内周のループを調整したりはしてたけど、でもそこまでだったしー。


メッセージウィンドウの背景画像(=frame)が半透明にならない!

メッセージウィンドウの背景画像(=frame)をPNGで作成した場合にハマる。 PNGなんだから半透明で表示してよ!と思うのだが、なんだか知んないけど 半透明になってくんない。半透明部分が全部不透明になる。 なぜだ!?大宇宙の意思か!?

理由は、Config.tjsでlayerType = ltAddAlpha; と指定しているのに、メッセージ ウィンドウの背景画像がaddAlpha形式でない(=フツーそうだ)ため。 よーく読むと、Config.tjsのlayerType部分の説明に、「layerType = ltAddAlpha なら背景画像もaddAlpha形式でないとダメ」と書いてある。気づくかこんなの! 回避策は以下の二つのいずれか。

  1. Config.tjsで layerType = ltAlpha; と設定する
  2. 背景画像を、画像フォーマットコンバータ(krkrtpc.exe)で[αチャネル付きTLG6]+[ltAddAlpha形式で出力する]にてTLG6に変換する

なぜConfig.tjsのlayerTypeのデフォルトを、わざわざ使いづらいltAddAlpha形式に 変更したのか、理由知ってる人は教えてください。「高速だから」っていったって、 三倍も四倍も高速なワケじゃないから、それ以外の理由がきっとあるはず…だろうと 思うんだ…。


KAGの引数の書き方いろいろ

KAGの引数は基本的に文字列で渡される。シングルクオート・ダブルクオートで囲おうが 囲うまいが文字列になるので、書く手間を省くために、我輩はクオートしないことを オススメする。クオートが必要なのは、引数の中に特殊文字や空白が入るときのみ。

[KAGTAG  arg1=うでたてふせ  arg2="死ぬまで  続ける"]

引数の頭に '&' をくっつけると、それはTJS式として評価し、結果を渡す。 次の例は、KAGTAGに上の例と同じ引数を渡す。

[iscript]
function abc()
{
	return "うでたてふせ";
}
[endscript]

[eval exp="f.arg2 = '死ぬまで  続ける'"]

[KAGTAG  arg1=&abc()  arg2=&f.arg2]

もっと捻って、文字列を接続することもできる。そのときは、TJSの文字列の二項演算子 '+' を指定することができる。このとき、シングルクオートとダブルクオートを 駆使する必要があったりするので注意。あんまり複雑なときは、 '@' (@ つき文字列即値)を使うことも考えたほうがよいかもしれない。

[eval exp="f.arg1   = うでたて"]
[eval exp="f.arg2_1 = '死ぬ'"]
[eval exp="f.arg2_2 = '続ける'"]

[KAGTAG  arg1=&"f.arg1 + 'ふせ'"  arg2=&"@'${f.arg2_1}まで  ${f.arg2_2}'"]

「@ つき文字列即値」は こちらから探してくれたまえよ。


「呼び出そうとした機能は未実装です」

TJSでクラス書いたりしていると、このエラーが出ることがある。いや実装されてるって できるはずだって!と思っちゃうアナタのために、典型的な例をご紹介。
例えば以下のようなプログラムがあったとしよう。MyLayerクラスは、Layer クラスから 派生して、自身の内部にも l としてLayerを持つクラスだ。

class MyLayer extends Layer {
	var l;
	function MyLayer(w, par)
	{
		super.Layer(w, par);
		l = new Layer(w, par);
	}

	function finalize()
	{
		invalidate l;
	}
};

global.a = new MyLayer(kag, kag.fore.base);

このスクリプトを動作させると、「呼び出そうとした機能は未実装です」となる。 理由はわかるだろうか?うんきっとわかんないからここ見てんだよね。
では、先に正解を。こう書くとエラーにならないのだが、さて上との違いが判るかな? ヒント:違いは一文字だけ。

class MyLayer extends Layer {
	var l;
	function MyLayer(w, par)
	{
		super.Layer(w, par);
		l = new .Layer(w, par);
	}

	function finalize()
	{
		invalidate l;
	}
};

global.a = new MyLayer(kag, kag.fore.base);

判った? new Layer を new .Layer(= new global.Layer) に変えている。 さてここまでで、何故エラーになるかわかるだろうか。

もったいぶって悪かった、エラーの原因は、「new の後の Layer が、MyLayerの メンバ関数(=スーパークラスLayerのコンストラクタ)を呼ぼうとしているから」だ。 Layerから派生したクラスは、その中にLayerのコンストラクタ(関数名Layer) を 持っている。だから、このクラス中で Layer とだけ書くと、関数を参照しようと するわけだ。クラス定義なら、global に定義されている .Layer(=global.Layer) を 呼ばねばならない、ということ。

判りにくいよね。白状すると、我輩もかなり長いことよくわかんなかった。 でもまぁ、今はわかったし、すっきり宿便取れた気分でしょ?


インスタンスからクラス名を文字列で得る

TJS上で、あるインスタンスから、そのクラス名や、親クラスのクラス名が欲しいなー、 ということがたまにある。普通はそんなものどうしようもなかったのだが、 裏技というか、Scripts.getClassNames()という関数でそういうのが出来る。

この関数は、「そのインスタンスのクラス名・そのまた親クラス名を、文字列の 配列で返す。例えば、以下のような例なら、"A" "B" のように表示される。 添字0がそのインスタンスのクラス名、添字1以降は親クラス名・そのまた親クラス名 となっている。複数のクラスから継承された場合は、それも全て配列中に列挙される ことに注意(※そういうとき、親クラスか祖父クラスかを知る術はない)。 単一継承なら、getClassNames()が ["A", "B","C"] のような値を返せば、 Cが祖父、Bが親、Aが今のインスタンスの元クラス、となる。

// class A を定義
class A {
	function A()
	{
	}
};

// class B を class A を継承して定義
class B extends A {
	function B()
	{
		super.A(...);
	}
};

// class B のインスタンスを作成
var b = new B();
var ary = Scripts.getClassNames(b);

// クラス名を表示してみる
for (var i = 0; i < ary.count; i++)
	dm(ary[i]);


イベント発生の順番

LayerやWindowクラスを見ると、onMouseなんとかとかonKeyなんとかとか、そういう ユーザからの入力に対してイベントが発生したら呼ばれるのぜー、という コールバック関数がいくつかある。実際のイベントの時に何がどういう順番で 呼ばれるのかを知ってるとイイことあるんじゃないか、と調べてみた。そして、よーし これで一覧表ができた!と思ったら、 もっといいのがあるのを発見。先人の調査は偉大でありますな。 できれば調査の前に見つけたかったであります。ありがたく参考にさせて頂きます。

ただ、イベントって「時々発生しない」ことがあるみたい。たとえば Layer.setCursorPos()でマウスカーソルをレイヤ外に出すとLayer.onMouseLeave()が 呼ばれないとかそんな。だからボタンのステータスが変わらないことがあるんだなー。

フルスクリーンにした時の「黒帯部分のクリック」は、windowEx.dllを読み込まないと トラップできないみたい。説明は面倒なので 拙作onClickOutOfPrimaryLayer.ksを 見てくだされ。実はKAGEXを参考にしたのですがな。

2014/06/01追記。最新の開発版吉里吉里では、黒帯でクリックするとWindowクラスにonMouseDown()イベントが上がるようになった模様。MainWindow.tjsに、論理が入ってた。


関数のデフォルト引数と渡し方により、引数の値が変わる

TJSで、別の関数へ「この関数の引数を全部渡す」書き方に "..." というのがある。コレを使うと、その関数の引数を全部まとめて別の関数に 引き渡すことができて便利なのだが、デフォルト値が指定されていてもそれは別の 関数に引き渡されないので注意。実例出さないと難しいだろうから、 以下テストスクリプト。

class A {
	function A() {}
	function func(a, b=1)
	{
		dm('A: a = ' + a + ', b = ' + b);
	}
}

class B extends A {
	function B() {}
	function func(a, b=0)
	{
		dm('B: a = ' + a + ', b = ' + b);
		super.func(...);  // ★1
		super.func(a, b); // ★2
	}
}

var class_b = new B();
class_b.func(1);

これだと、実際には以下のように表示される。 ★1と★2で a と b は同じ値で渡るとうっかり期待しそうなんだけど。

B: a = 1, b = 0
A: a = 1, b = 1
A: a = 1, b = 0

TJSの関数説明の「引数の省略」 に、「引数変数の内容を変更していても(中略)元の内容、引数の数がわたります」 とあるので、少し違和感あるけれど、これは仕様通りの動作なんだろう、きっと。


マクロの再帰呼び出しに注意

KAGマクロは再帰呼び出しが可能。フツーに使っていいし便利なのだが、 あんま深い再帰(100万回とか)を呼ぶと超遅くなったり メモリ足りなくなったりするので、ここにメモしておく所存ナリ。 まぁ一般的に再帰ってそういうもんですけどな。

問題はKAGParserで起こる。KAGParserは、マクロを見つけると「今処理中の文字列の マクロ部分を、マクロで定義されてた文字列を埋め込む」という動作をする。

以下のような例を考えてみよう。マクロの機能には全く意味ないが、とりあえず 何度か再帰するマクロだと思ってくれたまえよ。

[macro name=abc]
[if exp="--tf.val <= 0"]
	[ch text=text][r]
	[return]
[endif]
[abc text=%text]
[endmacro]

[eval exp="tf.val = 5"]
[abc text=char]

さて、このマクロは、kag.conductor.macros.abc に、以下のように登録される。

[if exp="--tf.val <= 0"][ch text=text][r][return][endif][abc text=%text][macropop]

で、[abc text=char] が実行されると、

[abc text=char]

だった kag.conductor.curLineStr が、

[if exp="--tf.val <= 0"][ch text=text][r][return][endif][abc text=%text][macropop]

に置き換えられる。

問題はここから。これを実行していき、再帰が必要なことがわかると、 更にこの行中の[abc text=%text]をマクロで置き換える。すなわち、

[if exp="--tf.val <= 0"][ch text=text][r][return][endif][abc text=%text][macropop]

[if exp="--tf.val <= 0"][ch text=text][r][return][endif][if exp="--tf.val <= 0"][ch text=text][r][return][endif][abc text=%text][macropop][macropop]

になるということ。更に再帰が進むと、CurLineStrがどんどん長くなって…という、 つまりはそういうことだ。

というわけで、KAGのマクロを再帰呼び出しするのは内部的にちょい重いので、 ループでできるならなるべくループでやっといた方がいいよ、というお話でしたとさ。


[if]タグで[else][elsif]が使えるようになった

そんなんフツーに知ってるだろ、という人が居るかもしれないけれど一応。 吉里吉里2.28くらいから、KAGで [if] に続く [else] と [elsif] タグが 使えるようになった。なぜかそれまでこの機能が無くて使えなかった、 モノスゴ助かる。これで随分見通しが良いKAGスクリプトが書けるようになった。

以前の[else]が使えなかった場合場合[else]が使える場合
[macro name="メッセージ枠"]
[if exp="mp.表示"]
; メッセージ枠表示処理
[endif]
[if exp="mp.消去"]
; メッセージ枠消去処理
[endif]
[if exp="!mp.表示 && !mp.消去"]
[eval exp="dm('エラー:[メッセージ枠] に引数が無い!')"]
[endif]
[endmacro]
[macro name="メッセージ枠"]
[if exp="mp.表示"]
; メッセージ枠表示処理
[elsif exp="mp.消去"]
; メッセージ枠消去処理
[else]
[eval exp="dm('エラー:[メッセージ枠] に引数が無い!')"]
[endif]
[endmacro]


TABはKAGスクリプト上で空白として捉えられる

KAGでタグを書く時、特に [if] の時にはインデントしたいと思うことがままある。 しかし、KAG スクリプトでは行頭にスペースを入れることが許されていない。 スペースはテキストの空白として表示されてしまう。KAG2互換モードだったら 書けるけど閑話休題。
変わりに、TABを使うとよい。TABはKAGでは単純に無視される。

[macro name="メッセージ枠"]
[if exp="mp.表示"]
	; メッセージ枠表示処理         ← 行頭にTABが入っている
[elsif exp="mp.消去"]
	; メッセージ枠消去処理         ← 行頭にTABが入っている
[else]
	[eval exp="dm('エラー:[メッセージ枠] マクロに必要な引数が無い!')"]
[endif]
[endmacro]

ああそれにしても。KAGのタグは横に長くなりがちなので、是非1タグの複数行記述は サポートして欲しいと思う次第。今はタグは行を跨って書けないので、 KAGスクリプトはやたらめったら横長になっちゃうしー。 半角スペースがデリミタとして使えないからそう作る方が簡単なのは判るん だけどねぇ。でも、タグ中の改行と半角スペース・TABは空白として扱うように しちゃえばいいだけなので、実現はそんなに難しくないと思うんだけどなぁ。


「ゲーム中の選択肢は、全て選択肢を実行した後にテキスト画面をクリアさせたい」 なんて要求はありがち。一番簡単なのは、全ての選択肢ラベル後に[cm]を無条件に 入れることだ。例えばこんな。

[link target="*start"   ]はじめから[endlink][r]
[link target="*continue"]途中から[endlink]
[s]

*start|はじめから
[cm]		← テキスト画面クリア
(snip)

*continue|つづきから
[cm]		← テキスト画面クリア
(snip)

しかし、こんなのいちいち手で指定してたら必ず指定漏れが発生してしまうし、 画面クリアだけではなくて、もっと複雑な共通処理、例えば変数のクリアとか 画面がフラッシュするだとか画面揺するだとか、そういうのが入った時に 結構タイヘン。だもんで、スクリプタはスクリプタらしく、マクロ化・自動化を 考えてみる。具体的には、[link]タグが選択された時に、一度共通部分(下では *procedure_after_every_link)を必ず通り、その後目的のリンクにジャンプ するように作ればよい。

macro.ks ファイル中↓
[macro name="start_link"]
[link storage="macro.ks" target="*procedure_after_every_link" exp=%exp]
[endmacro]

[return]
; この後ろはマクロ定義中には実行させない

; link 選択後は必ず一度ここに飛んでくる。
*procedure_after_every_link
[cm]		← テキスト画面クリア
; 共通処理後、目的のジャンプ先へ。
[jump storage=&tf.storage target=&tf.target]
ゲームスクリプト中↓
[start_link exp="tf.storage='main.ks', tf.target='*start'"   ]はじめから[endlink][r]
[start_link exp="tf.storage='main.ks', tf.target='*continue'"]途中から  [endlink]
[s]

*start|はじめから
(snip)		← いちいちテキスト画面クリアしなくてよくなった

*continue|つづきから
(snip)		← いちいちテキスト画面クリアしなくてよくなった

…うん、まぁ悪くない。しかし、もうちょっと [start_link] はひねった方が よさそう。「使う側から優しいマクロを作る」という意味で言えば、[start_link] は [link] と同じ引数を取るようにした方が判りやすい。というワケで、 結論はこんなカンジ。 [start_link] 中で、mp.storage と mp.target を作っている処理に注目。

macro.ks ファイル中↓
[macro name="start_link"]
; これだと引数expは使えなくなってしまうが、まぁいいでしょ。
[eval exp="mp.storage = kag.conductor.curStorage" cond="mp.storage === void"]
[eval exp="mp.exp = 'tf.target = ' + '\'' + mp.target + '\', tf.storage = ' + '\'' + mp.storage + '\''"]
[eval exp="mp.storage = 'macro.ks'"]
[eval exp="mp.target = '*procedure_after_every_link'"]
[link *]
[endmacro]

[return]
; この後ろはマクロ定義中には実行させない

*procedure_after_every_link
[cm]		← テキスト画面クリア
; 共通処理後、目的のジャンプ先へ
[jump storage=&tf.storage target=&tf.target]
ゲームスクリプト中↓
[start_link target="*start"   ]はじめから[endlink][r]
[start_link target="*continue"]途中から  [endlink]
[s]

*start|はじめから
(snip)		← いちいちテキスト画面クリアしなくてよくなった

*continue|つづきから
(snip)		← いちいちテキスト画面クリアしなくてよくなった

かなりすっきり。[start_link]はexp=が使えないこと以外は[link]と完全互換。 こういう「使う側に優しくする努力」を忘れないようにしよう。


マクロが実際にどう定義されているか

マクロに関するいろいろ豆知識を。デバッグとかの時に役立つ…かも? 以上、これだけ知ってると、いろいろ面白いことができる…かも?


マクロの引数をマクロ名としてマクロを定義する

キャラクタを操作するマクロを組むとき、[reg_char name=xxx](キャラクタxxxを登録) と [disp_char name=xxx pos=xxx](キャラクタxxxを表示)というマクロをよく使う。 でも、[disp_char name=xxx pos=right] なんてマクロだと表記が長すぎるから、 [xxx pos=right] のようにマクロが定義できると、 シナリオファイル中のタイプ量が減ってハッピーだ。しかし、吉里吉里では マクロは入れ子にはできないので、[reg_char] でこんな定義は不可能。

[macro name="reg_char"]
	[macro name=%name] ← マクロ中で、引数 name で新たにマクロを定義
	[disp_char *]
	[endmacro]
[endmacro]

で、このまま泣き寝入りするのもアレだなぁ、と思って試してたら、 マクロ中から呼び出したサブルーチン先ではマクロが登録できることがわかった。 ついでに、一つ上で述べたように、今のマクロの名前は mp.tagname で 参照できる。ので、こんな方法↓が使用できる。

macro.ks ファイル中↓
[macro name="reg_char"]
[call storage="macro.ks" target="*reg_char_sub"]
[endmacro]

[return]
; ここから下は macro.ks 定義時には使用されない

*reg_char_sub
[macro name=%name]
[disp_char * name=%tagname]
[endmacro]
[return]
ゲームスクリプト中↓
[reg_char name=あかね]

[あかね pos=center]

かなりの裏技なので、バージョン間で互換性とか無いかもしれないが、 ソース見る限りは大丈夫そうなのでまぁいいや。

注意点が一つ。マクロ名は、KAGParser内でtoLower()で必ず小文字に変換されるので、マクロ名に半角大文字を使わないようにすること。いや、使ってもいいけど 内部的には小文字に変換されてることを理解しておくこと。


トリガはキューされるか?

A という処理と、タイマハンドラ内で B という処理が動作している時、見かけ上は A と B は並列動作している。ここで、A でトリガを待ち、B でトリガを引く処理を 考える。このとき、普通に考えると A は [waittrig] でトリガ待ち、B は[trigger] または kag.conductor.trigger() でトリガを引くことになるだろう。

A の処理B の処理
  :
[waittrig name="trigger_name"]
  :
  :
[trigger name="trigger_name"]
  :

なのであるが、KAG のトリガは複数はキューされない上、同時には一つしか引けない。 従って、B が trigger を引いた「後」に A が [waittrig] で待ち状態になった場合、 次に B が trigger を改めて引くまで、A は永遠に待ち続けることになる。 微妙なタイミングで B の方が早く実行されちゃった場合など、A は永久に戻って こなくなり、なんかゲームが途中で止まる、ということがありうる。

これを防ぐためには、B のトリガを引く前に自らキューイングし、A の処理の前に トリガがあるかどうかをチェックする必要がある(しかも、そのときにはそのときで 別に注意事項がある)。…まぁ、現実問題そこまで厳密になることはないのだが。


排他的でアトミックな処理が必要な時

吉里吉里/KAGでは、KAGのスクリプトはアトミックに処理できないらしい。対して、 以下の二つについては、アトミックな処理であることが保障されているようだ。
  1. KAGタグの一行の処理が始まって終るまで([wait][ws][wf]などの待ち処理は別)
  2. TJS部分が走り切って吉里吉里に処理を戻すまで
アトミックな処理が必要なら、これらを守ることをお勧めする。

例えば、上で書いたようなトリガの場合、

A の処理B の処理
[if exp="待つべきトリガの処理はまだ終っていない"]
	[waittrig name="trigger_name"]
[endif]
  :
[trigger name="trigger_name"]
  :

というコーディングは、A の処理の一行目と二行目の間に B の [trigger] が 実行されて A がトリガを取りこぼす可能性があるため、ダメのダメダメだ。 こう書いた方がいい。

A の処理B の処理
[waittrig name="trigger_name" cond="待つべきトリガの処理はまだ終っていない"]
  :
[trigger name="trigger_name"]
  :

論理的には同じものなんだけどねぇ。面倒だけど、そういうものらしい…。 もっと正しい知識をお持ちの方は教えてください。


OP/ED/EyeCache中に、クリック一つでスキップする

サークルロゴ、オープニング、エンディング、スタッフロール、アイキャッチなど、 ユーザが入力しないで延々と待たされる処理で、クリックしても「ちょっとしか スキップできない」ようになっているゲームは案外多い。移動を待つための [wm canskip=true]タグなどが、クリック一つしか待てないから、というのが 主因なのは分かるけれど、ユーザがいらいらするのは間違いないので、 ちょっとくらいは捻ってみよう。

簡単に実現するなら、「一度でもクリックされたら終了処理にジャンプする」という マクロを一つ作って、これを要所要所に入れておけばよい。

; 現在のclick回数を保存しておく
[eval exp="tf.clickcountsave = kag.clickCount"]
; クリックスキップマクロ。クリックでスキップできる。
[macro name="checkclick"]
[jump target="*end" cond="tf.clickcountsave < kag.clickCount"]
[endmacro]
  :
; 何か移動処理
[wm canskip=true]
; 待ち処理の次は必ず[checkclick]する
[checkclick]
  :
; 何かトランジション処理
[wt canskip=true]
; 待ち処理の次は必ず[checkclick]する
[checkclick]
  :
; そんな調子で続ける
  :
; 最後にここに飛んでくる
*end
; 終了処理

カッコよく実現するなら他にも方法はいろいろあるけれど、気持ちこれが 一番簡単だと思う。KAGに手を入れてもいいなら入れればカッコいい方法があるが、 機能的にはどっちも同じだろと言われればそのとおりなので、こういう安直な 方法でもいいかなという気がする。


ハッシュに自身をassignしてはいけない

以下のようなTJSコードを実行すると、どうなるか考えてみよう。

var hash = %[ a:"1", b:"2", c:"3" ];

(Dictionary.assign incontextof hash)(hash);

自分に自分自身を assign している。フツーに考えるとコピーなんだから hash の中身は変わらんでしょ、という気がするが、実は正解は 「hashが空になる」。やってみるといい。

こんなことするやつぁ居ねぇよ!と思うかもしんないが、あるクラスのメンバ変数で あるハッシュ値を引数に渡した関数の先で、引数でそのメンバ変数を上書きするような 処理があったりすると確実にハマる。我輩は4時間くらいハマった。

class Abc {
	var hash = %[];

	Abc()
	{
		hash = %[ a:"1", b:"2", c:"3" ];
	}

	func1(elm)
	{
		; 渡されたハッシュで色々処理し、それをhashに保存する
			:
			:
		(Dictionary.assign incontextof hash)(elm);
	}

	func2()
	{
			:
			:
		func1(hash); ← func1 の中で hash を上書きしてしまう
	}
}

これで気づけというほうがどうかしてる!回避策は無いので、自ら気をつけて 下さいとしか言えない。


右クリックルーチン内でオートモードを有効にする

実は、吉里吉里では、右クリックルーチン内でオートモードを有効にすることは できない。system/MainWindow.tjs の restoreFromRightClick() 関数に細工 する必要がある。具体的には、restoreFromRightClick() 関数の最後に、 「autoModeになっていれば一度 click したことにする」処理を追加する。

右クリックルーチン内でのオートモードボタンの定義
[button graphic=rmenu_bt_auto target="*rclick_autoread" hint=自動読進]

(snip)

; オートモードを有効にしたらすぐに右クリックルーチンを抜ける
*rclick_autoread 
[call target="*rclick_return"]		← 右クリックルーチンの後処理 
[eval exp="kag.enterAutoMode()"]	← autoModeを有効化 
[return]				← 右クリックルーチンを抜ける
system/MainWindow.tjsの細工 in restoreFromRightClick()
        function restoreFromRightClick()
        {
                // 右クリックサブルーチンから抜けるときに呼ばれる
                if(typeof this.rightClickMenuItem != "undefined")
                {
                        if(rightClickName == "default")
                                rightClickMenuItem.caption = rightClickCurrentMenuName = rightClickDefaultName;
                        else
                                rightClickMenuItem.caption = rightClickCurrentMenuName = rightClickName;
                }
		// ここから下を追加
		// 右クリック中でautoMode設定された場合に対応。
		if (autoMode) {
			enterAutoMode();
			// inStable = 0でenterAutoMode()内でPrimaryClick()され
			// ないので、ここでclickしておく
			conductor.trigger('click');
		}
	}

うーん、理由を説明するのは面倒だから省略。とりあえず困ったことないし。 気になる人は自分で調べてみてくだされ。見てわからんヤツは聞いてもわからんって 死んだ親父がゆってたし。


右クリックルーチン内でスキップモードを有効にする

実は、吉里吉里では、右クリックルーチン内でスキップモードを有効にすることは できない。理由は右クリックルーチン内でのオートモードと同じ。 回避策もほぼ同一なんだが、右クリックルーチン内でスキップモードに入ったか どうかを判断するために kag.skipMode や kag.nextSkipEnabled を使おうと するとハマる。前者は、kag.returnExtraConductor()中でcancelSkip()を実行されて falseに設定されてしまうし、後者は「スキップモードしてもいいよ」フラグで あって実際にスキップモードかどうかを示していないため。

ではどうするか。仕方ないので、適当な変数(ここでは kag.skipMode_rclick)を 勝手に自前で定義して使う。右クリックルーチン先頭でこの値をfalseに設定し、 skipボタンが押されたらこの値をtrueに設定しつつ、kag.restoreFromRightClick()中で kag.skipToNextStopByKey() を実行する。

右クリックルーチン内でのスキップモードボタンの定義
*rclick
[eval exp="kag.skipMode_rclick = false"]
(snip)
[button graphic=rmenu_bt_skip target="*rclick_skip" hint=既読スキップ]
(snip)


; スキップモードを有効にしたらすぐに右クリックルーチンを抜ける
*rclick_skip 
[call target="*rclick_return"]		← 右クリックルーチンの後処理  
[eval exp="kag.skipMode_rclick = true"]	← skipModeを有効化したしるし
[return]				← 右クリックルーチンを抜ける
system/MainWindow.tjsの細工 in restoreFromRightClick()
        function restoreFromRightClick()
        {
                // 右クリックサブルーチンから抜けるときに呼ばれる
                if(typeof this.rightClickMenuItem != "undefined")
                {
                        if(rightClickName == "default")
                                rightClickMenuItem.caption = rightClickCurrentMenuName = rightClickDefaultName;
                        else
                                rightClickMenuItem.caption = rightClickCurrentMenuName = rightClickName;
                }
		// ここから下を追加
		// 右クリック中でskipMode設定された場合に対応
		if(skipMode_rclick)
			skipToNextStopByKey();
	}

※初出時は、kag.skipMode_rclickのような独立した値ではなくて、 kag.nextSkipEnabled を使おうとしていたためうまいこと動いていなかった。 指摘いただいた方に深謝しつつ、お詫びして訂正します。


ルビはどう実装されているか

吉里吉里の本文ルビ[ruby]は、『直後に続く一文字に対してルビを振る』機能だ。 ルビの振り方はいささか乱暴で、[ruby text=xxxx]タグがあったら xxxx を いったん内部的に保存し、次の一文字(正確には次の[ch])を表示する時に その文字に対してズバンとセンタリングして一気に書いてしまう。最初見た時、 ソレはあんまりだろう!と思った。
この方法には以下のような弱点がある。

以上を鑑みた上でルビは振ろう。一文字づつ振ることをお勧めする。

[ruby text="      うでた   ふ"]腕立て伏せ、ではなくて、
[ruby text="うで"]腕[ruby text="た"]立て[ruby text="ふ"]伏せ、のように。


複数文字へのルビ

上でルビの振り方についてなんとなく学んだところで、では複数文字にルビを振るには どうしたらよいか考える。ルビは、一旦バッファリングされて、次の一文字(正確には [ch]タグ)に対して振られる。ここで、「正確には…」としぶとく書いたのにはワケが あって、つまり、「一文字」ではなくて、「[ch]で指定した文字列」に対して ルビ振りができるわけだ。ルビ振りは通常センター割付なので、例えば複数文字に 対してルビを振るなら、

[ruby text=しめ][ch text=七五三]と読む、とか、
[ruby text=くらだない][ch text=やっちもにゃあ]のぅ、とか。

のように書くと、複数文字へのセンター割付ができる。

…なんだけど、これだと更に二つの問題が残る。

  1. 結局均等割付はできない
  2. ルビ中に改行・改ページできない
…ので、やっぱり(手前味噌なのは承知の上で) erubyを使うってので どうかしら?


複数の横棒(───)を書く時は

最初に多用されたのは月姫だったと記憶しているが、複数横棒(───)を使う時は 注意が必要だ。普通に書いてしまうと、文字に装飾(影や縁取り)がある場合に それらが前の文字にかかってしまい、途中で切れたような文字になってしまう ことがある。 これを防ぐには、[ch]タグを使う。

×:すばらしい───
○:すばらしい[ch text=───]
一見全く役に立たなそうな[ch]タグであるけれど、上のルビも含めて、実は こういうところで微妙に役立つのであるよ。 いちいち指定するのが面倒ではあるけれど。これでもダメな場合は、仕方ないので 字間を詰めるとかしないとダメね。

もう一つ、フォント限定になるが、色々な横棒を試してみてもいいかも。 横棒には、━(よこ)、─(よこ)、−(マイナス)、―(ダッシュ)、━(罫線)、─(罫線) など、色々な種類があって、それぞれフォントごとに特徴があるので、 フォントが決まっているなら、表示してみて一番良いものを選択する、というのも アリかもしれない。全角ダッシュに至っては、IBM-Unicode(0xe28094)と MS-Unicode(0xe28095)とで文字コードが異なるとか、面倒で鼻血出ちゃう。


KAG2互換モードはなるべく使わない

system/config.tjs 内で、global.ignoreCR = false と設定することで、 KAG2互換モード、具体的には「改行を画面上でも改行と扱う」ことが 可能になる。しかし、このモードは、ちゃんとわかっていないと よからぬところで改行を発生させることになるため、 使用には注意が必要だ。

具体的には、空行を書く時と、他人が作ったプラグインを利用する時。 空行は改行を含むため、単純に空行を書くだけで、そこでは改行と なってしまう。また、他人が作ったプラグインは、最近だと基本的に ignoreCR=true を前提に作成されているため、全てを見直して、末尾に¥を追加する作業が 必要となる。

ダメな例良い例
; 主人公マクロ
[macro name=主人公]

[if exp="mp.苗字 !== void"]
中西
[else]
慶介
[endif]

[endmacro]

*start|開始

[主人公] は、萌えた。
とても萌えた。
[p]
; 主人公マクロ
[macro name=主人公]\  ←※末尾の¥に注意
\           ←※空行に注意
[if exp="mp.苗字 !== void"]\
中西\
[else]\
慶介\
[endif]\
\           ←※空行に注意
[endmacro]

*start|開始
\
[主人公] は、萌えた。
とても萌えた。\
[p]
こんなカンジ。だから、実はKAG2互換モードは、かえって分かりづらいのだ。 どうしても、というのでなければ、なるべく使わないことをお勧めする。


コントロールキー長押しでメッセージスキップするには

昔の吉里吉里ではコントロールキー長押しでのメッセージスキップを実現するには system/MainWindow.tjs をつつかないといけなかったり、 拙作のプラグインを使ったりしないと いけなかった。 のだけれど、2.30 くらいから、system/config.tjsのKAGWindow_config()関数の中で

supportReadingKey = VK_CONTROL;
と定義するだけでよいようになった。 ただ、デフォルトではこの値はVK_SPACEになっていて、設定は背反なので、 スペースキーとコントロールキー両方は長押しスキップキーに指定できない。 また、拙作プラグイン では高速スキップが可能だが、コレはキーリピートにあわせたスキップしかできない。 …結局、使いやすいのは拙作プラグインじゃないの?という結論に…。


ImageMagickを上手に使う

コマンドラインで画像を色々つつけるImageMagickは、持っておくととても便利。 特に、ボタンを作る時、三つのイメージを合成したりするのとかそういうのに。 例えば、三つのイメージ(a.png,b.png,c.png)を横に並べてd.pngとしてくっつけるなら こんな。

# montage -label "" -background none -tile 3x1 -geometry +0+0 a.png b.png c.png d.png
これらのオプションが必須なのに注意。つけないとどういうことになるのか、是非 やってみて欲しい。こんなカンジでとても使いづらいのが玉に瑕。 あと、やたらとバグが多い(15年以上前から我輩いっぱいバグ報告してるのに未だに 直んなかったりエンバグしてたり)のとかがとっても困る。 作ってる人は完全に趣味プロでやってるみたい。 文句言ってても始まらないのはわかるけどさ。


cfファイルなしにオプションを有効・無効化する

例えばデバッグウィンドウを無効化したりする時は、コマンドラインオプションに -debugwin=no と書けばいい。コマンドラインで指定したくなければ、*.cf ファイルを 作成してその中に書いておけばいい。…しかし、市販ゲームとかでそんなの カッコ悪いでしょ、ということで本題。

// optsを必ず指定して自身を起動する関数
function set_cmdline_opts(opts=%[])
{
	var illegal = false;
	var optstr = "";
	for (var i = opts.count-1; i >= 0; i--) {
		optstr += " "+opts[i];
		var opt = opts[i].split("=");
		if (opt.count == 2 && System.getArgument(opt[0]) != opt[1])
			illegal = true;
	}
	// ヘンなオプションが指定されてたら、指定しなおして再起動する
	if (illegal) {
		System.shellExecute(Storages.getLocalName(System.exeName), optstr);
		System.exit();	// 自分はすぐ終わる
	}
}

// 呼び出し側。ここでは例として、-debugwin=no, -dbstyle=auto, -waitvsync=yes の
// 三つを指定している。数はいくらでもいいし、指定しなくてもよい。
set_cmdline_opts(["-debugwin=no", "-dbstyle=auto", "-waitvsync=yes"]);

…みたいな記述を、startup.tjsの頭くらいに書いておく(最初に公開したのは不十分 だったのですこし追記した)。これで、意図的に オプションを指定されても、回避できるようになる。あと、ジョイパッドを 無効にしたい人はset_cmdline_opts()に"-joypad=no"を指定すればいい。 あ、ちなみに、System.shellExecute()はすぐ戻ってくるので、二重起動にはならない。

けどまぁ、こんなの本気になられちゃうといくらでも回避策はあるので、あくまでも 「ライトユーザのライトな解析は防げるよ」という参考までに。


[iscript]〜[endscript]中のコメントの書き方に注意

いやー、久々にハマったハマった。ちょっとコメント修正したら、セーブデータの 互換性が崩れてしまった。そんなばかな、我輩どうやったら互換性保てるか ちゃんと調べたしそれに則って作業したよ!?ということでモノスゴ調べるハメに なったのだが。結局吉里吉里の(というかKAGParserの?)バグでした。 詳細はこちら。 ユーザID/Passwdは サポートページに書いてある。 ちなみに、掲示板のトップはこちら

簡単に言えばこう。[iscript]〜[endscript]の中では、 たとえコメント内であっても、*を行頭に書いてはいけない。 この言葉に尽きる。そんなのわかるか!キィッ! ネットラジオ聞きながらとはいえ、5時間も解析で無駄にしちゃったよ…。


「ハードウェア例外が発生しました」を信じるな

吉里吉里でTJSを使っていると、時々以下のようなエラーに出くわす。

00:37:02 ==== An exception occured at XMBPlugin.ks(53)[(function) finalize], VM ip = 10 ====
00:37:02 -- Disassembled VM code --
00:37:02 #(53) invalidate interpy;
00:37:02 00000006 gpd %1, %-2.*1	// *1 = (string)"interpy"
00:37:02 00000010 inv %1
00:37:02 -- Register dump --
00:37:02 %-2=(object)(object 0x0012D224:0x00000000)
00:37:02 %-1=(object)(object 0x1C751018:0x1C751018)  %0=(void)
00:37:02 %1=(object)(object 0x1DEF6AE8:0x1DEF6AE8)
00:37:02 %2=(object)(object 0x08767B04:0x00000000)
00:37:02 -----------------------------------------------------------------------------------

00:37:02 ハードウェア例外が発生しました
00:37:02 Exception : Access Violation(write access to 0x031EFD40)  at  EIP = 0x0040892B   ESP = 0x0012C864   in C:\KAICHO\HOGEHOGE\krkr.eXe
00:37:02 Context Flags : 0x0001003F [ CONTEXT_DEBUG_REGISTERS CONTEXT_FLOATING_POINT CONTEXT_SEGMENTS CONTEXT_INTEGER CONTEXT_CONTROL CONTEXT_EXTENDED_REGISTERS ]
00:37:02 Debug Registers   : 0:0x00000000  1:0x00000000  2:0x00000000  3:0x00000000  6:0x00000000  7:0x00000000  
00:37:02 Segment Registers : GS:0x0000  FS:0x003B  ES:0x0023  DS:0x0023  CS:0x001B  SS:0x0023
00:37:02 Integer Registers : EAX:0x031EFD40  EBX:0x031EFD40  ECX:0x00000000  EDX:0x00000000
00:37:02 Index Registers   : ESI:0x19A01D04  EDI:0x0012CC5C
00:37:02 Pointer Registers : EBP:0x0012CD00  ESP:0x0012CC4C  EIP:0x0040892B
(以下データダンプが続く)

これ見るとイッパツで「ああ、ハードウェアエラーか…ウチのPCどっかおかしいの かなぁ」とか思うだろう。のだが、実は99%はハードウェアエラーではない。 このメッセージは実に紛らわしいので是非変えて頂きたいところ。

注目点は以下の一行。

00:37:02 Exception : Access Violation(write access to 0x031EFD40)  at  EIP = 0x0040892B   ESP = 0x0012C864   in C:\KAICHO\HOGEHOGE\krkr.eXe

「Access Violation」。つまり、本来アクセスすべきでないメモリに アクセスしちゃったのが直接原因。普通にKAGスクリプト使ってりゃそんなことは 発生しないのだけれど、TJSでは作り方を間違うと簡単に起こる。典型的には 以下のような例で。

var layer = new Layer(kag, kag.fore.base);
invalidate layer;
dm('width = ' + layer.width);  ★ここでエラーになる

判る?invalidateで開放(無効化?)したインスタンスに、再びアクセスしようと している。このインスタンスはメモリ上から消えている(ことが多い)ので、当然 エラーになる。それこそがつまりAccess Violationなワケであるよ。

前置き長かったが、要するに、「開放したインスタンスにアクセスしようとした 場合」に、この「ハードウェア例外が発生しました」エラーが発生する、ということ (もちろん別の場合もあるが、多くはこれ)。 面倒なのは、invalidate してすぐにインスタンスが完全に無効化されない場合が あり、うまく動いてしまうことがあること、特にTimerが絡むと、タイミングの問題で 発生したりしなかったりすること。いずれにしても、TJS上の論理的な間違いで あることが極めて多い、ということだ。

上の例では、XMBPlugin.ksの53行、finalize()関数中の「invalidate interpy;」で 発生している。interpy、あるいはそれを含むインスタンスがどこかで 既に invalidate されていないかチェックすればよろしかろう。…デバッガが 無いから、こういうの探すの結構難しいんだけどね。

実は「KAGを終了する時にエラーが出る場合」も 同じ原因。興味がある人はなぜそうなるのか考えてみよう。


KAGParserインスタンスをrestore()するとハードウェア例外発生

KAGParserには store()restore() というメソッドがある。自作クラスのセーブとロードに使えるよなー、と思ってる人、 そのとおりなんであるが、時折リストア時に 上の「ハードウェアエラーが発生しました」になっちゃうことがある。 以下のコードで再現可能。

tf.a = new KAGParser();
tf.b = tf.a.store();
tf.a.restore(tf.b);  ★ここでエラーになる

いや何もやってないじゃん、newしてstore()してrestore()してるだけじゃん。 なんでこんなんでエラー出るのん。

ものスゴ長いこと悩んでたのだが、知人が2chに聞いてくれたのに答えが出なかった のをきっかけに、ええいもう!と自分で調べちゃった。 答え:吉里吉里のバグ(多分)。なんじゃーい!一度もParseしていない KAGParserインスタンスは、store()で作成される辞書配列の storageName エントリが ""(空文字)になっているが、これだとエラーになるみたい。なんでもいいから、 実在するファイルを代入すればエラーにならないことがわかった。こんなん おかしいやの!キィッ!とはいえ、「実在するファイル」を探すのが大変なので、 とりあえず storageName エントリが空文字だったらKAGParser()を再定義するように してみる。

// tf.a = new KAGParser();  // どっか先にこうやっておく
// tf.b = tf.a.store();
// 以下リストア時の操作
if (tf.b.storageName == "")
    tf.a = new KAGParser();
else
    tf.a.restore(tf.b);

これでエラーにならない。ンモー!


ゲーム画面を全画面表示・拡大縮小表示すると、マウス位置がずれる

マウスカーソルの位置に応じてメニューを表示したり、マウスカーソルを自動移動 したりしている場合に問題になるのがこれ。通常のウィンドウ表示だとどうという ことはないが、ゲーム画面を全画面表示したり、拡大縮小表示したりしていると、 ゲーム上で検知しているマウス位置と、kagインスタンスが知っているマウス位置が ズレて報告される場合がある。

具体的には、Windowクラス(→KAGWindowクラス→kagインスタンス)のイベント onMouseMove()、onMouseDown()、onMouseUp()、onMouseWheel() の 引数で渡される x, y の座標が、ゲーム画面の座標と異なる。 不思議なことに、kag.fore.base.cursorX/curosrY はその時にも 正しいゲーム画面の座標を返す。

原因は単純、「Windowクラス の onMouse*()のマウス位置は、拡大縮小された 実際の座標上で認識されているから」。 つまり、640x480ドットの画面を二倍表示していた場合、kag.onMouseDown()に渡される x が取りうる値は0〜1280、y が取りうる値は0〜960となる。 kag.setZoom()した場合、レイヤの座標なんかは拡大縮小表示しても気にせずよいように なってるけど、なぜか Window.onMouse*() の座標だけは気にしないとダメなのだった。 実際には「座標がズレる」のではなくて、「そういう動作なんだけどオレタチが それ知らなかった」わけね。

じゃぁどうやって防げばいいか。色々悩んだが、もういっそ kag.onMouse*() の x, y は無視して、常に kag.fore.base.cursorX と cursorY を参照するのが 一番簡単だという結論に。つまり、例えば KAGWindow.onMouseDown() の実装では こんな。

function onMouseEnter(x, y, shift)
{
	x = fore.base.cursorX, y = fore.base.cursorY;
	実際の処理
}

というのは、最初、以下のようにすればいいよ!と思っていた(このページでも 長いことそうやってお知らせしてた)のであるが、 これだと、「フルスクリーン時、かつ画面の左右または上下に黒帯が表示される時」 にうまいこと動かないことが判ったから。

function onMouseEnter(x, y, shift)
{
	x = int(x * scWidth  / innerWidth);
	y = int(y * scHeight / innerHeight);
	実際の処理
}

640x480 のゲーム画面を、1280x800のディスプレイにフルスクリーン表示した場合、 左右に黒帯が表示される。この時、kagの各メンバは kag.scWidth=640, kag.scHeight=480, kag.innerWidth=1280, kag.innerHeight=800 に なる。すると kag.scWidth/kag.innerWidth が kag.scHeight/kag.innerHeight と 等しくなくなり、比率がおかしくなってしまう。どっちか片方使えばいいじゃん、と 思うかもしれないが、黒帯が左右に付く場合と上下に付く場合でこれを切り替え なければならない(不可能ではないが)し、そもそもそうやっててホントに 大丈夫なのかが判断できない。フルスクリーンの時に kag.layerTop/layerLeft は 両方 0 になってるし。

じゃぁ代わりに kag.zoomDenom と kag.zoomNumer を使えばいいんじゃないか? と思ってやってみた。

function onMouseEnter(x, y, shift)
{
	var scrrate = kag.zoomNumer/kag.zoomDenom;
	x = int(x * scrrate);
	y = int(y * scrrate)
	実際の処理
}

でも、やっぱりフルスクリーンの時にダメだった。フルスクリーンの時、なぜか kag.zoomNummer と kag.zoomDenom が両方 1 に設定されてしまうから。実際には 拡大表示されてるのになんでだ!

というわけで、Window.onMouse*() で渡された x, y からゲーム座標での マウスカーソル位置を計算で求めるナイスな方法を思いつかなかったので、 もういいよ!ベースレイヤのcursorX/cursorY 使っちゃうよ! ということにするのが一番よろしいんじゃないかと思いました、という話。

いいのかなぁ。もっとトレビヤンな方法知ってる人は教えて下さい是非。


台詞をインデントしよう

シナリオ中、"「" で始まり "」" で終わる台詞、これがインデントされていないと かなりカッコ悪い。

×「アキヒト、お前ホントに頭悪いのな。昨日言ったじゃないか。お箸を持
 つのが右手だってば」

○「アキヒト、お前ホントに頭悪いのな。昨日言ったじゃないか。お箸を持
  つのが右手だってば」

手動で行頭に全角空白を入れてる人も居るけれど、幅がfixedでないフォントだと "「" と " "(全角空白) の幅が異なる場合もあり、根本解決にはならない。 だもんで、もっとエレガントにインデントする方法を考えてみる。というか このくらいは2秒で思いつこう。マクロ書けばいいんですよ!

; マクロ定義
[macro name="「"]
「[indent]
[endmacro]
[macro name="」"]
」[endindent]
[endmacro]

;シナリオスクリプト中
[「]アキヒト、お前ホントに頭悪いのな。昨日言ったじゃないか。お箸を持
つのが右手だってば[」]

"「" だけじゃなく、"『" や "(" なんかも対象にこういうマクロをひとつ作って おくと便利。


KAG2.x互換モード中でKAG3モードのスクリプトを読み込む

KAG2.x互換モード(system/config.tjsのignoreCR=false)は、スクリプト上の改行を 表示上も改行とする機能。我輩はあんま好きではないのだが、これを使ってる 人も結構いらっしゃる模様。なんだけど、我輩が提供してるのも含め、最近の プラグインなんかは殆どKAG3モード(ignoreCR=true)前提で書かれているので、 こういうプラグインをKAG2.x互換モードなスクリプト上から使うのには、 ちょっと書き換えなければならなかった。

んだけど、よく考えたらignoreCRってKAGParserのプロパティなんだから、 動的に変えちゃえばいいんじゃないの?と思ってやってみたらちゃんと動いたので ご紹介。

;<例>
; KAG3モードで書かれたSaveAnywhwere.ksとAltEnterFullscreen.ksを、
; KAG2互換モードのスクリプトから読み込むには、以下のように指定する。
;
; KAG3モードにする
@eval exp="kag.conductor.ignoreCR = true"
;
; プラグインを読み込む
@call storage=SaveAnywhere.ks
@call storage=AltEnterFullscreen.ks
;
; 元のモードに戻す(=falseとしてもよいが念のため)
@eval exp="kag.conductor.ignoreCR = global.ignoreCR"
;

おォ…おォエレガントでワンダホでトレビヤーン!プラグイン中でignoreCRを つついてるようなものには対応できないが、とりあえずは以上のように 設定することで、KAG2.x互換モードなスクリプトから、KAG3モードの プラグインとかを修正なしに読み込んで使うことが可能。内容的には 「あ、そうか」くらいの話だけど、 拙作のマクロやプラグインとか使う時にも結構お役に立てそうですよコレ。

以上をマクロ化しようと頑張ってみたんだけど、残念ながらマクロ中で ignoreCRを変えると、要らん '\' がどっからか出てくることが判ったので マクロ化は断念。まぁ…たかだか二行だけだから、手で書こうよ、うん。

ただし、この方法には制限があって、読み込むスクリプト中に以下のような [call]や[jump]の記述があるとうまくいかない。理由はわかる…よね?

; 複数効果音を同時に停止できるマクロ
; [se_stop bufs="1,2,3..."]
[macro name=se_stop]
[call storage=thisscript target=*se_stop_loop]
[endmacro]

[return]

*se_stop_loop
[eval exp="mp.seary = mp.bufs.split(/, */)"]
*se_stop_loop_loop
[return cond="mp.seary.count <= 0"]
[stopse buf="&mp.seary.pop()"]
[jump target=*se_stop_loop_loop]

まぁ、役に立たんわけではない、ということでひとつ。

重要な注意点が一つ。kag.conductor.ignoreCRはセーブされない。 そのため、セーブしてロードすると、必ずglobal.ignoreCRに戻ってしまう。 これはKAGParserの仕様で、ignoreCRは頻繁に更新するようには作られてないみたい。 従って、kag.conductore.ignoreCRを変更するときは、 それを元に戻すまでの間にセーブポイントを設置してはいけない。 プラグイン読み込みやマクロ定義であればセーブポイントは無いから大丈夫だろう。

拙作ExtKAGParserでは、0.1.3.0からignoreCRをセーブするようにした。 なので、シナリオ中でもりもり変更可能になった。はず。


既読判定がおかしくなるときは

読んだ筈の部分を読み返すと、なぜか既読でなくなってる(=AutoやSkipモードが 停止させられる)、あるいはその逆の場合、[if]〜[endif]の間にラベルを書いて しまったからであることが多い。例で示そう。 以下のようなスクリプトを実行した場合を考えてほしい。

*start
[if exp=0]
	; この部分は絶対に実行されないはず
	*skipped
	スキップされる部分[r][cm]
[endif]
*end
ラベル*skippedは[emb exp="sf['trail_first_skipped'] ? '既読' : '未読']"]です。
[s]

この実行結果、フツーは「未読です」と表示されることを期待するだろうが、実際には 必ず「既読です」が表示される。なぜか?これはもうKAGParserの仕様で、 [if]〜[endif]間のラベルは、実行されなくても「既読」としてしまうからだ。 簡単に言えば「行頭の*は全部ラベルとし、[if][endif]中であろうがなんだろうが KAGがそこを通過するとラベルを既読とする」みたくなってる。

で、絶対バグだと思ったので、ンモー報告してやるッ!と思いつつ調べたら、 KAGのタグリファレンス ifの項に、「if 〜 endif の間にはラベルを挟まないでください。」と 書いてあるというそんなオチ。それを仕様にするのは、書式の自由度が落ちすぎて キツい!
もし仕様にするなら、[if]中でラベルを発見したらエラーを出すようにしないと イカンと思う。あと、なぜダメなのかの理由詳細をちゃんとどっかに書いといて ほしい。「やっちゃダメ」と書いてあることをエラー検出せず、理由も書いてない なんてのはありえねぇ!とか思っちゃうのは、エンタープライズOS屋の 職業病なんだろうか。

もう一つ、[if]〜[endif]間に明示的にラベルを使ってなくても(?) ヘンなことが起こりうる例として、 どこでもセーブプラグインの[label]マクロを使っていた場合を考えてみよう。

; np = New Page、改ページマクロ
[macro name=np]
[p][cm][label][cm]
[endmacro]

*start
あいうえお
[np]
[if exp="条件"]
	かきくけこ
	[np]
[endif]
さしすせそ
[s]

さて、上の例で、"条件"が成立しなかった場合、画面に表示されるのは「あいうえお (改ページ)さしすせそ」、条件が成立した場合は 「あいうえお(改ページ)かきくけこ(改ページ)さしすせそ」となる。問題は生成される ラベルだ。条件が成立した場合、「さしすせそ」の部分でのラベルは *start:2、 条件が成立しなかった場合は *start:1 となることがわかるだろうか。 つまり、同じ場所なのに、ラベル名が異なるために、条件によって既読状態が 変わってしまうということだ。自動ラベルを使う場合はこういうことがよく起こる。 だから、たとえ明示的でなくても、[if]〜[endif]の間では シナリオの既読に関わるラベルを使ってはいけない。 実は[if]以外にも同様のケースはあるので注意。

結局、シナリオ分岐のために[if]〜[endif]の間にラベルを挟まないようにするには、 [jump]や[call]、cond=引数を使って表現するしかない。 念のため、シナリオ分岐の時は、[if]〜[endif]間は分岐命令のみ記述し、 シナリオテキストを一切書かない方がいい。最初の例ならこんな。 これなら*skippedは必ず未読となる。面倒くせぇ!

*start
; [jump target=*end cond="!0"] でもいいけど、まぁ例なので。
[if exp="0"]
	[jump target=*skipped]
[else]
	[jump target=*end]
[endif]

*skipped
スキップされる部分[r][cm]
; そのまま *end へ続く

*end
ラベル*skippedは[emb exp="sf['trail_first_skipped'] ? '既読' : '未読']"]です。
[s]

…ところで[if]〜[endif]の間、[call]先での既読に関わらないラベル使うのは ええんかいな。仕組み的にはいいはずだけど、他になんか弊害あるんやろか。 …だから「使っちゃダメ」の理由の詳細がほしいんだよね…。

おまけ。KAGタグリファレンス眺めたら、他にも[ignore]〜[endignore]、 [wait]〜[resetwait]、[nowait]〜[endnowait]は間にラベル書いちゃダメという 話があった。[resetwait]の部分にだけ簡単に理由が書いてあった(しかもそれは 非常に重要な情報で、[call]先でもセーブ可能ラベルは書いちゃダメということが はっきりわかった)けど、他のはどうなんやろか…。


配列と辞書、どちらが早い?

やってみたかったんじゃーい! というわけで、配列と辞書の速度比較。 実行した吉里吉里は 2.32、比較マシンはノートPC、CoreDuo U2500(1.2GHz) 1.5GB Memoryという最近にしては非力なソレで。テストしたスクリプトは以下の通り。 やってることは、10万要素の配列と辞書作って、ランダムに作った1000要素を 引っ張り出す時間を表示しているだけ。


配列作成中 ....

[iscript]
var NUM = 100000;
var ary = [];
var hash = %[];

// 配列作成
var now = System.getTickCount();
for (var i = 0; i < NUM; i++) {
	var elm = "hash_number_is_" + "%06d".sprintf(i);
	ary[i] = elm;
}
tf.arytime = (System.getTickCount() - now);

// 辞書作成
var now = System.getTickCount();
for (var i = 0; i < NUM; i++) {
	var elm = "hash_number_is_" + "%06d".sprintf(i);
	hash[elm] = 1;
}
tf.hashtime = (System.getTickCount() - now);

[endscript]

完了。[r]
ary作成時間は[emb exp=tf.arytime]msです。[r]
hash作成時間は[emb exp=tf.hashtime]msです。[r]

[iscript]

// 検索対象をランダムに作成
var chknums = [];
var CHKSIZ = 1000;
for (var i = 0; i < CHKSIZ; i++)
	chknums[i] = intrandom(0, NUM*2-1);
	// *2 して「ヒットしないもの」も探す

// 配列検索時間測定
var now = System.getTickCount();
for (var i = 0; i < CHKSIZ; i++)
	ary.find("hash_number_is_" + "%06d".sprintf(chknums[i]));
tf.arytime = (System.getTickCount() - now);

// 辞書検索時間測定
var now = System.getTickCount();
for (var i = 0; i < CHKSIZ; i++)
	hash["hash_number_is_" + "%06d".sprintf(chknums[i])];
tf.hashtime = (System.getTickCount() - now);
[endscript]

ary検索時間は[emb exp=tf.arytime]msです。[r]
hash検索時間は[emb exp=tf.hashtime]msです。
[s]

結果は以下の通り。まぁアタリマエといえばアタリマエだけど。

配列作成中 ...完了。
ary作成時間は388msです。
hash作成時間は33558msです。
ary検索時間は5msです。
hash検索時間は198msです。

やっぱ辞書の方が遅いんだね。予想通りで満足満足。ちうか2桁違うというのは あんまりじゃないかコレ。
ただ、CHKNUMを増加させても結果は必ずしも線形には増加しない。NUMを増加させると 辞書の作成に死ぬほど時間がかかる。チュウイ。あくまで参考値ということでひとつ。

それにしても、(要素数が増えると)辞書作るのは抜きん出て遅いね! 配列は人間が意識できないくらい早いのに。 だからコレとかが起こるんだよね。

2013/03/12追記。 更に詳細に調査してくれた方を発見したので。

おォ…おォスバらしい!全部論理的に繋がったよ!あと上のテストプログラムは 一部よくなかったのですな!勉強になった!ありがとうございます ありがとうございます!
…んだけど、ソースコード見ずにソレを知るのは無理だし、結局 吉里吉里本体をリコンパイルしなきゃどうしようもないので、回避策としては (highSpeedLabelerみたく)手動で辞書分割くらいしか対策がなさそうなのが アレですなぁ。…ハッシュテーブルの初期サイズが8って…。

ちなみに、上のテストプログラムで、最初に hash = new Distionary(16);で 初期要素数を指定する(※吉里吉里2.32では不可、開発版の吉里吉里2でのみ使用できる) と、hash作成時間が5倍に、hash検索時間が2倍になる。思ったより効果あるなぁ。

2013/11/23追記。 吉里吉里Zで、 ハッシュテーブルサイズの初期値を大きくした場合の速度を計測してくれている。 ここでの結論は「あんま早くならないから初期値変えない」となった模様。 この結果なら我輩だったら絶対初期値変えるけどなぁ。理由は以下の通り、 デメリットがなくメリットがある、というところ。

  1. 初期値を大きく(BIT3→BIT5)しても、メモリ消費量はほぼ変わらない
    ※多くの場合、もともとハッシュサイズは初期値で不足している可能性がある
  2. そのとき、ある処理は532.4→482.6と10%「も」高速化される
    ※Layerのメンバ数は高々300くらいなので、もともとハッシュに繋がれる リストが短く、そんなに性能に大きな影響はなさそう。
  3. 既に存在するアプリケーション(TJSスクリプト)を全く変更することなく高速化が望める
  4. 最低性能が飛躍的に向上できる
特に3.と4.。3.では、Dictionaryの初期値を指定すれば…とかsaveStructで バイナリモードを使えば…というのは、 たった一行たりとも既存の資産の書き換えが生じるし、 「どこどう変えればいいんだっけ」という無駄な調査が必要になる。 4.は、ハッシュ+配列での検索込みの作成はO(n^2)だから、最低ケースで 計算量を1/k^2へ低減することが望める。これは大きい。

仕方ないので、コンパイル環境揃ったら自前でbuildすることにする…。 ファイル読み込み時のsigcheck機能も追加したいしね。

こういうのを見ると、エンタープライズとコンシューマーの、 「極端なケースをどう捉えるか」という思想の差が判って面白い。 業務プログラムでは、最低性能向上は至上命題。 我輩が仕事で関わるプログラムは全て「極端なケース」をクリアしたモノ達で、 時にはそのためにピーク性能を下げることも厭わない。…あー、でも、そういうのにも もう疲れてきたなー…。

2014/06/01追記。じゃぁ辞書に初期値を与えると早くなるのか、という観点で、 以下のスクリプトを動かしてみる。初期値8、16、32、64、100000にて、100000要素の 辞書を作成してみるというそんな。

[nowait]

[eval exp="tf.init=8, tf.num=100000"]
[call target=*createdic]
[eval exp="tf.init=16, tf.num=100000"]
[call target=*createdic]
[eval exp="tf.init=32, tf.num=100000"]
[call target=*createdic]
[eval exp="tf.init=64, tf.num=100000"]
[call target=*createdic]
[eval exp="tf.init=100000, tf.num=100000"]
[call target=*createdic]

[s]

*createdic
辞書作成中(hashinit=[emb exp="tf.init"]) ... 
[iscript]
var hash = new Dictionary(+tf.init);
var now = System.getTickCount();
for (var i = 0; i < tf.num; i++) {
	var elm = "hash_number_is_" + "%06d".sprintf(i);
	hash[elm] = 1;
}
tf.hashtime = System.getTickCount() - now;
invalidate hash;
[endscript]
[emb exp=tf.hashtime] ms[r]
[return]

結果(Win7Pro(64bit), Core i3 2120T(2.6GHz), Mem8GB)。圧倒的ではないかわが軍は!

辞書作成中(hashinit=8) ... 3799 ms
辞書作成中(hashinit=16) ...2086 ms
辞書作成中(hashinit=32) ... 1251 ms
辞書作成中(hashinit=64) ... 764 ms
辞書作成中(hashinit=100000) ... 170 ms

やっぱり、初期値に応じて大きく速度が変わる。Core i3でコレだから、Core2Duoだと もっと激しい時間になるだろう。これが10000要素だとあんま変わらない。 数が多くなるとハッシュ値が重複して指数関数的に手間が増えるというアレだ。 この100000という値には根拠があって、我輩が関わったゲームの、(highSpeedLabelerを 使わなかった場合の)多めのラベル数がそのくらいだったから、というところ。 下手すると200000なんてのもありえるんじゃなかろうか。 「最低性能を上げたい」という欲求の根拠はこのあたりにある。

…まぁ、今はもうhighSpeedLabeler作っちゃったので、よっぽどヘンなスクリプト 書かなきゃ問題になることはなくなったんだけどね。

おまけ。要素数200000の時の結果。ワーォ。

辞書作成中(hashinit=8) ... 27598 ms
辞書作成中(hashinit=16) ... 15109 ms
辞書作成中(hashinit=32) ... 7962 ms
辞書作成中(hashinit=64) ... 4451 ms
辞書作成中(hashinit=200000) ... 344 ms


KAGPluginを組み込む時の注意点

KAGPluginを組み込む時は、first.ksの、「最初のセーブ可能ラベルより前」で 組み込むようにしよう。そうしないと色々不具合が起こるため。
※本件は掲示板でご指摘頂いたためここに書き下した次第。指摘してくれた方、 ありがとうございました

KAGPluginは、通常 [call storage="プラグイン.ks"] のように組み込む。 実際には、この中では二重読み込みチェック、KAGPluginクラスの 派生クラスのインスタンス作成、それのKAGへの登録、などを実行している。 なんでKAGPluginなんてものがあるのかというと、KAGの画面遷移・save/load時に、 能動的にそれにあわせた動作をプラグイン側で実行するため(KAGが自動的に 呼び出してくれる)。

さてここで、以下のようなプラグイン KAGPluginTest と、first.ks内の それの組み込み箇所を考えてみる。

KAGPluginTest.ks
; 二重読み込みを防止
[return cond="global.KAGPluginTest_obj !== void"]

[iscript]

// クラス定義
class KAGPluginTest extends KAGPlugin {
	var data = 0;

	// コンストラクタ
	function KAGPluginTest(win)
	{
		super.KAGPlugin(...);
	}

	// デストラクタ
	function finalize()
	{
		super.finalize(...);
	}

	// オプション設定
	fuction setOptions(elm)
	{
		data = elm.data if (elm.data !== void);
	}

	// セーブ時に呼ばれる
	function onStore(f, elm)
	{
		var dic = f.KAGPluginTest;
		dic.data = data;
	}

	// ロード時に呼ばれる
	function onRestore(f, clear, elm)
	{
		var dic = f.KAGPluginTest;
		data = dic.data;
	}
}
// 実際にkagにプラグイン組み込み
global.KAGPluginTest_obj = new KAGPluginTest(kag);
kag.addPlugin(global.KAGPluginTest_obj);

[endscript]

[return]

first.ksでのKAGPluginの間違った組み込み方
*start|開始
; 「最初に戻る」を有効にする
[startanchor]

; プラグイン読み込み
[call storage=KAGPluginTest.ks]

(省略)

さて、この組み込み方だと、「最初に戻る」で最初に戻ると、KAGPluginTest クラス中、onRestore()でエラーになる。何故かわかるかなー?

  1. 一番最初に実行するとき、*start ではまだプラグインが定義されていないので プラグイン周りのデータは何もセーブされない
  2. プラグインが読み込まれる
  3. 「最初に戻る」を実行すると、*start まで戻ってくる。 この時、sf.* や f.* は *start 時のものに戻るが、global.* は戻らない
  4. KAGにはKAGPluginTest_objが登録されているので、onRestore() 関数が呼ばれる
  5. f.KAGPluginTest.data をアクセスしようとして、f.* は *start通過時の状態に 戻っている(=f.KAGPluginTestが定義されていない)ので、ここでエラーになる
ややこしいが、判ったろうか。だから、セーブ・ロードされる「後」で KAGプラグインを読み込んではイカンのだ。

「いやいや、だったら f.KAGPluginTest が void かどうか、onRestore() 中で 判断すりゃいいじゃん」と思うキミはまだまだ青い。真っ青だ。 確かにそうすればエラーにはならないが、 KAGPluginTest_obj.setOptions()で内部のデータ data を変更していた場合、 「最初に戻る」でその時のデータに戻らなくなる。最初に戻った はずなのに変数の一部が最初に戻ってくれないわけだ。これは根が深い バグに発展しうる。これは、KAGPluginTest.ks 先頭で二重読み込みを禁止して いることに起因する。

「じゃぁ単純に、二重読み込み禁止してる[return ...]行をコメントアウトすれば いいの?」などと聞くキミは、もうアメリカンチョコレートばりに大味に甘い。 甘すぎる。 そんなことをしたら kag.addPlugin()が二回呼ばれて、「同じ動作をする プラグインが二つ登録されてしまう」。 もっとよくモノを考えてから発言したまえよキミィ!

同じ理由で、Config.tjs中で saveMacros(マクロをセーブする) が true に 指定されていた場合、「最初に戻る」でプラグイン中で定義するマクロが 全部定義されていないことになり、「マクロ xxx が定義されていません」という エラーが表示されるだろう。これは以下のロジックで起こる。

  1. 一番最初に実行するとき、*start ではまだプラグインが定義されていないので プラグイン中で定義するマクロも「存在しない」状態でセーブされる。
  2. プラグインが読み込まれる
  3. 「最初に戻る」を実行すると、*start まで戻ってくる。 この時、マクロも「存在しない」状態に戻る。
  4. プラグインを再登録しようとするが、既にKAGPluginTest_objが登録されている のでマクロを定義しない
  5. 実際にマクロを使おうとする時、定義されていないのでエラーになる

結局、こういうのを許すようにするには、「プラグイン中で、二度読みされたら 全体を初期化するように作る」しかない。しかしそれはさすがに面倒だし、 既にそういう作り方になっていないKAGPluginが多数存在する。

だから、こういうのを回避するために、first.ks の、最初のセーブ可能ラベルより 「前」にKAGPluginを全部読んでおくことが大切なのだ。我輩は、loadPlugins.ks というプラグイン読むためだけのファイルを作って、そこでKAG/TJS含めて プラグインを全て読み込むようにしている。こうすれば first.ks も短くなるしね。

first.ksでのKAGPluginの正しい組み込み方
; プラグイン読み込み
[call storage=KAGPluginTest.ks]

*start|開始
; 「最初に戻る」を有効にする
[startanchor]

(省略)


KAGのタグをTJSから使う

TJSをもりもり書いてると、KAGのタグをTJSから使いたくなるときが時々ある。 一番簡単なのは、辞書配列 kag.taghandlers に登録されているKAGデフォルトの タグのTJS関数を直接呼び出しちゃうことだ。[layopt]だったらこんなカンジ。

kag.tagHandlers.layopt(%[layer:1, page:'fore', visible:true]);

ただ、これだと実行できるのはKAGのデフォルトタグだけで、登録してるマクロとかは 実行できない。そういうのに対応するTJS関数 kageval() は、こんなカンジに書く。

function kageval(kagscript)
{
        var tmp = kag.onConductorScenarioLoad;
        kag.onConductorScenarioLoad = function(name){ return name; };
        kag.process("\n"+kagscript, "");
        kag.onConductorScenarioLoad = tmp;
}

簡単だよね。ただ、いずれにしても[wt]とか[wait]とか「待つ」動作はできないので 注意。なぜかって問われたら、それはもうそういうものだから、としか言えない…。


マウスのボタン状態を得る

キーボードのキー状態は System.getKeyState()で得られる。マウスの状態も得られりゃいいのに、と 思ってたら、なんのことはない、 仮想キーコード一覧見たら、VK_LBUTTON/VK_RBUTTON/VK_MBUTTON で マウスの左、右、中の3つのボタンは状態が得られるらしい。 PADについては注意事項書いてあるんだから、マウスについても書いといて ほしかった…。getKeyStateって名前でマウスのボタンまで得られるんだぜー とか誰が気付くというのか(普通気付きます)。 というか、コメントでいいので、どのキーかをちゃんと書いておいて 欲しいよね。VK_ATTNとかVK_CRSELとかVK_PA1とか、一体何のキーなんだか。

今までは、マウスのどのボタンが押されたかを調べるためだけに、onMouseDown()と onMouseUp()をフックしてたのはヒミツ中のヒミツ。恥ずかしいからこっち見ないで!


なぜ辞書配列にassignメソッドが存在しないのか

辞書配列の変数を定義しても、assign()とかassignStruct()とかのメソッドは定義 されていない。仕方ないからincontextofを使って以下のように書かないといけない。 スゲー面倒。

var dic = %[];
(Dictionary.assign incontextof dic)(dic2);

なんで?と思う人もいるだろうから簡単に理由を。最初からassign()とかが使える ようになってると、「assignとかが辞書配列の予約メンバとして定義されちゃう」 から。つまり、↓のように定義するのと同等、ということ。

var dic = %[];
dic.assign = Dictionary.assign incontextof dic;
dic.assign(dic2);

…でも、やっぱり最初から定義されてるほうがうれしいなぁ、ということで、 なんかこう、定義するための関数とか作っといた方がいいんじゃない? keysとかisEmptyとか、定義したメンバを除いて考えないといけないのが面倒。 まぁ、…だからこそ、最初辞書配列にはメンバは定義されていないのだ、と 理解したまえよ。

function newDic(init = %[])
{
	var dic = %[];
	dic.assign       = Dictionary.assign       incontextof dic;
	dic.assignStruct = Dictionary.assignStruct incontextof dic;
	dic.saveStruct   = Dictionary.saveStruct   incontextof dic;
	dic.clear        = Dictionary.clear        incontextof dic;
	dic.keys = function () {
		var ary = [], ret = [];
		ary.assign(this);
		for (var i = 0; i < ary.count; i+=2)
			if (ary[i] !== "assign" && ary[i] !== "assignStruct" &&
			    ary[i] !== "clear"  && ary[i] !== "saveStruct"   &&
			    ary[i] !== "keys"   && ary[i] !== "isEmpty")
				ret.add(ary[i]);
		return ret;
	} incontextof dic;
	dic.isEmpty = function () {
		return this.keys().count == 0;
	} incontextof dic;

	dic.assignStruct(init);
	return dic;
}

; 以下のように使う
var dic = newDic(%[aa:1, bb:2]);
dic.assign(dic2);


Windows8上でKAGの[quake]を使うと画面が乱れる

我輩はWin8を持ってないので確認できないのだが、 そうらしい。 KAGの[quake]はkag.setLayerPos(x,y)でWindowに表示するレイヤの位置を変更して [quake]を実現しているのだが、コレを使った時に、描画領域外に描画された部分が 残ってしまうのが原因。だから、フルスクリーンモード以外では起こらない。 ソースコードを見ると、本来クリッピングされるべきじゃないかなぁと思うのだが、 はてそのように動くのが意図した動作なのかはよくわかんなかった。

さておき、KAGで回避する方法はない。だってプラットフォームの問題 だもの…。kag.{fore|back}.base.setPos()でプライマリレイヤを 動かせば同じ効果が得られるじゃん!と思ったアナタは偉いけれど、残念ながら プライマリレイヤを動かすことはできない(動かすと実行時エラーとなる)のですよ。 最初、kag.{fore|back}.layersだけを対象にして、以下みたいなスクリプトを最初に えいって動かせばなんとかなるかなぁと思ったんだけど、コレだと、レイヤが [move]とかで動かされてるとうまいこといかないことに気付いてしまった。あと、 kag.{fore|back}.baseを親レイヤとする自作レイヤも動かないしね。

kag.setLayerPos = function(x, y) {
	for (var i = fore.layers.count-1; i >= 0; i--)
		fore.layers[i].setPos(x, y);
	for (var i = back.layers.count-1; i >= 0; i--)
		fore.layers[i].setPos(x, y);
} incontextof kag;

なので、もう無視するのが一番いいんじゃないかしら。動作上は問題ないし。 とか言えるのは我輩が同人ベースだからなんだろうけどね。 商業の人はそういうの許されない…のか?(その前に致命的な問題が残った作品が しれっとリリースされてる現実がゴニョゴニョ)。

KAGEXではこの問題が起こらない。それは、KAGEXが[quake]を実現するために kag.setLayerPos()を利用していないから。代わりに、kag.{fore|back}.base.setPos() を利用している。え、上で使えないってゆったやん!?というアナタのために 解説すると、KAGEXではkag.{fore|back}.baseの下にプライマリレイヤを定義 していて、kag.{fore|back}.baseはその子レイヤ(=プライマリレイヤではない)と なっているので、kag.{fore|back}.base.setPos()が使えるのだった。

KAGEXユーザの人に多いのだが、「だからKAGはダメなんだ!KAGEXを使えって ゆってるやん!」と鬼の首取ったように言う人がいる。いやね、 それはKAGEXが(多分[quake]時にメッセージレイヤを揺らさないようにするために) 施した変更の副次的効果という偶然であって、最初からソレを見越してた ワケじゃないですやん。根本原因から目を逸らして何かを正当化しちゃダメ。 そういう感情的な話は、技術的な課題を隠しちゃうからね。こういうの、 日本の三下SIerに多いんですよな。自戒自戒。

回避策として、KAGEXのようにプライマリレイヤを別に作っちゃうのはアリだと思う。 実際、開発版KAGにはそういうブランチがある(変更部分は こちら)。 ただ、今のKAGは(拙作プラグイン含めて)プライマリレイヤ = kag.fore.baseである ことを前提に作っているものがあって、そういうのの対応手間を考えたら、諦めるのが リーズナブルかなー、と思う。あと吉里吉里かWin8側で なんとかしてくれることを期待。

「フルスクリーン切り替え方法」を "ddraw"から"cds"に変更すると防げるのだそう。 やっぱりこんなのOS側(今回はDirectDraw側か)でなんとかして欲しいもんですな。


吉里吉里リリーサがアプリケーションエラーになる

なんでやねーん!と憤ることしきりだが、そういうことがある。理由は簡単、 「krkrrel.exeはkrkr.exeを利用するのに、krkr.exeが知ったところにないから」。 krkrrel.exeを移動していないか?まずそれを確認してみよう。 どうしてもわかんなかったら、どっかからkrkr.exeを持ってきてkrkrrel.exeと 同じディレクトリにコピーしてみる。これでうまくいかなかったら別の問題。

ちうかなんでアプリケーションエラーやのん。エラーダイアログ表示してくれれば こんなに悩まなかったのに!(恨み節)


セーブデータの中を読みたい

セーブデータの形式は、system/config.tjsの saveDataMode= で指定する。 普通ゲームとしてリリースするときは 'c'(暗号化) または 'z'(圧縮)を指定する。 でも、開発中はセーブデータを見てチェックしたいこともあるよなー、という人は、 開発中だけは ''(空文字を指定)すると吉。これにより、セーブデータは テキスト(UTF-8)でセーブされる。メモ帳とかでも中身を見れるはず。 案外知らない人がいるので、メモ代わりに書いとくよ!

リリース時に'c'や'z'(または、使えるなら'b')に戻すのを忘れずに。 忘れるとユーザがセーブデータ見放題つつき放題になるので、…まぁ ソレもアリといえばアリかなぁ…。


プロパティはプロパティしかオーバーライドできない

親クラスのメンバ変数を子クラスでプロパティとして定義して、中でごそごそ やるのを隠蔽したいことがあるだろう。たとえば以下のように、class Bでは 親のval+1を管理する、ような。

class A {
	var val;
}

class B extends A {
	function B()
	{
		dm('val = ' + val);
	}
	property val {
		getter {
			return super.val+1; ★ここでエラーになる
		}
		setter(x) {
			super.val-1 = x;
		}
	}
}

tf.b = new B();

しかし、このスクリプトは、★印のところで「メンバ "val" が見つかりません」という エラーになる。理由はタイトルの通り、class Aでメンバ変数として定義されたvalを、 classBではプロパティで再定義しようとしているから。これ、できてしかるべきだと 思うんだけど…。なんでそんな制限があるのかは よくわからないが、今はそういうものらしい…。だから、class Aでもvalを スゴい単純なプロパティとして定義すれば、このスクリプトはエラーにならない。 困るなぁ!


一般的に、セーブラベルをどこに配置するか

アドベンチャーゲームを作る際、一画面ごとにセーブするなら、そのセーブ場所を 一画面を構成するスクリプトのどこに配置するかは知っておいたほうがいい。 一画面は、「テキスト画面をクリアし、立ち絵を表示し、本文を書き、最後に クリック待ちする」、というシーケンスで表示されるとした場合、 マジメにセーブラベルを手で(あるいは自動で)配置するなら、 セーブ場所は、ずばり下の位置がベスト。

(あれば場面転換=背景変更)
[立ち絵表示]
*セーブラベル|セーブ場所
[テキスト画面クリア]
(必要なら音声再生開始)
本文表示
(必要なら音声再生待ち)
[クリック待ち]
[テキスト画面クリア]

(以下繰り返し)

何故か。以下の条件を満たすためだ。

とはいえ、我輩は面倒になっちゃったので、最後の「音声再生待ち」「クリック待ち」 「テキスト画面クリア」を一つのマクロにして、その末尾にどこでもセーブ プラグインとかを組み込むことが多い。同人レベルだったらそれでも十分だと思う。


HttpRequest.dllが思ったように動いてくれない

パッチの自動ダウンロードとWebPayへの課金プラグイン作ろっとー、と思って 色々試していたら、なんかいくつも不具合があった。主なものは以下の三つ。

  1. Basic認証が通らない
  2. status=201の時に結果が得られない(WebPayはtoken作成時200じゃなくて201を返す)
  3. 21MBを超える大きさのファイルを入手しようとすると、onProgress()コールバックのパーセントが0〜100にならない

全部HttpRequest.dllのバグだったので、修正したものを置いておくソースコードへのパッチはこちら。 こんなのが残ってるなんて、誰もチェックしてないのかなぁ…。

「TJSに挑戦!」の最後が尻切れ なのは、実はこのバグに引っかかったからじゃなかろうか、 などと邪推しちゃう我輩であった。まる。


[waittrig]が予期せず終わることがある

[waittrig]タグで指定したトリガを待っているとき、全然別の契機で[waittrig]から 戻ってきて処理が先に進んでしまうことがある。典型的には、画面上のシステム ボタンから kag.closeByScript(%[ask:false]) を実行し、[いいえ]を選択した時。

これは、以下の条件が重なった場合に起こる。

  1. KAG上で [waittrig name=あるトリガ] でトリガ待ち状態である
  2. その状態で、TJSから kag.conductor.wait(%[別トリガ:xxx]) を実行

実は、conductorのwait()(=[waittrig]で呼び出される)は、 唯一のトリガのみを待つ。複数登録することはできず、あとから登録したほうで 上書きされる。だから、 [waittrig] 中に kag.closeByScript()を実行する(=not_closedトリガを待つ)と、 [waittrig]で待っていたトリガはnot_closedトリガに置換されてしまい、 [いいえ]を選択すると [waittrig] が戻ってきてしまうのだった。

待ってたトリガーが終わったかどうかを [waittrig]タグの後に 判別できればいいが、現在ではその方法は用意されていない。 なんとかするには、system/Conductor.tjs の Conductor クラスの中で、 wait() と trigger() を複数のトリガを許容するように変更すればよい。 ご要望が多ければそういうプラグインを書くかも。

これがバグか仕様かは、作った人に聞かないと判らない。同じコンダクタ中で conductor.wait()が複数回呼ばれる可能性があることを考えると、 バグに近い気はするけれど。