KAGEX拡張例(自動検索パスの追加)

KAGではカスタマイズ用にOverride.tjsとAfterinit.tjsが用意されています。

KAGEXではこれに加えてStorages.tjsがあります。これは独自の検索パスを追加するためのスクリプトです。初期状態では存在しないので作成してください。

以下の例ではseフォルダとscenario/openingフォルダを検索パスに追加しています。

// Storages.tjs
Storages.addAutoPath("se/"); // seフォルダを追加
Storages.addAutoPath("scenario/opening/"); // scenario/openingフォルダを追加

KAGEXの初期状態で使えるフォルダ名はbgimage, bgm, evimage, face, fgimage, image, init, main, others, rule, scenario, sound, sysscn, system, thum, uipsd, video, voiceになります。それ以外のフォルダを使ったり、フォルダの中にフォルダを作りたいときはStorages.tjsで追加します。

KAGEX拡張例(onSoundPropertyChanged)

・前回まで
KAGEX拡張例(音量設定)
KAGEX拡張例(ミュート設定)
KAGEX拡張例(システム効果音)

BGMの音量、またはミュートの状態が変更された際は、onSoundPropertyChangedというフックが呼ばれます。kag.addHookで呼び出す関数を追加できます。大体の動作は以下を見てください。

// 音量などが変更された際に呼ばれる関数を登録
kag.addHook("onSoundPropertyChanged", function(tag, prop, value) {
    switch (tag) {
    case "bgm":
        if (prop === "enable") {
            // BGMのミュートが変更された(value => true or false)
        } else if (prop === "globalVolume") {
            // BGMの音量が変更された(value => 0~100000)
        }
        break;
    case "se":
        if (prop === "enable") {
            // 効果音のミュートが変更された(value => true or false)
        } else if (prop === "globalVolume") {
            // 効果音の音量が変更された(value => 0~100000)
        }
        break;
    case "sysse":
        if (prop === "enable") {
            // システム効果音のミュートが変更された(value => true or false)
        } else if (prop === "globalVolume") {
            // システム効果音の音量が変更された(value => 0~100000)
        }
        break;
    case "movie":
        if (prop === "enable") {
            // 動画のミュートが変更された(value => true or false)
        } else if (prop === "globalVolume") {
            // 動画の音量が変更された(value => 0~100000)
        }
        break;
    case "voice":
        if (prop === "enable") {
            // キャラ共通のミュートが変更された(value => true or false)
        } else if (prop === "globalVolume") {
            // キャラ共通の音量が変更された(value => 0~100000)
        } else if (prop === "しおり") {
            // キャラごとの音量(しおり)が変更された(value => 0~100000)
        }
        break;
    case "bgv":
        if (prop === "enable") {
            // BGV(bvoice)のミュートが変更された(value => true or false)
        } else if (prop === "globalVolume") {
            // BGV(bvoice)の音量が変更された(value => 0~100000)
        }
        break;
    case "bgv2":
        if (prop === "enable") {
            // BGV(hvoice)のミュートが変更された(value => true or false)
        } else if (prop === "globalVolume") {
            // BGV(hvoice)の音量が変更された(value => 0~100000)
        }
        break;
    case "voiceon":
        if (prop === "しおり") {
            // キャラごとのミュート(しおり)が変更された(value => true or false)
        }
        break;
    }
});

tagには”bgm”, “se”など何の値が変更されたかが渡されます。

propが”enable”ならミュートの状態、”globalVolume”なら音量が変更されたと言うことです。prop==”enable”ならvalueは通常の状態ならtrue, ミュートされた状態ならfalseになります。prop==”globalVolume”ならvalueは変更後の音量ですが、0~100000の数値になるので注意してください。value/1000のように1000で割れば0~100に変換できます。

tag==”voice”の時は少し特殊です。propが”enable”,”globalVolume”の他にキャラ名になることがあります。このときはそのキャラの音量が変更されたということです。

キャラごとのミュートはtag==”voiceon”で渡されます。propがキャラ名になります。

全体の音量(wavevolume)やミュート(waveenable)が変更されたときはonSoundPropertyChangedが呼ばれないようになっています。それも必要なときは少し改造する必要があります。
MainWindow.tjsでwavevolumeとwaveenableが定義されている部分を書き換えます。setterの最後にcallHook(~)の行を追加しただけです。これでtag==”wave”でprop==”enable”またはprop==”globalVolume”として変更が通知されるようになります。

/**
 * グローバル WaveSound 音量の設定
 * 100 段階設定
 */
property wavevolume {
    getter()  {
        return scflags.waveVolume !== void ? +scflags.waveVolume : global.WaveSoundBuffer.globalVolume / 1000;
    }
    setter(v) {
        v = +v;
        if      (v < 0)   v = 0;
        else if (v > 100) v = 100;
        scflags.waveVolume = v;
        global.WaveSoundBuffer.globalVolume = scflags.waveEnable ? v * 1000 : 0;
        callHook("onSoundPropertyChanged", "wave", "globalVolume", v * 1000);
    }
}

/**
 * グローバル WaveSound の有効値の設定
 * true / false
 */
property waveenable {
    getter() {
        return scflags.waveEnable !== void ? +scflags.waveEnable : true;
    }
    setter(v) {
        scflags.waveEnable = +v;
        wavevolume = scflags.waveVolume;
        callHook("onSoundPropertyChanged", "wave", "enable", !!v);
    }
}

KAGEX拡張例(システム効果音)

・前回まで
KAGEX拡張例(音量設定)
KAGEX拡張例(ミュート設定)

kag.sameSysSEVolume=falseの状態ではゲーム中の効果音とシステム効果音の音量を別々に設定できます。Config.tjsの効果音バッファの数を設定している部分で設定するのが良いと思います。

// ◆ 利用可能な効果音バッファの数
// 利用可能な効果音バッファの最大値を指定します。つまり、ここで指定した数の
// 分だけ効果音を同時に再生できます。効果音を使用しない場合は 0 を指定して
// かまいません。
;numSEBuffers = 3;
;numSysSEBuffers = 2;
;sameSysSEVolume = false;

numSysSEBuffersとsameSysSEVolumeの設定を追加しました。numSysSEBuffersはシステム効果音のバッファの数です。sameSysSEVolume = falseで音量を別にしています。

デフォルトのsameSysSEVolume = trueの状態でも、音量が共通になるだけでシステム効果音が使えないわけではありません。

システム効果音のバッファは通常の効果音の後ろに追加される形になります。今回の例だとバッファ番号0~2が通常の効果音バッファ、バッファ番号3~4がシステム効果音のバッファとなります。

バッファ番号を指定する際にシステム効果音の番号を指定すればシステム効果音として再生されます。

@linemode mode=none
@nowait
@link clickse=seテスト clicksebuf=4
テスト
@endlink
@endnowait
@s

4番のバッファなのでクリックするたびにシステム効果音として再生されます。

また、システム効果音はワールド拡張では効果音として使用されません。例えば[seテスト]とするとseテスト.oggが適当な空いているバッファで再生されますが、システム効果音のバッファが使われることはありません。[seテスト buf=4]のように直接指定しても使えません。システム効果音を再生したいときはtjsを使ってください。

; 4番のバッファでseテスト.oggを再生
@eval exp="kag.se[4].play(%[ storage:'seテスト' ])"

KAGEX拡張例(ミュート設定)

・前回まで
KAGEX拡張例(音量設定)

KAGEXでは0~100の音量に加えて、オン/オフの設定もあります。

kag.waveenable ゲーム全体のミュート
kag.bgmenable BGMミュート
kag.seenable 効果音ミュート
kag.sysseenable システム効果音ミュート
kag.sameSysSEVolume=falseの時のみ
kag.movieAudioEnable ムービーミュート
kag.independentMovieAudioProperty=trueの時のみ
kag.voiceenable キャラ共通のボイスミュート
kag.bgvenable キャラ共通のBGV(bvoice)ミュート
kag.bgv2enable キャラ共通のBGV(hvoice)ミュート
kag.setVoiceOn(名前, 設定) キャラごとのミュートを設定する関数
kag.getVoiceOn(名前) キャラごとのミュートを得る関数

・全てtrueまたはfalseを設定します。デフォルトではtrueです。falseにすると対象の音がミュートされます。

以下は前回のスライダの例にミュート用のチェックボックスを追加した例です。

; スライダとチェックボックスで音量を設定する使用例
@iscript
// しおりのキャラ音量のプロパティ
property voiceVolumeShiori {
  setter(value) { kag.setVoiceVolume("しおり", value); }
  getter() { return kag.getVoiceVolume("しおり", true); }
}
// しおりのキャラミュートのプロパティ
property voiceEnableShiori {
	setter(value) { kag.setVoiceOn("しおり", value); }
	getter() { return kag.getVoiceOn("しおり"); }
}
@endscript

@linemode mode=none
@backlay
@nowait

; 操作対象を message1/back に設定
@current layer=message1 page=back

; メッセージレイヤの大きさを調整
@position left=0 top=0 width=800 height=600 visible


; BGM音量スライダを追加
@locate x=0 y=0
BGM
@locate x=120 y=0
@slider width=200 height=30 min=0 max=100 value=kag.bgmvolume jumpmode
@locate x=330 y=0
@checkbox onchange=kag.bgmenable name=kag.bgmenable

; 効果音音量スライダを追加
@locate x=0 y=80
効果音
@locate x=120 y=80
@slider width=200 height=30 min=0 max=100 value=kag.sevolume jumpmode
@locate x=330 y=80
@checkbox onchange=kag.seenable name=kag.seenable

; ボイス音量スライダを追加
@locate x=0 y=160
ボイス
@locate x=120 y=160
@slider width=200 height=30 min=0 max=100 value=kag.voicevolume jumpmode
@locate x=330 y=160
@checkbox onchange=kag.voiceenable name=kag.voiceenable

; キャラ音量(しおり)スライダを追加
@locate x=0 y=240
しおり
@locate x=120 y=240
@slider width=200 height=30 min=0 max=100 value=voiceVolumeShiori jumpmode
@locate x=330 y=240
@checkbox onchange=voiceEnableShiori name=voiceEnableShiori

; 操作対象を戻す
@current layer=message0 page=fore

@endnowait

; トランジションで表示
@trans method=crossfade time=1000
@wt
@s

KAGEX拡張例(音量設定)

KAGEXのサウンドはBGM、ゲーム効果音、システム効果音、ボイス、BGVの5種類に大別できます。
その音量はプロパティまたは関数で設定できます。

kag.wavevolume ゲーム全体のマスター音量
kag.bgmvolume BGM音量
kag.sevolume 効果音音量
kag.syssevolume システム効果音音量
kag.sameSysSEVolume=falseの時のみ
kag.movieAudioVolume ムービー音量
kag.independentMovieAudioProperty=trueの時のみ
kag.voicevolume キャラ共通のボイス音量
kag.bgvvolume キャラ共通のBGV(bvoice)音量
kag.bgv2volume キャラ共通のBGV(hvoice)音量
kag.setVoiceVolume(名前, 音量) キャラごとの音量を設定する関数
kag.getVoiceVolume(名前, true) キャラごとの音量を得る関数

・音量はすべて0~100の数値になります。デフォルトでは100です。

・システム効果音はkag.sameSysSEVolumeがtrueの時のみ使えます。デフォルトではfalseです。今回は解説しません。

・動画の音量はデフォルトではBGMの音量と同じになります。kag.independentMovieAudioPropertyをtrueにすると動画の音量を別に設定できるようになります。

BGVには実はbvoiceとhvoiceの2種類があります。hvoiceの使い方はbvoiceと全く同じで、属性名がbvoiceからhvoiceになるだけです。音量調整がbgvvolumeとbgv2volumeで別々にできます。実際に使うことは滅多にありません。

・キャラごとの音量はボイス、BGVの両方に影響します。kag.voicevolumeは全キャラのボイスのみ、kag.bgvvolumeは全キャラのbvoiceのみに影響します。

以下は実際に使ってみた例です。使った見た以上の意味はありません。

; 音量設定の簡単な使用例
@linemode mode=vn

; 現在のマスター音量を表示
現在の音量は[emb exp="kag.wavevolume"]%です。

; ゲーム全体の音量を100に設定
@eval exp="kag.wavevolume = 100"

; BGMの音量を80に設定
@eval exp="kag.bgmvolume = 80"

; ボイス全体音量を20に設定
@eval exp="kag.voicevolume = 20"

; キャラ音量を50に設定
@eval exp="kag.setVoiceVolume('しおり', 50)"

; キャラ音量を表示
現在のしおりの音量は[emb exp="kag.getVoiceVolume('しおり', true)"]%です

以下は音量を調整するスライダを配置する例です。こちらはよく使うかもしれません。

; スライダで音量を設定する使用例
@iscript
// しおりのキャラ音量のプロパティ
property voiceVolumeShiori {
	setter(value) { kag.setVoiceVolume("しおり", value); }
	getter() { return kag.getVoiceVolume("しおり", true); }
}
@endscript

@linemode mode=none
@backlay
@nowait

; 操作対象を message1/back に設定
@current layer=message1 page=back

; メッセージレイヤの大きさを調整
@position left=0 top=0 width=800 height=600 visible


; BGM音量スライダを追加
@locate x=0 y=0
BGM
@locate x=120 y=0
@slider width=200 height=30 min=0 max=100 value=kag.bgmvolume jumpmode

; 効果音音量スライダを追加
@locate x=0 y=80
効果音
@locate x=120 y=80
@slider width=200 height=30 min=0 max=100 value=kag.sevolume jumpmode

; ボイス音量スライダを追加
@locate x=0 y=160
ボイス
@locate x=120 y=160
@slider width=200 height=30 min=0 max=100 value=kag.voicevolume jumpmode

; キャラ音量(しおり)スライダを追加
@locate x=0 y=240
しおり
@locate x=120 y=240
@slider width=200 height=30 min=0 max=100 value=voiceVolumeShiori jumpmode

; 操作対象を戻す
@current layer=message0 page=fore

@endnowait

; トランジションで表示
@trans method=crossfade time=1000
@wt
@s

KAGEX講座(36) – BGV再生(bvoice)

KAGEXにはBGV再生機能がついています。キャラクタのbvoiceコマンドでファイル名を指定すると再生されます。

@linemode mode=vn

@しおり bvoice=cvl_shiori__0001
BGVとしてcvl_shiori_h_0001が再生されます。

次のページに進んでも再生されたままです。

@しおり voice=cv_shiori_0001
【しおり】
「通常のボイスとしてcv_shiori_0001が再生されます。
再生されている間BGVは停止します」

通常のボイスが止まると再びBGVが再生されます。

@しおり bvoice=""
BGVを停止しました。

通常のボイス再生についてはKAGEX講座(30)を参照してください。

bvoiceコマンドにファイル名を指定するとそのキャラのBGVとして再生されます。停止したいときはbvoice=””となります。また、BGVはデフォルトでループ再生されます。再生時にloop=falseを付ければループしません。

上の例のように通常のボイスと同時には再生されません。ボイスが流れている間はBGVは自動的に停止され、ボイスの再生が終わったら自動的に再開されます。

KAGのintrandomを使ってはならない

吉里吉里2/KAG3のintrandomは簡単に乱数が使えて便利ですが、デフォルトの状態ではバグがあります。例えば「@eval exp=”f.乱数 = intrandom(0, 5)”」とするとf.乱数は0以上5以下の数値になるはずですが、非常に稀に6になってしまいます。
これはtjsのMath.randomがバグっているためですが、吉里吉里2は既にメンテナンスされていないので多分修正されません。ということで自分で直しましょう。以下のスクリプトをOverride.tjsかAfterinit.tjsにコピーしておけば直ります。

// 0以上1未満の乱数を返す(Math.RandomGeneratorを使うように修正)
Math.random = function() {
    if (Math.random.staticGenerator === void) {
        Math.setSeed();
    }
    return Math.random.staticGenerator.random();
};
Math.random.staticGenerator = void;

// Math.randomのシードを設定 
Math.setSeed = function(seed) {
    if (seed === void) seed = System.getTickCount();
    if (Math.random.staticGenerator === void) {
        Math.random.staticGenerator = new Math.RandomGenerator(seed);
    } else {
        Math.random.staticGenerator.randomize(seed);
    }
};

Math.randomをMath.RandomGeneratorを使って計算する形に修正しています。Math.randomが直ればそれを使っているintrandomの動作も直ります。

吉里吉里2から吉里吉里Zへの移行

吉里吉里Zに移行しようとしてみた
吉里吉里Zに移行しようとして挫折した話を読んだ。吉里吉里2と吉里吉里Zではかなり違うので意外と大変そう。

(勝手に)吉里吉里Z移行ガイドはまず読んだ方がいいです。以下、ここに書いていない部分など。

・コンソールがない
githubにあるKrkr2Compatにtjsで書かれたコンソールがあります。
右にあるDownload ZIPボタンから吉里吉里Zのソースコードをダウンロードできます。その中のscript/Krkr2Compat/フォルダがKrkr2Compatです。使い方などの詳しい説明はreadme.txtを読んでください。

・Layerのメソッドが減ってる
Layer.affineBlendなど古いメソッドが無くなっています。以下のスクリプトをstartup.tjsの最初などに書いておけばいいです。厳密にはLayer.faceとLayer.typeを使っていると描画結果が変わることがあるかもしれません。

Layer.affineBlend = function(src, sleft, stop, swidth, sheight, affine, A, B, C, D, E, F, opa=255, type=stNearest) {
    this.operateAffine(src, sleft, stop, swidth, sheight, affine, A, B, C, D, E, F, omOpaque, opa, type);
};
Layer.affinePile = function(src, sleft, stop, swidth, sheight, affine, A, B, C, D, E, F, opa=255, type=stNearest) {
    this.operateAffine(src, sleft, stop, swidth, sheight, affine, A, B, C, D, E, F, omAuto, opa, type);
};
Layer.blendRect = function(dleft, dtop, src, sleft, stop, swidth, sheight, opa=255) {
    this.operateRect(dleft, dtop, src, sleft, stop, swidth, sheight, omOpaque, opa);
};
Layer.pileRect = function(dleft, dtop, src, sleft, stop, swidth, sheight, opa=255) {
    this.operateRect(dleft, dtop, src, sleft, stop, swidth, sheight, omAuto, opa);
};
Layer.stretchBlend = function(dleft, dtop, dwidth, dheight, src, sleft, stop, swidth, sheight, opa=255, type=stNearest) {
    this.operateStretch(dleft, dtop, dwidth, dheight, src, sleft, stop, swidth, sheight, omOpaque, opa, type);
};
Layer.stretchPile = function(dleft, dtop, dwidth, dheight, src, sleft, stop, swidth, sheight, opa=255, type=stNearest) {
    this.operateStretch(dleft, dtop, dwidth, dheight, src, sleft, stop, swidth, sheight, omAuto, opa, type);
};

・自分でビルドしよう
配布されているバージョンはあまり更新されないようなのでできれば自分で最新版をビルドして使った方が良いです。発見されたバグなどが修正されていません。
やり方はgithubのHowToBulid.txtに書いてあります。簡単に手順を説明すると
1.Visual Studio Express 2012をインストールする
2.nasm-2.10.09-installer.exeをダウンロードしてインストールする
3.githubのDownload ZIPボタンからソースコードをダウンロードする
4.解凍してsrc/core/vc2012/tvpwin32.slnをダブルクリックしてVisual Studio 2012を起動する
5.メニューのビルド>ソリューションのビルドをクリックする
しばらく待つとbin/win32/フォルダにtvpwin32.exeができます

・移行する意味あるの?
新しい機能を使わないなら苦労するだけです。やめておきましょう。

最初に作ったWindowを閉じると終了してしまう

以下のスクリプトを実行すると、ウィンドウが一瞬表示されてもすぐに吉里吉里が終了してしまいます。吉里吉里ではメインウィンドウ(最初に作ったウィンドウ)が無効化されると自動的に終了する仕組みになっているからです。
この場合は先に作成したtemporaryWindowが無効化されているため終了してしまいます。

var temporaryWindow = new Window();
invalidate temporaryWindow;

var mainWindow = new Window();
mainWindow.visible = true;

System.exitOnWindowCloseをfalseにしておけばメインウィンドウが無効化されても終了しなくなります。なので以下のようにすればtemporaryWindowが無効化されても吉里吉里が終了しないようにできます。

// temporaryWindowが無効化されても吉里吉里が終了しないようにする
System.exitOnWindowClose = false;

var temporaryWindow = new Window();
invalidate temporaryWindow;

// mainWindowが無効化されたら吉里吉里が終了するようにtrueに戻しておく
System.exitOnWindowClose = true;

var mainWindow = new Window();
mainWindow.visible = true;

mainWindowが作成される時点で他のウィンドウがないので、mainWindowが新しいメインウィンドウとして機能します。よってSystem.exitOnWindowCloseをtrueに戻しておけば、mainWindowが無効化されたときに吉里吉里も終了するようになります。
falseのままだとウィンドウが全て消えても吉里吉里自体は起動したまま、ということになってしまうので戻しておくのが安全です。

実際に表示して使用するウィンドウを作成する前に、別の用途で一時的にウィンドウを使う場合などに便利だと思います。ちょっとウィンドウ作成したら勝手に終了するようになったので、この辺を覚えていないと微妙に詰まりました。

ちなみにですが、ウィンドウオブジェクトは右上の閉じるボタンやWindow.closeなどで閉じると自動的に無効化されるようです。無効化せず非表示にしたいだけの場合はWindow.visibleの方を使います。

Arrayに関数を追加しても使えない?

Array.assignStructやDictionary.assignStructで二次元配列などをコピーすると、中に入っている分の配列はシステム側で新しく作成されます。そのような配列には追加した分の関数が反映されません。

以下のコードのERRORという部分では「testFuncが見つかりません」というエラーがでます。
(Array.testFunc incontextof bar[0])(); のように直接呼ばない形なら使えます。

// Array に関数を追加
Array.testFunc = function() { Debug.message("testFunc called"); };

var foo = [];
foo.testFunc(); // OK

foo[0] = [];
foo[0].testFunc(); // OK

var bar = [];
bar.assignStruct(foo);
bar.testFunc(); // OK

bar[0].testFunc(); // ERROR

assignStruct以外にも(const)を付けた場合や関数の可変長引数としての配列の場合などにも駄目なようです。

Array.testFunc = function() { Debug.message("testFunc called"); };

var foo = (const)[];
foo.testFunc(); // ERROR

[].saveStruct("array.tjs");
var bar = Scripts.evalStorage("array.tjs");
bar.testFunc(); // saveStructで保存すると(const)が付くので ERROR

function func(args*) {
  args.testFunc(); // ERROR
}
func();

saveStruct.dllのArray.save2など、プラグインで追加されるものについても同様です。