====== Xtal一問一答 ======
===== なんて読むの? =====
**くりすたる**です。
「えっくすたる」でもまあ、とかどこかで作者がおっしゃっていたような気もしたけど、「くりすたる」が正しいです一応。
===== int/float変換 =====
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
===== 終了時にassertされるんだけど! =====
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 fs = xnew(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(ptr, size)));
code->call();
}
===== foreachないの? =====
あるよ。
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を複数つくりたい =====
次のようにやると全てのオブジェクトで同じ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
===== includeみたいのないの? =====
C的なinclude(コンパイル時に展開される)という意味では、存在しないと思います。
Rubyで言うrequireにはloadが相当し、includeにはinheritが相当するのではないかと(実際に動作的に等しいかはわかりませんが)。
===== ファイル分割したい =====
上に関連しますが、loadを利用することで可能です。基本的にはtop-levelからのreturnかグローバルを介したやりとりとなります。
==== 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 にならないの?)
===== gc効いてないよ? =====
循環参照が発生していると、full_gcを使わない限り回収されません(=メモリ使用量が増えていく)。full_gcは時間がかかりがちなアルゴリズムを利用しているため毎フレーム呼ぶのは避けるべきですが、長期間シーン切り替えなどが発生しない場合には十分に気を配る必要があります。
よく発生する状況は以下のとおり
* メンバとしてfiber, fun, lambdaを保有している
* これらはthisを暗黙的に保持していますので注意が必要です
* インスタンスを他の参照から外すときに明示的にhaltしたりnullを入れる必要があります
* 子から親を参照する
* Actorのようなグローバル基底クラスを使用する設計のときに発生しがちです
* 破棄するときにnullを代入する必要があります
STGのようにシーンごとの最大滞在時間が決まっている場合にはあまり気にしなくても良い(full_gcをシーン切り替え時に忘れなければ)と思われます。\\
その他の場合には、適当なタイミングでfull_gcをかけてあげるか、CPU使用率の増加に目をつぶって毎フレームfull_gcをかける必要があります。
まあ、延々とメモリ使用量が増えることに目を瞑るのであれば毎フレームgcをかけシーン切り替えでfull_gcをかけるだけで良いのですけども。
ちなみに、full_gcを呼び出してもメモリ使用量は減らなかったと思います(キャッシュされてる? もしかしたらWindows側の処理かも? あまり詳しくないのです)。