くりすたるです。
「えっくすたる」でもまあ、とかどこかで作者がおっしゃっていたような気もしたけど、「くりすたる」が正しいです一応。
Int → Float : int.to_f();
Float → Int : float.to_i();
i : 128; i.p(); f : i.to_f; f.p(); i2 : f.to_i; i2.p(); // > 128 // > 128.0 // > 128
Xtalは、参照カウンタ方式スマートポインタを利用したガベージコレクト、を利用しています。
基本的にはメモリ管理を気にしないでプログラムできる、といういかにもスクリプト言語な感じです。
ただ、これはXtalスクリプトだけで完結している場合の話で、C++組み込みスクリプトとして使う場合に陥りやすい罠が潜んでいるところでもあります。
Xtalは潜在的に危険なオブジェクトの開放忘れを防ぐ目的で、「uninitialize時まで開放されていないオブジェクトが存在するときにはassertする」という仕様となっています。
対策としては、uninitialize前に全てのxtal::AnyPtrやxtal::SmartPtrなどXtalのオブジェクトに片っ端からxtal::nullを突っ込むのが一つとして挙げられます。確実。
void DumpByteCode(const xtal::CodePtr& code, const xtal::StringPtr& filename){ using namespace xtal; SmartPtr<FileStream> fs = xnew<FileStream>(filename, "w"); fs->serialize(code); }
// ファイル"file_name"から読み込んだ内容を処理する xtal::compile_file(const StringPtr& file_name); // sourceに渡した内容を処理する xtal::compile(const AnyPtr& source); // AnyPtrとして渡すことのできる型を明示したもの // xtal::compile(const Stream& source); // xtal::compile(const String& source);
を利用することができます。
内部で先頭4byteを”xtal”と比較してバイトコードかソースコードかを判断します。よって、
xtal_foo : "foo";
のようなソースコードを渡すと不正なコードでないのにnullが返ってきてしまいますので注意してください。
次のように使います。
void exec_xtal() { // xtal::StringPtr filename : ファイル名 const xtal::CodePtr code(xtal::compile_file(filename)); code->call(); }
void exec_xtal() { // void* ptr : メモリ上のバイトコードの先頭ポインタ // int size : バイトコードの長さ const xtal::CodePtr code(xtal::compile(xtal::xnew<PointerStream>(ptr, size))); code->call(); }
あるよ。
ary : [1, 2, 3, 4, 5]; // before for (i : 0; i < ary.size(); ++i){ ary[i].p; } // after ary{ it.p; } // ary{ |it2| などとすることで変数名を指定できます // > 1 // > 2 // > 3 // > 4 // > 5 // > 1 // > 2 // > 3 // > 4 // > 5
また、自分で配列のようなコンテナを作成した場合も、適切なイテレータとeachメソッドをバインドしてあげることで
ary : lib::MyArray(10); ary.each{ it.update(); }
みたいに同じような制御ができるようになります。
次のようにやると全てのオブジェクトで同じfiberが共有されてしまっています。
class Obj{ + _fib : null; update(){ _fib().p; } } fib : fiber(){ count : 0; while(true){ yield count; ++count; } } obj1, obj2 : Obj(), Obj(); obj1.fib = fib; obj2.fib = fib; obj1.update(); obj2.update(); obj1.update(); obj2.update(); // > 0 // > 1 // > 2 // > 3
これもこれで使い道はありますが、今回はobj1, obj2それぞれに独立した動作をするfiberを使いたいと思います。
“fiber(){}“構文は呼び出すたびに新しくfiberを作成するので、fiberを返す関数なりメソッドなりを定義すれば良いだけです。
class Obj{ + _fib : null; update(){ _fib().p; } } fun fib(){ return fiber(){ count : 0; while(true){ yield count; ++count; } } } obj1, obj2 : Obj(), Obj(); obj1.fib = fib(); obj2.fib = fib(); obj1.update(); obj2.update(); obj1.update(); obj2.update(); // > 0 // > 0 // > 1 // > 1
C的なinclude(コンパイル時に展開される)という意味では、存在しないと思います。
Rubyで言うrequireにはloadが相当し、includeにはinheritが相当するのではないかと(実際に動作的に等しいかはわかりませんが)。
上に関連しますが、loadを利用することで可能です。基本的にはtop-levelからのreturnかグローバルを介したやりとりとなります。
// foo.xtal return class { initialize(){ "Foo constructed".p(); } };
// bar.xtal return class { initialize(){ "Bar constructed".p(); } };
// hoge.xtal Foo : load("foo.xtal"); Bar : load("bar.xtal"); foo : Foo(); bar : Bar(); // > Foo constructed // > Bar constructed
標準Cライブラリの基本的な数学関数はデフォルトでバインドされているので、簡単に使うことができます。
a : math::sin(3.14); b : math::cos(1.57);
math::とつけるのが面倒であれば次のようにすると良いです。
inherit(math); a : sin(3.14); b : cos(1.57);
publicもしくはprotectedにしていることを確認し、”this.”をつけることでアクセスできます。
class A{ # _value : 0; // # = protected , + = public foo(){ ("foo "~_value.to_s).p; } } class B(A){ bar(){ "bar".p; this.foo(); this.value = 1; this.foo(); } } b : B(); b.bar(); // > bar // > foo 0 // > foo 1
可視範囲の関係で、使用したいメソッドを定義したスコープのメンバでなければ直接アクセスすることができません(たぶん?)。
“this.”をつけることで、親まで定義をたどってくれます(表現は正しいのかな?)。
グローバル変数なり、グローバルな領域にオブジェクトを配置したいことはあると思います。
globalもしくはlibを使いましょう。
globalとlibの使い分けは、名前の通りがおすすめです。
globalはリロードが容易なので、スクリプトでグローバル変数なりクラスが使いたいと思ったらglobalを使い、ゲームの初期化でグローバルにクラスとか定数とか指定したい! と思ったらlibを使う、と。
// foobar.xtal class global::Foo{ initialize(){ "Foo constructed".p(); } } class global::Bar{ initialize(){ "Bar constructed".p(); } }
// hoge.xtal load("foobar.xtal"); foo : global::Foo(); bar : global::Bar(); // > Foo constructed // > Bar constructed
「コールバック関数を登録」みたいな感じで次のようなコードを書いた。リロードしたのに前の関数が呼ばれてしまいリロードされていない。
書きかけ!
fun global::fun1(x){ print("before : ");x.p; } // ... class CallBackCaller{ _f : null; initialize(_f){} call(x){ _f(x); } } caller : CallBackCaller(global::fun1); caller.call("hoge"); // -> before : hoge fun global::fun1(x){ print("after : ");x.p; } caller.call("hoge"); // -> before : hoge // (-> after : hoge にならないの?)
循環参照が発生していると、full_gcを使わない限り回収されません(=メモリ使用量が増えていく)。full_gcは時間がかかりがちなアルゴリズムを利用しているため毎フレーム呼ぶのは避けるべきですが、長期間シーン切り替えなどが発生しない場合には十分に気を配る必要があります。
よく発生する状況は以下のとおり
STGのようにシーンごとの最大滞在時間が決まっている場合にはあまり気にしなくても良い(full_gcをシーン切り替え時に忘れなければ)と思われます。
その他の場合には、適当なタイミングでfull_gcをかけてあげるか、CPU使用率の増加に目をつぶって毎フレームfull_gcをかける必要があります。
まあ、延々とメモリ使用量が増えることに目を瞑るのであれば毎フレームgcをかけシーン切り替えでfull_gcをかけるだけで良いのですけども。
ちなみに、full_gcを呼び出してもメモリ使用量は減らなかったと思います(キャッシュされてる? もしかしたらWindows側の処理かも? あまり詳しくないのです)。