====== ボールが反射し続けるサンプル ======
XtalとC++の連携がわかりにくいという無言の圧力を受けたので、描画やらオブジェクト管理やらのいわゆるゲームエンジン部分を除いた、UpdateDraw/Frameの単純なサンプルコードを載せることにしました。面倒なのでエラーチェックはほとんど除いています。
** ※ シンタックスチェックなどしてません(というか部分的に擬似コード的になってます)、参考程度に留めてください **
メインループをどこにもってきて、スクリプトをどの程度利用するかというのは結構考えられるので、ここでは適当に以下のパターンを考えることにします。
* メインループからXtalにする
* メインループはC++にし、各オブジェクトの制御のためにXtalの関数を呼び出す
他にも、
* 一部のサブシステム(UIの各要素の管理、更新など)をXtalで制御しつつ、C++で生成したインスタンスへの弱参照を持つ
といった場合には上とは少し異なる注意を払う必要がありますが今回は触れません。
===== いずれのパターンにおいても共通の簡単な仕様 =====
==== C++ ====
* class Ball
* 位置と速度、Sprite描画用オブジェクトをメンバに持つ。Xtalにバインドし、Xtalから更新を行う
* class SpriteElement
* 内部にSprite描画に必要なさまざまな情報をもっている、という設定
* class SpriteSystem
* 作成されたSpriteElementをパラメータにしたがって勝手に描画する、という設定のシングルトンクラス。Xtalにバインドし、ハンドルを介して各SpriteElementの操作を行う
* int main
* 当然必要となる。Xtalの初期化、メインループ呼び出し、Xtalの破棄を行う
* void bindGameSystem
* ゲームシステムをバインドする。今回はSpriteSystemの各インタフェースとBallクラスをバインドする
* void updateSystemStatus
* ゲームシステムのステータス(キー入力など)を更新する
==== Xtal ====
* class global::Ball (bound C++ class)
* 画面端で反射するように操作を行う
* class global::SpriteSystem (bound C++ class)
==== コード ====
// main.cpp
/*
面倒なので全コードを1ファイルにまとめてしまいました
*/
typedef int SpriteHandle;
const int Width = 800;
const int Height = 600;
class SpriteElement{
public:
void setPos(float x, float y){
x_ = x; y_ = y;
}
void setRect(float x, float y, float w, float h){
setPos(x, y);
w_ = w;
h_ = h;
}
void setTexture(Texture *texture){
texture_ = texture;
}
private:
float x_;
float y_;
float w_;
float h_;
Texture* texture_;
};
// シングルトンのつもり
class SpriteSystem : private noncopyable{
public:
static SpriteSystem &instance(){
static SpriteSystem sys;
return sys;
}
public:
// releaseはありません
SpriteHandle createSprite(){
sprites_.push_back();
return sprites_.size()-1;
}
// わざわざハンドルを使ってマネージャを介する必要があったのか謎
public:
void setPos(SpriteHandle handle, float x, float y){
sprites_[handle].setPos(x, y);
}
void setRect(SpriteHandle handle, float x, float y, float w, float h){
sprites_[handle].setRect(x, y, w, h);
}
void setTexture(SpriteHandle handle, Texture *texture){
sprites_[handle].setTexture(texture);
}
private:
SpriteSystem(){
}
std::vector sprites_;
};
class Ball{
public:
Ball(float x, float y, float dx, float dy, SpriteHandle handle)
: x_(x), y_(y),
dx(dx), dy(dy),
handle_(handle)
{
auto &sys = SpriteSystem::instance();
sys.setRect(handle_, x_, y_, 32, 32);
sys.setTexture(new Texture("ball.png"));
}
// 関連する更新が行われていたときのみSpriteに通知
// 毎フレーム1回呼び出す
void notify(){
if (changed_){
changed_ = false;
SpriteSystem::instance().setPos(handle_, x_, y_);
}
}
void set_x(float x){
changed_ = true;
x_ = x;
}
void set_y(float y){
changed_ = true;
y_ = y;
}
float x() const{
return x_;
}
float y() const{
return y_;
}
// x,yへは自分で定義したsetter/getterを使う
private:
float x_;
float y_;
// dx,dyへはXtalが勝手に定義するsetter/getterを使う
public:
float dx;
float dy;
// ここらはアクセスできないことにする
private:
SpriteHandle handle_;
bool changed_;
};
// コピーとか禁止してるときに問題なくバインドできるかチェックしたこと無いので
// だめかもしれない
XTAL_PREBIND(SpriteSystem){}
XTAL_BIND(SpriteSystem){
Xdef_method(createSprite);
}
XTAL_PREBIND(Ball){
Xdef_ctor5(float, float, float, float, SpriteHandle);
}
// set_hogeをバインドすると foo.hoge = 100 とか書けるようになる
// 同様に hoge をバインドすると fuga : foo.hoge とか書ける
// Xdef_varは両方を自動で定義・バインドする簡易記述
XTAL_BIND(Ball){
Xdef_method(set_x);
Xdef_method(set_y);
Xdef_method(x);
Xdef_method(y);
Xdef_var(dx);
Xdef_var(dy);
Xdef_method(notify);
}
/*
今回は簡単のためにglobal領域へバインドする
(global()->... の代わりに code->...を用いることでファイルへのバインドを行うこともできる)
*/
void bindGameSystem(){
using namespace xtal;
// シングルトンインスタンスをバインドする
// Classオブジェクトをバインドしないことにより、Xtalからの生成を阻止
global()->def(Xid(SpriteSystem), SmartPtr(&SpriteSystem::instance(), undeleter));
// BallのClassオブジェクトをバインドすることにより、Xtalから生成することができる
// その際に利用されるコンストラクタ(initializer)はXdef_ctorN
global()->def(Xid(Ball), cpp_class());
// ゲームシステムのグローバル定数をバインド
global()->def(Xid(Width), Width);
global()->def(Xid(Height), Height);
}
// ゲームシステムのステータスの更新
void updateSystemStatus(){
// キー入力更新
// 非同期ファイル読み書きチェック
// 音楽再生スレッドとの同期的処理
// etc.etc.
}
// プロトタイプ宣言
// 後のセクションでそれぞれ定義する
/*
ゲームのメインループ
システムの更新、オブジェクトの更新、描画を順に行う無限ループを実装する
*/
void mainLoop();
int main(int argc, char* argv[]){
using namespace xtal;
CStdioStdStreamLib std_stream_lib; // std*はCの標準ライブラリを使う
WinThreadLib thread_lib; // Windowsのスレッドを使う
WinFilesystemLib filesystem_lib; // Windowsのファイルシステムを使う
SJISChCodeLib ch_code_lib; // SJISを使う
// 環境依存である機能についてどれを使うかを設定
// 他にもアロケータなども設定できるが割愛
Setting setting;
setting.std_stream_lib = &std_stream_lib;
setting.thread_lib = &thread_lib;
setting.filesystem_lib = &filesystem_lib;
setting.ch_code_lib = &ch_code_lib;
// ここで指定したstd_stream_libなどのポインタが示す先のオブジェクトは、
// uninitializeを呼び出すまで存在している必要がある。
// Xtalを初期化
initialize(setting);
// エラーメッセージをバインド
bind_error_message();
bindGameSystem();
mainLoop();
// Xtalを破棄
uninitialize();
return 0;
}
===== メインループからXtalにする =====
この場合は、Ballインスタンスの管理がXtal内で完結します。ゲームエンジンレベルでXtalに対応することが必要となりがちです。1回仕組みを作ればまったく異なるジャンルのゲームも作成することが可能(エンジンのインタフェースがXtalになるだけなので)な柔軟性を持ちますが、主に速度の面で問題が発生します。また、使う使わないにかかわらずエンジンの要素の各種バインドが行われるため、実行ファイルサイズの増加などを招きます。
今回は小さいサンプルなのでfilelocalにBallの配列を持っている単純な構成となっています。
// mainLoopの実装
void mainLoop(){
using namespace xtal;
const CodePtr code(compile_file("main.xtal"));
code->call();
code->member(Xid(mainLoop))->call();
}
// main.xtal
inherit(math);
balls : [];
fun initBalls(){
for (i : 0; i < 100; ++i){
angle : random()*PI*2;
balls.push_back(global::Ball(
random()*global::Width, random()*global::Height,
cos(angle), sin(angle),
global::SpriteSystem.createSprite());
}
}
fun update(){
balls{
if ((it.x < 0 && it.dx < 0) || (it.x+32 > global::Width && it.dx > 0)){
it.dx = -it.dx;
}
if ((it.y < 0 && it.dy < 0) || (it.y+32 > global::Height && it.dy > 0)){
it.dy = -it.dy;
}
it.x += it.dx;
it.y += it.dy;
// Sprite用の情報更新
it.notify();
}
}
fun mainLoop(){
initBalls();
updateSystem : global::updateSystemStatus;
spriteSystem : global::SpriteSystem;
for (;;){
updateSystem();
update();
spriteSystem.draw();
}
}
===== メインループはC++にし、各オブジェクトの制御のためにXtalの関数を呼び出す =====
一部オブジェクトの制御のみにXtalを利用する場合は、だいたいこのような利用の仕方をすることになると思います。Xtalは各スレッドに1つずつ処理系を持っているため、オブジェクトの実行がマルチスレッドで行われている場合には注意が必要です(スレッドごとに初期化バインドを行う必要があり、また、他スレッドの処理系のグローバル要素にアクセスすることが不可能)。
また、他のオブジェクトへの参照を行うためには一段上の"Manager"の準備およびそれへのアクセス手段が必要となります(これは全てC++で実装しても同じ状態になる場合が多そうですが)。
こちらも、Ballインスタンスの管理は適当に行っています。
namespace {
std::vector balls;
boost::mt19937 random;
xtal::CodePtr code;
void initBalls(){
balls.reserve(100);
for (int i = 0; i < 100; ++i){
const float angle = random()*3.14159265358979323846*2;
balls.emplace_back(
random()*Width, random()*Height,
std::cos(angle), std::sin(angle),
SpriteSystem::instance().createSprite());
}
}
void updateBalls(){
using namespace xtal;
for (auto it = balls.begin(); it != balls.end(); ++it){
SmartPtr ball(&(*it), undeleter);
code->filelocal()->member(Xid(Ball_update))->call(ball);
it->notify();
}
}
}
// mainLoopの実装
void mainLoop(){
code = xtal::compile_file("ball_ctrler.xtal");
code->call();
::initBalls();
for (;;){
updateSystemStatus();
::updateBalls();
SpriteSystem::instance().draw();
}
}
// ball_ctrler.xtal
fun Ball_update(ball){
if ((ball.x < 0 && ball.dx < 0) || (ball.x+32 > global::Width && ball.dx > 0)){
ball.dx = -ball.dx;
}
if ((ball.y < 0 && ball.dy < 0) || (ball.y+32 > global::Height && ball.dy > 0)){
ball.dy = -ball.dy;
}
ball.x += ball.dx;
ball.y += ball.dy;
}
----
ここで肝となるのは、やはり
void updateBalls(){
using namespace xtal;
for (auto it = balls.begin(); it != balls.end(); ++it){
SmartPtr ball(&(*it), undeleter);
code->member(Xid(Ball_update))->call(ball);
it->notify();
}
}
ですね。
for文の条件式部分までは良いとしましょう(C++11のautoを知らない人は適当に調べて自分なりに納得してください)。for文のスコープ内を1行ずつ見ていきます。
SmartPtr ball(&(*it), undeleter);
まず、**it**はstd::vector::iterator型ですから、参照はがしによってBallインスタンスの参照になります。よって、&演算子で現在イテレートしているBallインスタンスのアドレスを取得することができます。それを**undeleter**を指定しながらスマートポインタに変換します(デフォルト引数はundeleterです)。
undeleterはundeleter_t型の関数オブジェクトです。GCによるスマポ破棄時に、保持しているポインタに対してどのような動作を行うかを決定します。undeleterは何も行いません。deleterも存在しており、そちらはdeleteを呼び出すことになっています(たしか)。
code->member(Xid(Ball_update))->call(ball);
順に読んでいくと、**code**に定義されている**Ball_update**というオブジェクトを取得し、**ball**を引数として**関数形式で呼び出す**、となります。「Ball_updateという『関数』」ではなく『オブジェクト』と書いたのは、operator()が定義されていればどのようなクラスのオブジェクトでもかまわないからです。ここではfunを利用していますが、fiberやlambdaを利用することも可能です。
また、BallがActorのサブクラスになっていてActorリストに登録されているActor::updateを呼び出していく、というタイプの実装になっている場合には
void Ball::update(){
SmartPtr ball(this, undeleter);
code->member(Xid(Ball_update))->call(ball);
notify();
}
という風になります。
===== まとめ =====
とにもかくにもXtalの関数なりファイルローカルなりグローバルなりにオブジェクトのポインタを渡すことができればXtalからアクセスすることができるわけですね。
C++とXtalでオブジェクトのやりとりを行う(所有権の移動が発生する)場合には寿命に注意しましょう。