ちょっとしたTIPS・定石集です





デバッグプリント

console.log()を使ってログファイルに文字を出力します。改行付きです。

console.log()は引数を1つだけしか出力できませんので、複数の値を1行に表示したいときは
文字列連結演算子'##'を利用するといいかもしれません。

//
// デバッグプリント
//
var a=99.999, b="hogehoge", c=0xaaaaaaaa;

console.log(a); // 99.999000
console.log(b); // hogehoge
console.log(c); // -1431655766

var d="変数eの値は", e=12345, f="です。";

console.log(d ## e ## f); // 変数eの値は12345です。




ファイルを読み込む

load_file関数を使えば、任意のデータファイルを読み込めます。
ユーザー定義のデータファイルを処理したいときに利用して下さい。

読み込み先バッファとして、型付き配列 ArrayBuffer を指定します。

//
// バイナリーファイル読み込み
//
var buf = new ArrayBuffer(512);		// 読み込み先バッファ(512バイト)
var av = new Int8Array(buf,512,0);	// 8ビット整数×512個としてアクセスする

// my000.datというファイルをバッファに読み込む
if ( load_file("my000.dat", buf) < 1 ) {
	throw ; // 読み込み失敗で例外送出(終了します)
}

var i;
for (i=0;i<512;i++) { // 全データをログに出力
	console.log(av[i]);
}




セーブファイルを使う

データをファイルに書き出すには SaveFile オブジェクト定数を使います。
任意形式のデータファイルを読み込むだけなら load_file関数 を使えますが、
読み書き両方が可能なファイル操作は SaveFile オブジェクトだけです。

読み書きのバッファには、load_file関数と同様に、
型付き配列のバッファ(ArrayBuffer)とそのビュー(Int**Array)を利用します。

//
// セーブファイル 書き出し
//
// mygame.sav というファイルを パスキー無しで開く
if ( SaveFile.open("mygame" ) ) { // 拡張子は指定しないでね
	console.log("OK");
}
else {
	console.log("fail");
	throw ; // オープン失敗で例外送出(終了します)
}

// 型付き配列とそのビュー
var ab = new ArrayBuffer(256);	// 256バイトのバッファを
var av = new Int32Array(ab,64);	// 32ビット整数×64個としてアクセスする

// 適当なデータを書き込む
av[0] = 0xaaaaaaaa;
av[1] = 0xbbbbbbbb;
av[63] = 0x12345678;

SaveFile.write(ab,256); 	// 型付き配列の内容をセーブファイルへ書き出し実行

ab.dispose(); ab=av=null;	// 型付き配列の後始末をして
SaveFile.close(); 		// 最後にファイルを閉じる

この「セーブファイル 書き出し」スクリプトを実行してみると、本体のあるフォルダに
mygame.sav というファイルが作られているかと思います。


ご注意
SaveFileには以下のような制限があります。
・セーブファイルの拡張子は *.sav に固定されます。
・セーブファイルを保存できる場所は本体のあるフォルダとそのサブフォルダに制限されます。
・スクリプトは1度のセッション(*)で最大で1個のセーブファイルにしかアクセスできません。
・セーブファイルには既定のヘッダーが強制的に適用されます。
・DXアーカイブには対応していません。
・セーブファイルのオープン時にはパスワードを指定します。(現在未実装です)
(*)本体を起動してから終了するまで

制限が多くて使いにくいと感じるかもしれませんが、間違って大事なファイルやデータを壊さないための用心です。



上で作った"mygame.sav"というセーブファイルを読み込むときには次のようにします。
//
// セーブファイル 読み込み
//
// mygame.sav というファイルを パスキー無しで開く
if ( SaveFile.open("mygame") ) { // 拡張子は指定しないでね
	console.log("OK");
}
else {
	console.log("fail");
	throw ; // オープン失敗で例外送出(終了します)
}

// 型付き配列とそのビュー
var ab = new ArrayBuffer(256);	// 256バイトのバッファを
var av = new Int32Array(ab,64);	// 32ビット整数×64個としてアクセスする

SaveFile.read(ab,256); 	// 読み込み

// データをログに出力して確認
console.log(av[0]);	// -1431655766 (= 0xaaaaaaaa)
console.log(av[1]);	// -1145324613 (= 0xbbbbbbbb)
console.log(av[63]);	// 305419896   (= 0x12345678)

ab.dispose(); ab=av=null;	// 型付き配列の後始末をして
SaveFile.close(); 		// 最後にファイルを閉じる
>SaveFile.write(ab,256);
という箇所が
>SaveFile.read(ab,256);
となった以外は同じ構成でセーブファイルの読み書きができることがわかると思います。


クリックボタンを作る

マウスポインタの位置やマウスボタンの押下げ状態を取得するためのメソッドは
Systemオブジェクト定数にあります。
これらを組み合わせれば、WindowsのGUIのボタンのようなものから、外見や振舞いも色々に自由なものまで作れそうです。

やり方もいろいろあるとは思いますが、ここでは次の例で、シンプルなクリックに反応するボタンを
一つの実現方法として紹介します。
//
// シンプルなクリックボタン
//
// レイヤー
var lay1 = new Layer(1);
var lay2 = new Layer(2); // ボタンのテキスト用
// イベントを待つオブジェクトの配列(キューとして利用)
var qu = new Array();
// ラベル
var lab1 = new NumericLabel(0); enlayer(lab1, lay1); lab1.x = lab1.y = 50;
// カラー値
var red = get_color(0xd0,0x00,0x00);
var lightred = get_color(0xa0,0x30,0x30);
var blue = get_color(0x00,0x00,0xd0);
var lightblue = get_color(0x30,0x30,0xa0);

// カウンター用変数
var cnt = 0;

// クリックボタン定義
function Button(x,y,w,h, text, c1,c2, f) {
	this.box = new BoxShape(w,h, true); enlayer(this.box, lay1); this.box.x=x; this.box.y=y;
	this.box.brush = c1;
	this.lab = new Label(text); enlayer(this.lab, lay2); this.lab.x = x+110; this.lab.y = y+70; 
	this.flag = false; // 連射防止用
	this.within = function (tx,ty) { return tx>=x && ty>=y && tx< x+w && ty< y+h; };
	this.on_down = function () { if (!this.flag) {this.on_click();} this.flag=true; this.box.brush = c2; };
	this.on_up = function () { this.flag= false; this.box.brush = c1; };
	this.on_click = f;
}

// マウスイベントの送出関連
function eachup(e) {e.on_up();}
function buttondown() { var a = qu.find( function(e) { return e.within( System.mouseX, System.mouseY );} );
	if (a!=undefined) {a.on_down(); } }
function buttonup() { qu.forEach( eachup ); }

// メインループ
function loop() {
	while (1) {
		if (System.mouseButton & 1) {buttondown();} else {buttonup();}
		wait;
	}
}

// ラベル更新ループ
function lab_loop() {
	while (1) {
		lab1.setValue(cnt);		
		wait;
	}
}

// コールバック定義
function _countup() { cnt++; }
function _countdown() { cnt--; }

// キューへ登録
qu.push( new Button(100,100, 300,100, "加算ボタン",red,lightred, _countup) );
qu.push( new Button(200,150, 300,100, "減算ボタン",blue,lightblue, _countdown) );

// 起動
spawn(lab_loop);
spawn(loop);
実行すると、赤い「加算ボタン」青い「減算ボタン」が出ました。
ボタンはわざと一部が重なるように位置調整してあります。
それぞれのボタンをクリックして、カウンターの動きをテストしてみて下さい。

かなり長くなっていますが、
中ほどの「クリックボタン定義」「マウスイベントの送出関連」あたりに注目して下さい。
その他はいつもと同じようなことをやっているだけです。

「クリックボタン定義」では
コンストラクタへの引数をインスタンスの各メンバーに単純に割り当ててる他は、
・this.within でボタンの内側かどうかの判断を
・this.on_down/this.on_up でボタンの上でマウスが押下げ/上げられた場合の処理を定義しています。
・this.flag はボタン押下げで連射状態にならないようにフラグ管理しています。

この例はイベントドリブンではなく完全に同期的ですが、
配列を利用することでイベントを待ち受けるオブジェクト(ここではButton)を複数入れておけるようにしました。

なぜ一部重ねて表示させているのか?というと・・・
ボタン押下げという「イベント」をどのオブジェクトが取るのか?という流れを確認しやすくするためでした。
上の例ではボタン押下げイベントはfindメソッドで配列の先頭から順にチェックしていくので、 配列変数qu内でのそのボタンの順序で決まるというわけなのです。


import文/export文

モジュール化をより進めるために import文/export文 という構文を導入します。(ver. 0.1.29)
import文は実行時に、外部ファイルを読み込み、評価します。
export文はその呼び出しに備えて、外部に公開すべき識別子を指定しておきます。
それぞれ以下のように1種類の形式をサポートします。

export文→
export { 識別子 [,識別子,識別子...] };

import文→
import 識別子 % モジュール名文字列 ;


export文のブレース波括弧は必須です。省略できません。
import文のモジュール名文字列は文字列リテラルです。「式」ではないので変数や演算子を使うことはできません。
どちらも「文」なので、最後のセミコロンを忘れないように。

下記のような内容の2つのスクリプトファイルを作って本体を起動してみて下さい。
main.gjs(インポート側) から mymodule.gjs(エクスポート側) の内容を呼び出す実験です。
// mymodule.gjs (エクスポート側)
var v1 = 42;
var v2 = "hello!";
var v3 = function(n) { return n*n + v1; };
export { v1, v2, v3 };
// main.gjs (インポート側)
import foo % "mymodule.gjs";
console.log( foo.v1 );		// 42
console.log( foo.v2 );		// hello!
console.log( foo.v3(3) );	// 51

export-importで行われる内部処理の流れをおおまかに追うと、
export文が指定した変数について、空オブジェクトを生成し、そこに変数と同名のプロパティを作り、
値を与えた上でそのオブジェクトごとimport側の変数に代入する――となります。

これにより外部ファイルにあるオブジェクトにアクセスできるようになります。
自作ライブラリなどを実行時ロードして利用するなどの用途に使えそうです。
これは実行時ロードなので、条件によりロードしたりしなかったりを選択できるということでもあります。

#includeのインクルード機能も同じモジュール化のための機能ですが、
インクルードはコンパイル時に、import/exportは実行時に行われるという違いがあります。

加えて、import文には特殊な形式として、非リターン 形式を導入します。

非リターンimport文→
import @ モジュール名文字列 ;

これは対象モジュールの冒頭にジャンプして実行を開始します。そこまでは普通のimport文と同じです。
しかし以後の制御は対象モジュール側に移ったままになります。つまり通常関数のようにリターンして戻る――ことが出来ないので注意してください。
イニシャルローダー的な用途がありそうです。


低い優先度のマイクロスレッドの利用

MicroThreadのメソッド setPriorityを使用すると、そのマイクロスレッドの実行優先順位を変更できます。
といっても、
現在のこのメソッドの実装では低い(=遅い)実行優先度を示す値しか与えることができません。
これでいったい何ができるのでしょうか?

低優先度のマイクロスレッドに想定している利用法は
「後片付けスレッド」を作ることです。

マイクロスレッドをたくさん使うスクリプトはカンタンに書けます。
しかし生成したマイクロスレッドの実行順を任意に設定して正しく使うのは現状では難しいでしょう。

低優先度を持つ「後片付けスレッド」が動いていれば、次フレームの直前のタイミングもここで取れることにもなるので、
フレーム毎の初期化処理もここに書くことができる――という便利さを見ています。(*1)

重要なことは「同じ優先度を持つマイクロスレッドどうしでは実行順は決まっていない」という点で、これは仕様です。
とはいえ、たぶん実際にはspawnで生成された順序になってるとは思います。
しかし、このこと利用することは表面上問題ないかもしれませんが、
上の仕様を無視しているので、推奨できない、悪い書き方になってしまいます。
それとは逆に、明示的に低優先度スレッドを使用することは、
実行タイミングが仕様により保証されいるのでまさに正しいやりかたと言えます。

(*1) 低優先度スレッドを複数動かしてしまうと、やはり同じ問題になってしまいますが

2つの例を見て下さい。
func3を他の関数より後に実行させたいのですが、、、
2つのスクリプトはたぶん同じ結果(「1」「2」「last!」の順で出力)だと思います。
// 悪い例
function func1() { console.log(1); }
function func2() { console.log(2); }
function func3() { console.log("last!"); }
spawn(func1);
spawn(func2);
spawn(func3);
// 正しい例
function func1() { console.log(1); }
function func2() { console.log(2); }
function func3() { console.log("last!"); }
var mth1 = spawn(func3);
mth1.setPriority(0); // 低い優先度を与える
spawn(func1);
spawn(func2);
上の例のようにspawn順を期待して書くと、その後にspawn(func4)などが加わった場合に混乱するかもしれません。
関数名をもっとそれらしく工夫するなどの対処ももっともなのですが、本質的ではありません。下の例のように低優先度を明示的に
与えれば、マイクロスレッドをどんな順序でspawnするかは問題ではなくなります。

もっと優先度を細かく指定できたほうが便利なのでは?――たとえば256段階とか
はい。そう思います。
2段階というのは実装上の制限であり、仕様ではありません。
将来バージョンでは増やすべきかもと考えています。

しかし優先度ソートのための計算コストも発生するため、この機能をどう拡張するかは少し慎重に考えたほうが良さそうです。


Pictureのコピー

Pictureのインスタンスをコピーするには、2つの方法があります。
●コンストラクタを使う
●duplicateメソッドを使う

この2つのやり方にはどういう違いがあるのでしょうか?
コンストラクタを使うと、内部でハンドルの値がコピーされるだけで、画像データの実体は共有されています。
一方duplicateメソッドは、画像データの実体を別の場所に複製してしまいます。

このことは、以下のスクリプトで確かめることができます。
Pictureのインスタンスを2個作って、一方にだけフィルターを適用してみます。

素材は山の絵を使います。[yama.jpg]

// Pictureのコピー その1
var a = load_picture("yama.jpg");
var b = new Picture(a);	// コンストラクタを使ってコピー
var lay = new Layer(0); enlayer(a,lay); enlayer(b,lay);
b.x = 300;
a.applyInvertFilter(); // aにだけ色調反転フィルターをかける
// 結果:aもbも反転される
// Pictureのコピー その2
var a = load_picture("yama.jpg");
var b = a.duplicate();	// duplicateでコピー
var lay = new Layer(0); enlayer(a,lay); enlayer(b,lay);
b.x = 300;
a.applyInvertFilter(); // aにだけ色調反転フィルターをかける
// 結果:aだけ反転され、bはそのまま
duplicateで複製したPictureのほうは変更されず
コピーコンストラクタで作ったPictureは本体を共有しているだけなので、一緒に変更されたのです。


#include

#includeディレクティブは C/C++を使う人には親しみやすい書き方です。
スクリプトの中で
#include "hoge.gjs"
などとして、外部ファイルの内容をコンパイル時に取り込むことができます。

元のC言語では *.h というヘッダーファイルを取り込むための仕組みとしておもに利用されますが、
Midgnightでは、分割されたスクリプトを挿入してつなげていくという感覚で使えると思います。
文法に沿っていれば、どんなテキストファイルでも入力として利用できます。

スクリプトをどんどん書いていくと、main.gjs が膨張していって扱いが面倒になることもあります。
そんなときには#includeでファイルをうまく分割していってください。

なお#ディレクティブ行は特殊な文法要素で、行末までを切り取ります。
(後方に文や式を書いてもそれらは全て無視されます)

import/exportが実行時に外部ファイル(モジュール)を取り込むのとは対照的に
#includeはコンパイル時に効果を発揮します。


renderX()とマウス操作

renderX()
renderY()
すべての描画要素オブジェクトには上記2つのメソッドが用意されています。
これらは描画要素の絶対座標を手に入れたいような場合に便利です。

コンテナContainerオブジェクトを利用して座標の相対指定ができるのは多くのケースで便利ですが、
いくつかのケース、たとえば――
オブジェクト同士の衝突判定やマウスポインター座標との比較などを行いたい場合には
相対座標よりも絶対座標が欲しいかもしれません。

renderX() , renderY() はオブジェクトに結びつけられているコンテナをすべて適用・計算して
絶対座標を返却します。


function*宣言とyield演算子

ver.0.1.36からの試験的要素として、軽量ジェネレーター というものを導入しています。
(ジェネレーターまわりの動きはなかなか厄介なので、詳しい解説が必要な方は別のしっかりしたサイトや書籍にあたることをお勧めします)

「軽量」と付いた意味はなんでしょうか?

Midgnightの 軽量なfunction*宣言 は環境を捕捉しません
したがって、function*宣言の中では、仮引数でもローカル変数でもないような変数(自由変数)を使うことができません。
さらに、軽量なyield演算子の注意として普通の式文と比べ、あまり複雑な代入の組み合わせができないようになっています。
この演算子の優先順位は低いので、代入時は yieldと値をカッコで囲む必要もあります。(下記サンプル参照)

・・・と、Midgnight独自仕様の軽量ジェネレーターには制限があるわけですが、
引数で渡してローカル変数に保存するなど、回避方法はあると思います。
それ以外は普通のいわゆるジェネレーターとして使用できると思います。

簡単に流れを見ると・・・
function*宣言はジェネレーター関数を宣言定義します。この関数をコールするとすぐオブジェクトが返ります。
ここではまだ関数の内容は実行されていません。
このオブジェクトは next というメソッドだけを持ち、このメソッドをコールすると
先に宣言定義した関数の内容が実行されます。
この実行は yield演算子によって中断され、値(*)をnextメソッドコールに戻します。
次回以降のnextメソッドコールで、yield以降の実行を再開できます。このときnextメソッドに与える引数をyield演算子自身の値とできます。

(*) nextメソッドの返り値として得られる値は done と value の2つのプロパティを持つオブジェクトです。



// 軽量ジェネレーター その1
function* foo(arg) {
  yield 42+arg;
  yield 84-arg;
  yield 100*arg;
}

var it = foo(11);
var x;
while (!(x=it.next()).done) {
  console.log(x.value);
}
// 実行結果
// 53
// 73
// 1100

その1の例のように、next引数なし、yieldから代入なし、で使用する場面も多いようですが、
次のその2のように、next引数あり、yieldから代入あり、となると、値のやりとりと実行フローの理解が
最初のうちはやや複雑に感じるかもしれません。
// 軽量ジェネレーター その2

function* foo(arg) {
  var y = 0;
  y = (yield 42+arg+y);
  y = (yield 84-arg+y);
  y = (yield 100*arg+y);
}

var it = foo(11);
var x;
x = it.next(1); console.log(x.value); // 初回実行:xは53(引数の1は利用されない)
x = it.next(2); console.log(x.value); // 2回目:xは75
x = it.next(3); console.log(x.value); // 3回目:xは1103

function*は現在のところありません。
yield*演算子は現在のところありません。

乱数生成器

Mathオブジェクトにはすでに、randomInt/randomFloatという乱数を得るためのメソッドがあるのですが、
おもにテスト・デバッグの目的で、(毎回異なり予測不能な乱数系列ではなく)
つねに固定された擬似乱数があれば便利な場合もあるかと思います。
そういう時にRandomGeneratorオブジェクトを利用します。

具体的には、10個の整数値をシード(種)として、コンストラクタ引数に与えることで、 新たなユーザー定義の乱数生成器を作ります。
(整数値の並びは型付き配列に入れて渡します)

なおユーザー定義の乱数は、このRandomGeneratorインスタンスのメソッドから直接得ることもできますが、
System.setDefaultRandomGeneratorメソッドを使うことで、
Math.randomInt や Math.randomFloat が使用する乱数生成器を変更できます。

こちらの方法はほとんどスクリプトを変更することなく、ユーザー定義の乱数を代わりに利用できるので、
テスト・デバッグ時にはより都合が良いと思います。