linq.tjs公開しました

TJSでLINQ to Objectsが使えるようになるライブラリlinq.tjsを公開しました。
https://github.com/sakano/linq.tjs

LINQ to ObjectsはC#の強力なコレクション操作ライブラリです。foreachすらないtjsではfor文を回すしかありませんでしたが、linq.tjsではより簡潔に複雑な処理を記述出来ます。

吉里吉里2/Zどちらでも使えます。

使用方法

  1. releaseページからSource codeをダウンロードします。
  2. linqフォルダに入っているlinq.tjs, linq_utility.tjs, linq__order.tjs, linq_generate.tjsを自分のプロジェクトフォルダに入れます。
  3. 最新のScriptsEx.dllをダウンロードして吉里吉里と同じフォルダに入れます。
  4. 以下のコードのようにlinq.tjsを読み込みます。
    Scripts.execStorage("linq.tjs");
    

使用例

例えば以下のように使えます。

Enumerable.range(1, 10)
    .where(function(x) { return x % 2 == 0;})
    .select(function(x) { return x * 2; })
    .forEach(function(x) { System.inform(x); });
// 4,8,12,16,20 が表示される

使い方はC#のLINQと全く同じです。簡単に説明すると次のように動作します。
1. Enumerable.range(1, 10)で1~10の整数を生成
2. .where(function(x) { return x % 2 == 0;}) で偶数のみ抽出
3. .select(function(x) { return x * 2; }) で抽出した偶数を2倍
4. .forEach(function(x) { System.inform(x); }); でダイアログを表示

文字列関数

function(x) { return ~; }が鬱陶しいのでlinq.jsを見習って文字列で指定できるようにしています。
“XXX => YYY”を”function(XXX) { return YYY; }”に変換するだけですが結構便利です。先の例は以下のようになります。

Enumerable.range(1, 10)
    .where("x => x % 2 == 0")
    .select("x => x * 2")
    .forEach("x => System.inform(x);");
// =>,4,8,12,16,20

さらに省略してYYY部分のみでも使えます。この場合は第1引数が_、第2引数が_2、第3引数が_3、…のようになります。つまり”function(_, _2, _3, _4, _5, _6, _7, _8, _9) { return YYY; }”に変換されます。

Enumerable.range(1, 10)
    .where("_ % 2 == 0")
    .select("_ * 2")
    .forEach("System.inform(_);");
// =>,4,8,12,16,20

引数の受け渡し

tjsにはjavascriptと異なりレキシカルスコープがありません。つまりfunctionの中で外にある変数が参照できずLINQを使う上で非常に不便です。なのでselectやwhereなどコールバックを引数にとる関数では、通常の引数の後ろに渡された引数をそのままコールバックに渡すようにしています。

以下の例ではargやcaptionとして引数を受け取っています。indexはここでは使っていませんが現在のインデックス番号です。

Enumerable.range(1, 10)
    .where("x, index, arg => x % arg == 0", 2)
    .select("x, index, arg => x * arg", 2)
    .forEach("x, index, caption => System.inform(x, caption);", "結果表示");
// =>,4,8,12,16,20

Enumerable.from

Enumerable.fromに配列、辞書配列、文字列を渡すとLINQが使えるようになります。

配列の場合はそれぞれの要素が順番に列挙されるだけです。また、toArray()を使うと列挙中のシーケンスを配列に変換できます。

var array = Enumerable.from([10, 20, 30])
    .select("x => x * x")
    .toArray();
// arrayは[100, 400, 900]になる

辞書の場合はkeyにキー、valueにその値が入った辞書配列として列挙されます。

Enumerable.from(%["hoge" => 100, "moge" => 200])
    .forEach(function(x) {
        System.inform(@"キー:${x.key}, 値:${x.value}");
    });
// "キー:hoge, 値:100"
// "キー:moge, 値:200"
// と表示される。

文字列の場合は先頭から1文字ずつ列挙されます。toString()でシーケンスを文字列に変換できます。

var str = Enumerable.from("StRinG")
    .where("_ == _.toUpperCase()") // 大文字のみ抽出
    .toString();
// strはSRGになる。

Enumerable.extendTo

LINQを頻繁に使う場合、Enumerable.fromを毎回呼び出すのは面倒です。今のところ配列限定ですが、一度Enumerable.extendTo(Array);を呼び出せば配列からselectなどの関数を直接呼び出せるようになります。

Enumerable.extendTo(Array);

var array = [1,2,3,4,5];
array = array.select("_ * 10").toArray();
// arrayは[10,20,30,40,50]になる

関数一覧

C#のLINQ to Objectsにある関数は全て移植済みです。その他にもInteractive Extensionsやlinq.jsなどから必要そうな関数を追加しています。

クエリ関数一覧

aggregate, aggregateWithSeed, all, any, average, buffer, concat, contains, count, defaultIfEmpty, distinct, do, elementAt, elementAtOrDefault, except, first, firstOrDefault, forEach, force, groupBy, groupJoin, indexOf, innerJoin, intersect, isEmpty, last, lastOrDefault, max, min, orderBy, orderByDescending, outerJoin, repeat, reverse, scan, scanWithSeed, select, selectMany, sequenceEqual, single, singleOrDefault, skip, skipWhile, startsWith, sum, take, takeWhile, thenBy, thenByDescending, toArray, toDictionary, toLookup, toString, trace, union, where, zip

生成関数一覧

Enumerable.from, Enumerable.empty, Enumerable.range, Enumerable.repeat, Enumerable.return, Enumerable.matches, Enumerable.random, Enumerable.randomInt, Enumerable.generate

Enumerable.matches

linq.tjs独自の関数としてEnumerable.matchesを追加しています。TJSではRegExpクラスで正規表現を使えますが癖があっていまいち使いづらいです。Enumerable.matchesでは正規表現のマッチ結果を扱えます。

Enumerable.matches(/([a-zA-Z]+)=([0-9]+)/g, "foo=100 bar=200")
    .forEach(function (matches) {
        System.inform(@"0:${matches[0]}, 1:${matches[1]}, 2:${matches[2]}");
    });
// "0:foo=100, 1:foo, 2:100"
// "0:bar=200, 1:bar, 2:200"
// が表示される

第1引数は使用する正規表現です。gフラグは必須です。第2引数は正規表現の対象となる文字列です。
このようにするとRegExp.matchesの結果が列挙されます。もう正規表現を使うのにfor文は必要ありません。

吉里吉里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など、プラグインで追加されるものについても同様です。

Layer.type = ltBinder

吉里吉里のリファレンスには書いてませんがレイヤタイプにはltBinderというのがあります。


parentが違うレイヤ間の合成は見た目が変になったりしますがparentのレイヤタイプをltBinderにしておけば防止できたりします。
続きを読む

実行中のファイルと行番号を表示

吉里吉里のキャプションに実行中のksファイル名と行番号を表示できます。こちらも開発中用の機能。
currentfilde-indicater
※first.ks(13)の部分

以下のスクリプトをafterinit.tjsにコピーするだけです。KAGとKAGEXどちらでも使えます。

kag.mainConductor.onTag_org = kag.mainConductor.onTag;
kag.mainConductor.onTag = function() {
	kag.caption = System.title + @" [${curStorage}(${curLine})]";
	return onTag_org(...);
} incontextof kag.mainConductor;
kag.extraConductor.onTag_org = kag.extraConductor.onTag;
kag.extraConductor.onTag = kag.mainConductor.onTag incontextof kag.extraConductor;

実行中のシナリオファイルを開くプラグイン

吉里吉里実行中に現在のksファイルをShift+Sで開くプラグインです。

ダウンロード

KAG/KAGEXどちらでも使えます。ゲーム製作中は結構便利。

秀丸、サクラエディタ、TeraPadなど対応しているテキストエディタなら実行中の行までジャンプします。
対応してほしいエディタがあればコメント欄まで。かぐや姫Studio、KKDEは行指定で開く機能がないので無理です。

バイナリ形式の辞書でメモリリーク?

最新開発版の吉里吉里では配列や辞書をバイナリで読み書きできます。が、読み込んだ辞書が解放されない。

以下のloadBin()を呼ぶたびにメモリ使用量が際限なく増えていきます。load()では大丈夫。明示的にinvalidateしても消えない。詳細は不明。

var dic = %[];
(Dictionary.saveStruct incontextof dic)("dic.tjs", "");
(Dictionary.saveStruct incontextof dic)("bin.tjs", "b");

function load() {
	for (var i = 0; i < 10000; ++i) {
		var d = Scripts.evalStorage("dic.tjs");
	}
}

function loadBin() {
	for (var i = 0; i < 10000; ++i) {
		var d = Scripts.evalStorage("bin.tjs");
	}
}

■追記
吉里吉里Zではこのバグは修正されています。
吉里吉里2は修正されないようなのでバイナリ形式のファイルを繰り返し読み込むのは止めましょう。メモリを使いすぎて最終的にゲームが強制終了します。

mapPrerenderedFontで表示されないメモ

Font.mapPrerenderedFontの説明では”現在選択されているフォント名”に対してレンダリング済みフォントを割り当てることになっていますが、フォント高さなども揃えないと適用されないようです。
あまり使わない機能なのでちょっと詰まった。

layer.font.face = "レンダリングフォント";
layer.font.height = 24;
layer.font.bold = true;
layer.font.mapPrerenderedFont("new_font.tft"); // フォント割り当て


layer2.font.face = "レンダリングフォント";
layer2.drawText(0, 0, "てすと", 0); // レンダリング済みフォントは使われない

layer2.font.height = 24;
layer2.font.bold = true;
layer2.drawText(0, 100, "てすと", 0); // レンダリング済みフォントが使われる