Aqualeadチュートリアル


目次

1. 概要
2. スプライトの表示
3. モーションの再生
4. パッド入力
5. コントローラによる機能追加
6. ファイバを利用した更新処理
7. 衝突判定
8. アセンブルによる生成
9. パーティクルによるエフェクト
10. 最後に

第1章 概要

Aqualeadとは、少ない行数で本格的なゲームを作ることができるゲームフレームワークです。 このチュートリアルではAqualeadの基本的な機能を使いながら簡単なシューティングを作ります。 これがこのチュートリアルで作るシューティングの画面です。

スコアやゲームオーバーもないシンプルなサンプルですが、そのかわりコード行数はわずか100行で作られています。 だからといって、簡単なものしか作れないというわけではなく、 シンプルながらプロ用途でも問題なく使用できる能力を持っています。 どんなゲームジャンルでも使え、対応プラットフォームも多いため一度Aqualeadに慣れれば 今後のゲーム開発をすべてAqualeadのみで行うことが可能になります。

C++以外にもC#やJavaからも使用が可能です。また、独自のC++->Javaトランスレータがあるので、C++からJavaに変換しブラウザ上で動かすことも可能です。

それでは次の章からAqualeadを実際に使ってみましょう。

第2章 スプライトの表示

まずは、自機をスプライトで表示してみましょう。 Aqualeadではアプリケーションの初期化はすべてAqualeadが自動で行います。 必要があれば初期化パラメータを設定することもできますが、 特に必要がなければすべてデフォルトパラメータで初期化されます。 このように、Aqualeadの基本方針はすべてデフォルト値で初期化を行い、必要なものだけを修正するとなっています。 そのため、特殊なことをしない限りはコード量がかなり少なくなり、覚えることも少なくてすみます。

以下がAqualeadの最小コードです。

#include "Aqualead.h"

void ALInit()
{
}

void ALMain()
{
}

インクルードするヘッダは一つのみ、定義すべき関数は二つだけです。 このうちALInitはなにか特殊な初期化が必要なときのみ使用します。 ただし、この関数自体は必須です。 ALMainがゲームのコードを記述する場所となります。

当然この例では中身がなにもないのですぐに終了してしまいます。 それではスプライトを一つ表示してみましょう。

#include "Aqualead.h"

void ALInit()
{
    ALStream::SetBasePath( "..\\..\\..\\..\\Data\\" );  (1)
}

void ALMain()
{
    ALSprite *spr = ALSprite::Create( "Player" );       (2)
    spr->SetPos( 64, 240 );                             (3)
    spr->Show();                                        (4)

    while( !ALSystem::IsTerminated() ){                 (5)
        ALYield();                                      (6)
    };
    spr->Destroy();                                     (7)
}

これでスプライトが表示されます。 終了するにはウィンドウの閉じるボタンをクリックします。

前述のように初期化周りはほとんど自動になるので、明示的に変更するもの以外は記述する必要がありません。

おそらくこのコードを見るだけで動作はおおよそ想像はつくと思いますが、それぞれの説明を行います。

(1)

ファイルの基準パスを設定します。コンパイル時にはプロジェクトのある場所がカレントディレクトリになるので、データのあるフォルダを相対パスで指定します。

カレントディレクトリからファイルを読む場合はこの行は不要です。

(2)

スプライトを生成します。 Aqualeadでは原則newは使用せず、Create関数で生成を行います。 引数はスプライトに使用する画像ファイル名です。 Windows環境ではbmpとtga、それと後述するAqualeadテクスチャが使用可能で、拡張子は省略可能です。

(3)

表示座標を設定します。座標は左上が原点になり、デフォルトでは画面サイズは640,480です。ですので、64,240では左寄りで上下の真ん中あたりになります。

(4)

スプライトを表示します。 Aqualeadではすべての表示物は生成後、明示的にShowを実行するまで表示されません。

(5)

アプリケーションが終了するまでループを行います。 ALSystem::IsTerminated()関数は、ウィンドウの終了ボタンが押されるとtrueになるので、 終了ボタンが押されるまでループすることになります。

(6)

ここでAqualeadにいったん処理を戻し、1フレーム処理を進めます。 1フレームとは1/60秒で、Aqualeadはこのフレーム単位で動作します。 この時点で画面の更新等が行われるため、この関数がないと画面の更新が行われません。 この関数を実行せずにループを実行すると更新が行われず、ウィンドウが固まるので注意して下さい。

(7)

生成したスプライトを削除します。 表示オブジェクトはこのようにDestroy関数で削除を行います。

次は、これにモーションをつけてみます。

第3章 モーションの再生

この章では、先ほどのスプライトにテクスチャモーションをつけてみます。 Aqualeadには複数画像を切り替えることができるテクスチャと言うものがあります。 これは、画像自体の他にその画像がどのように分割されているかという情報を含んだデータです。 それに対して、前の章で使ったようなbmp等の画像のみのデータは、イメージと呼びます。

前の章で使った画像データでは画像パターンが1枚しかないので、2枚分の画像パターンを新たに作ります。 この2枚分の画像を横に並べ、等間隔に配置します。 その後、そのファイル名をリネームし、

Player@2.bmp

というファイル名に変更します。 このような@付のファイル名を使用すると、Aqualeadのツールは等間隔に分割されたイメージと認識します。 これを元にツールを使いテクスチャを生成します。 なお、今回は横のみに分割しましたが、縦横に分割した場合、~~@2_4.bmpの用に、 横分割数_縦分割数と指定することもできます。

なお、ツールでいったんコンバートしない限りテクスチャとして読み込めないので注意して下さい。

モーションデータは、smtというテキストで記述したモーションファイルを ALMotConvというツールでamtというバイナリファイルに変更したものを使用します。 このモーションデータは2Dだけでなく、3Dやユーザー定義データ等、 Aqualeadの大半のパラメータをモーションさせることができます。 今回はテキストファイルを直接編集しますが、GUIツールを使いモーションを定義したり、 3Dツールで作ったデータから変換したりすることができます。

今回使用したsmtファイルは以下のようになります。

[Global]
RepeatFlg=true
[0:0]
PatternNo=0
[0:12]
PatternNo=1
[0:24]
PatternNo=0

このように[]でセクションを区切り、内部にパラメータを記述することになります。 [Global]セクションはこのsmt全体に影響するもので、今回は RepeatFlag=true と指定することで、このモーションをループモーションに指定します。

次以降のものがモーションの本体で、セクションは[ノードID:フレーム]という指定になっています。 ノードIDは複数ノードがあった場合の区別用ですが、今回は使用しないので0を指定します。 コロンの後ろはモーションのフレームです。 今回は0~24まであるので、24/60秒の長さのモーションとなります。 それぞれのセクションの中に、パラメータ名=パラメータの形で各種パラメータを指定します。 今回はスプライトのパターン番号のみを指定しますが、 座標や色、表示フラグ等必要に応じて各種パラメータも同時に指定ができます。

モーション再生するコードを追加したソースはこのようになります。

#include "Aqualead.h"

void ALInit()
{
    ALStream::SetBasePath( "..\\..\\..\\..\\Data\\" );
}

void ALMain()
{
    ALSprite *spr = ALSprite::Create( "Player2" );
    spr->LoadMotion( "PlayerMotion" );                  (1)
    spr->SetPos( 64, 240 );
    spr->PlayMotion();                                  (2)
    spr->Show();

    while( !ALSystem::IsTerminated() ){
        ALYield();
    };
    spr->Destroy();
}

(1)

生成したモーションデータはこのようにLoadMotion関数で読み込みます。

(2)

読み込んだモーションは再生する必要があるので、PlayMotion関数で再生します。

これでプレイヤーキャラがアニメーションするようになりました。

次はこのプレイヤーを動かしてみます。

第4章 パッド入力

この章ではプレイヤーキャラを移動させてみます。 Aqualeadではデフォルトで、ゲームパッドとキーボード両方の入力に対応しています。 特殊なことをしなければ、ゲームパッドがつながっているかを気にする必要はありません。

コードは以下のようになります。

#include "Aqualead.h"

void ALInit()
{
    ALStream::SetBasePath( "..\\..\\..\\..\\Data\\" );
}

void ALMain()
{
    ALSprite *spr = ALSprite::Create( "Player2" );
    spr->LoadMotion( "PlayerMotion" );
    spr->SetPos( 64, 240 );
    spr->PlayMotion();
    spr->Show();

    ALPad * pad = ALPad::GetDevice( 0 );            (1)
    while( !ALSystem::IsTerminated() ){
        if( pad->GetDigital() & ALPAD_UP ){         (2)
            spr->AddY( -4 );                        (3)
        };
        if( pad->GetDigital() & ALPAD_DOWN ){
            spr->AddY( 4 );
        };
        if( pad->GetDigital() & ALPAD_LEFT ){
            spr->AddX( -4 );
        };
        if( pad->GetDigital() & ALPAD_RIGHT ){
            spr->AddX( 4 );
        };

        ALYield();
    };
    spr->Destroy();
}

(1)

パッドの入力を取得するにはまずパッドのデバイスを取得する必要があります。 この取得したパッドデバイスからボタンデータを取得します。

(2)

パッドからデジタルデータを取得するには、そのままの入力を取得する、今回利用したGetDigital()の他、 押した瞬間のみを取得するGetDigitalTrig()、メニュー等でリピート付入力ができるGetDigitalRepeat()などもあります。

各種ボタンのシンボルはこのようにALPAD_??というような形で定義されています。 これと比較することによりパッドの入力を判断できます。

(3)

今回はその入力で、プレイヤーの位置を移動させています。

このプレイヤー移動のコードは一般的なものですが、 Aqualeadではコントローラという機能を使うともっと簡単に記述することもできます。

次はこのコントローラを使用してみます。

第5章 コントローラによる機能追加

コントローラとはAqualeadの特徴的な機能の一つで、 ゲームでよく使う処理をコントローラという形でまとめ、簡単に利用できるようにしたものです。 普通に再利用するような関数・クラスよりは、プラグインに近いものになります。

では、実際に使用してみましょう。 まずは、前の章のコードをコントローラを使用する形に変更してみましょう。

#include "Aqualead.h"

void ALInit()
{
    ALStream::SetBasePath( "..\\..\\..\\..\\Data\\" );
}

void ALMain()
{
    ALSprite *spr = ALSprite::Create( "Player2" );
    spr->LoadMotion( "PlayerMotion" );
    spr->SetPos( 64, 240 );
    spr->PlayMotion();
    spr->Show();

    spr->AddController( ALPadMove8Controller::Create( 
        ALPad::GetDevice( 0 ), 4, ALRect( 0, 0, 640, 480 ) ) ); (1)

    while( !ALSystem::IsTerminated() ){
        ALYield();
    };
    spr->Destroy();
}

(1)

このように、パッドでの移動処理が1行になりました。 このALPadMove8Controllerというのは、パッドの入力を元に対象となるノード、 この場合はスプライトを8方向に移動させるコントローラです。 パラメータとして、対象のパッドデバイス、速度、移動範囲、斜め補正の有無を指定します。斜め補正は省略すると無しになります。

コントローラはノードの削除時に自動的に削除されるので、生成したコントローラを保持する必要はありません。

次は別のコントローラを使用し、プレイヤーに弾を撃たせてみましょう。

#include "Aqualead.h"

void ALInit()
{
    ALStream::SetBasePath( "..\\..\\..\\..\\Data\\" );
}

void ALMain()
{
    ALSprite *spr = ALSprite::Create( "Player2" );
    spr->LoadMotion( "PlayerMotion" );
    spr->SetPos( 64, 240 );
    spr->PlayMotion();
    spr->Show();

    spr->AddController( ALPadMove8Controller::Create( 
        ALPad::GetDevice( 0 ), 4, ALRect( 0, 0, 640, 480 ) ) );

    ALPad * pad = ALPad::GetDevice( 0 );
    while( !ALSystem::IsTerminated() ){
        if( pad->GetDigitalTrig() & ALPAD_A ){                      (1)
            ALSprite *ps = ALSprite::Create( "PlayerShot" );        (2)
            ps->SetPos( spr->GetPos() );                            (3)
            ps->AddController( 
                ALAngleSpeedController::Create( ALANGLE_0, 16 ) );  (4)
            ps->AddController( ALAreaOverDestroyController::Create( 
                ALRect( 0, 0, 640, 480 ) ) );                       (5)
            ps->Show();
        };

        ALYield();
    };
    spr->Destroy();
}

弾の処理には二つのコントローラを使用します。

(1)

GetDigitalTrigを使用し、パッドのAボタンのトリガ入力があるとプレイヤーの弾を生成します。

(2)

生成処理はプレイヤー生成と同じです。

(3)

プレイヤーの位置から弾を生成する必要があるので、プレイヤーの位置を代入します。

(4)

ALAngleSpeedControllerは角度と速度を指定してノードを動かすコントローラです。ここでは方向は右向きで角度0、速度は1フレームあたり16ドットです。XYの速度を個別に指定することもできます。

(5)

ALAreaOverDestroyControllerは指定した範囲をノードが超えたときに自動的にノードを削除するコントローラです。 このコントローラの動作で、画面外に出た弾は自動的に削除されます。

このように、色々な機能を持ったコントローラが存在するため、 目的に応じてコントローラをノードにつけていくと書かなければならないコードは激減します。

また、コントローラは自作することも可能なため、 今後使いそうなコードはコントローラという形で実装しておくと、 今後の使い回しが容易になり、ますますゲーム作りが楽になります。

次は、ファイバという便利な機能を使って敵を出してみましょう。

第6章 ファイバを利用した更新処理

ファイバとはマイクロスレッドとも呼ばれるもので、同期処理の必要のないスレッドのようなものです。

一般的にはゲームの更新処理は1フレームごとに完結させる必要があるため、 状態変数のようなものを作り、状態を切り替えながら動作させます。 ファイバを使うと、更新処理を1フレームで終わらせなくてもよくなり、 更新処理の中でループが書け、処理によっては非常に簡潔になります。

Aqualeadではこのファイバが必要なときに自動的に使われます。 特に何も設定しなくても、更新中にALYield()という関数を呼ぶだけで処理を中断することができます。

これは馴染みがないとわかりにくい概念ですので、実際のコードを見てみましょう。なお、ここからソースは長くなるので2分割にします。

#include "Aqualead.h"

void ALInit()
{
    ALStream::SetBasePath( "..\\..\\..\\..\\Data\\" );
}

void EnemyUpdate( ALNode * node )                                   (1)
{
    for( int i=ALRandInt(30)+20; i>=0; --i ){                       (2)
        node->AddX( -3 );
        ALYield();                                                  (3)
    };
    ALYield( ALRandInt(20)+10 );                                    (4)

    float yp = ALRandFloat( 8 )-4;                                  (5)
    for( int i=ALRandInt(30)+20; i>=0; --i ){
        node->AddX( -3 );
        node->AddY( yp );
        ALYield();
    };
    ALYield( ALRandInt(20)+10 );
};                                                                  (6)

追加したのは2箇所です。 メインループ内の敵生成箇所と、EnemyUpdateで定義した敵の更新処理です。

(1)

更新用コールバック関数です。引数はこのコールバックを登録したノード自身です。

(2)

生成した敵をまっすぐ左へ動かすためのループです。ALRandIntとは0から指定した値未満のランダムな整数を取得する関数です。 そのため、ここは20~50フレームの間ループします。

(3)

ファイバはこのように、ループの中にALYield()を記述するだけで使用が可能です。 こうするとALYieldで処理がいったん中止され、1フレーム分の処理が実行された後その次の行から処理が再開されます。

(4)

ループを抜けた後は、そこでいったん停止をします。 ALYieldには引数の指定が可能で、指定したフレーム分そこで処理を停止します。

(5)

後半はY方向の移動値をランダムで決め、再び左へ動きます。 ここではALRandFloat関数を使います。これは0から指定した値未満のランダムな実数を取得する関数です。その後いったん停止してこの関数の終了になります。

(6)

SetUpdateFuncで指定した関数は、このように関数が終了すると関数の頭から再び実行を再開します。 Sleep関数などで処理を一時停止するか、このノードが破棄されるまで実行は続きます。

void ALMain()
{
    ALSprite *spr = ALSprite::Create( "Player2" );
    spr->LoadMotion( "PlayerMotion" );
    spr->SetPos( 64, 240 );
    spr->PlayMotion();
    spr->Show();

    spr->AddController( ALPadMove8Controller::Create( 
        ALPad::GetDevice( 0 ), 4, ALRect( 0, 0, 640, 480 ) ) );

    ALPad * pad = ALPad::GetDevice( 0 );
    while( !ALSystem::IsTerminated() ){
        if( pad->GetDigitalTrig() & ALPAD_A ){
            ALSprite *ps = ALSprite::Create( "PlayerShot" );
            ps->SetPos( spr->GetPos() );
            ps->AddController( 
                ALAngleSpeedController::Create( ALANGLE_0, 16 ) );
            ps->AddController( ALAreaOverDestroyController::Create( 
                ALRect( 0, 0, 640, 480 ) ) );
            ps->Show();
        };
        if( ALRandInt( 60 ) == 0 ){                             (1)
            ALSprite *enemy = ALSprite::Create("Enemy");
            enemy->LoadMotion( "EnemyMotion" );
            enemy->SetPos( 660, ALRandFloat( 400 )+40 );
            enemy->PlayMotion();
            enemy->SetUpdateFunc( EnemyUpdate );                    (2)
            enemy->AddController( ALAreaOverDestroyController::Create( 
                ALRect( 0, 0, 640, 480 ), true ) );                 (3)
            enemy->Show();
        };

        ALYield();
    };
    spr->Destroy();
}

(1)

敵の生成箇所です。メインループ内でALRandInt()と比較して処理を行っています。 そのため、ここは約120フレーム毎に実行されることになります。

(2)

SetUpdateFuncで先ほどのコールバック関数を登録します。 この関数が毎フレーム呼び出されます。

(3)

プレイヤーのショットと同じく、ALAreaOverDestroyControllerを登録します。 今回は画面外から登場するので最後の引数をtrueにし、一度画面内に入ってから範囲外チェックを有効にします。

次はこれに当たり判定をつけてみましょう。

第7章 衝突判定

Aqualeadで当たり判定を処理するには、ノードにコリジョンを生成して登録します。 そのコリジョンに種別用のアトリビュートを登録し、 どのアトリビュートとアトリビュートで衝突判定を行うかを登録すると自動的に判定が行われます。 ノードにコリジョンヒットコールバック関数を登録することにより、ヒットするとそのコールバックが呼ばれるようになります。

まず、プレイヤーのショットが敵に当たるようにしてみましょう。 コードはこのようになります。

#include "Aqualead.h"

enum {                                                              (1)
    ATTRIBUTE_PLAYER        = 1 << 0,
    ATTRIBUTE_PLAYER_SHOT   = 1 << 1,
    ATTRIBUTE_ENEMY         = 1 << 2,
    ATTRIBUTE_ENEMY_SHOT    = 1 << 3
};

void ALInit()
{
    ALStream::SetBasePath( "..\\..\\..\\..\\Data\\" );
}

void EnemyUpdate( ALNode * node )
{
    for( int i=ALRandInt(30)+20; i>=0; --i ){
        node->AddX( -3 );
        ALYield();
    };
    ALYield( ALRandInt(20)+10 );

    float yp = ALRandFloat( 8 )-4;
    for( int i=ALRandInt(30)+20; i>=0; --i ){
        node->AddX( -3 );
        node->AddY( yp );
        ALYield();
    };
    ALYield( ALRandInt(20)+10 );
};

void EnemyHitFunc( ALNode *self, ALCollision * selfcol
                  , ALCollision * targetcol, bool * cancel )        (2)
{
    self->AddController( ALFadeoutDestroyController::Create( 5 ) ); (3)
    self->SetCollisionEnable( false );                              (4)
};

追加したのはアトリビュート定義のenum、コリジョンヒットコールバック、 コリジョンマネージャの初期化、プレイヤーの弾と敵へのコリジョン追加です。

(1)

コリジョンで使用するアトリビュートはビット単位で指定するので、このように2の累乗を使用します。 このアトリビュートをコリジョンと、判定をする組み合わせ指定に使います。

(2)

コリジョンヒットコールバックです。引数はコールバックを設定したノード、自分側のコリジョン、相手側のコリジョン、キャンセルフラグです。

(3)

今回はプレイヤーの弾しかヒットする相手がいないので、そのまま消滅処理を行います。 必要があれば、相手側のコリジョンを調べることにより何とヒットしたかを調べることができます。

今回は敵に耐久度を持たせず、そのまま消滅させます。 ただ、ぱっと消すと味気ないのでフェードアウト後にノードを削除するコントローラを使いフェードアウトさせます。

(4)

すぐにこのノードを削除するなら必要ありませんが、 今回の場合は消えるまでにまたヒットする可能性があるので、SetCollisionEnableでコリジョンを無効化します。

void ALMain()
{
    ALCollisionManager::GetDefault()->SetTargetAttribute( 
        ATTRIBUTE_PLAYER_SHOT, ATTRIBUTE_ENEMY );                   (1)
    ALCollisionManager::GetDefault()->ShowAll();                    (2)

    ALSprite *spr = ALSprite::Create( "Player2" );
    spr->LoadMotion( "PlayerMotion" );
    spr->SetPos( 64, 240 );
    spr->PlayMotion();
    spr->AddCollision( 
        AL2DCircleCollision::Create( ATTRIBUTE_PLAYER, 16 ) );      (3)
    spr->Show();

    spr->AddController( ALPadMove8Controller::Create( 
        ALPad::GetDevice( 0 ), 4, ALRect( 0, 0, 640, 480 ) ) );

    ALPad * pad = ALPad::GetDevice( 0 );
    while( !ALSystem::IsTerminated() ){
        if( pad->GetDigitalTrig() & ALPAD_A ){
            ALSprite *ps = ALSprite::Create( "PlayerShot" );
            ps->SetPos( spr->GetPos() );
            ps->AddController( 
                ALAngleSpeedController::Create( ALANGLE_0, 16 ) );
            ps->AddController( ALAreaOverDestroyController::Create( 
                ALRect( 0, 0, 640, 480 ) ) );
            ps->AddCollision( AL2DCircleCollision::Create( 
                ATTRIBUTE_PLAYER_SHOT, 4 ) );                       (4)
            ps->Show();
        };
        if( ALRandInt( 60 ) == 0 ){
            ALSprite *enemy = ALSprite::Create("Enemy");
            enemy->LoadMotion( "EnemyMotion" );
            enemy->SetPos( 660, ALRandFloat( 400 )+40 );
            enemy->PlayMotion();
            enemy->SetUpdateFunc( EnemyUpdate );
            enemy->AddController( ALAreaOverDestroyController::Create( 
                ALRect( 0, 0, 640, 480 ), true ) );
            enemy->AddCollision( AL2DCircleCollision::Create( 
                ATTRIBUTE_ENEMY, 16 ) );                            (5)
            enemy->SetHitFunc( EnemyHitFunc );                      (6)
            enemy->Show();
        };

        ALYield();
    };
    spr->Destroy();
}

(1)

コリジョンマネージャへの設定を行います。 コリジョンはこのコリジョンマネージャが自動的に毎フレームチェックを行います。 必要があれば、コリジョンマネージャを使用せずに手動でコリジョン同士のチェックを行うことも可能です。

コリジョンマネージャにどのコリジョンとコリジョンが衝突するかをアトリビュートで指定します。 このアトリビュートはorで結んで複数のアトリビュートの指定も可能です。 ただし、必要のないものまで指定をすると衝突チェックの時間がかかるため、必要最低限の指定をしてください。

(2)

デバッグ用にコリジョンをラインで表示します。 デバッグ用なので使用しなくてもかまいませんが、コリジョンがどう設定されているかを確認しやすくなるので最初は表示するのを推奨します。 このデバッグ表示はRelease時は自動的に削除されます。

(3)

プレイヤーにコリジョンを設定します。コリジョンはAddCollisionでノードに登録して使用します。 2Dでは現在、点・円・矩形の3種類が使用可能で、混在も可能です。 今回は円のみを使用します。引数はアトリビュートと半径です。

(4)

プレイヤーの弾のコリジョンを設定します。

(5)

敵のコリジョンを設定します。

(6)

コリジョンコールバックを登録します。 今回は敵側にコールバックを登録しましたが、コリジョン同士がヒットするとコリジョンが所属しているノード両方のコールバックが呼ばれるため、 プレイヤーの弾側にコールバックを設定してもかまいません。

続けて敵に弾を撃たせたいところですが、少しコードが長くなってきました。 ここでAqualeadの大きな特徴の一つであるアセンブル機能を使い、敵や弾の生成を簡略化してみましょう。

第8章 アセンブルによる生成

アセンブル機能とは、各種ノードやコントローラ、コリジョンなどをデータから生成する機能です。 大半のパラメータを設定でき、モーションも含めることが可能なため固定用途であれば生成が1行で終わります。 RPGのイベントで使用するようなキャラ劇などもこのアセンブル機能で実現が可能です。 また、プログラムを変更しなくてもパラメータの変更ができるため、トライ&エラーがすばやく行えます。 ただし、変化があるパラメータには向いていないので、ものによってはプログラムで設定する方がいい場合もあります。

プレイヤーのAssembleで使用しているオブジェクト定義ファイルは以下のようになります。

[0]
Type=Sprite
Texture0ID=0x10000
Position=64,240,0
Disp=1
Collision[0].ShapeID=2CIR
Collision[0].Radius=16.0
Collision[0].Attribute=1
Controller=PadMove8,0,4,0,0,640,480,0

書式はモーション定義ファイルからフレーム指定を抜いたような形になります。 このように生成するノードの種類、設定するパラメータ、 生成するコリジョン、コントローラなどを指定しておくと、 Assembleの1行でこれらが自動的に生成、設定されます。

また、コンバート時にモーションを指定しておくとモーションのロードも自動的に行われます。

今回はモーションファイルと共に手動で作成していますが、 これらを生成できるアニメーションエディタも存在します。 通常はこのエディタでモーションファイルやオブジェクト定義ファイルを作ります。

アセンブルを使う場合は、通常アーカイブも同時に使用します。 アーカイブとは複数のファイルをまとめて一つのファイルにしたものです。 ファイル数を減らすことによりロードの高速化が行われます。 またアーカイブ内のファイルにはIDを使用することができ、 このIDを使用するとファイルの検索が高速になります。

アセンブル処理等でテクスチャ等のファイルを参照する場合はこのIDが必須になります。

アーカイブ定義ファイルは以下のようになります。

Player2.atx=0x10000
Player.aod=0x10000
PlayerShot.atx=0x10100
PlayerShot.aod=0x10100
Enemy.atx=0x20000
Enemy.aod=0x20000
Bullet.atx=0x20100
Bullet.aod=0x20100

このように、ファイル名=IDという形になります。 IDを使用せず、ファイル名だけで使うことも可能です。

IDは24ビットが使用可能で上位は拡張子情報などに使用されます。 このため、種別が違えば同じIDが使用可能です。 今回は、テクスチャと後述するAssembleで使うオブジェクト定義ファイルが同じIDになっています。

アセンブルとアーカイブを使ったソースは以下のようになります。

#include "Aqualead.h"

enum {
    ATTRIBUTE_PLAYER        = 1 << 0,
    ATTRIBUTE_PLAYER_SHOT   = 1 << 1,
    ATTRIBUTE_ENEMY         = 1 << 2,
    ATTRIBUTE_ENEMY_SHOT    = 1 << 3
};

void ALInit()
{
    ALStream::SetBasePath( "..\\..\\..\\..\\Data\\" );
}

void EnemyUpdate( ALNode * node )
{
    for( int i=ALRandInt(30)+20; i>=0; --i ){
        node->AddX( -3 );
        ALYield();
    };
    ALYield( ALRandInt(20)+10 );
    ALNode *bul = ALNode::Assemble( 0x20100 );                      (1)
    ALNode *player = ( ALNode * )node->GetUserData();               (2)
    bul->AddController( ALAngleSpeedController::Create( 
        ALArcTan2( player->GetY()-node->GetY()
        ,player->GetX()-node->GetX() ), 10 ) );                     (3)
    bul->SetPos( node->GetPos() );

    float yp = ALRandFloat( 8 )-4;
    for( int i=ALRandInt(30)+20; i>=0; --i ){
        node->AddX( -3 );
        node->AddY( yp );
        ALYield();
    };
    ALYield( ALRandInt(20)+10 );
};

今回変わった箇所はEnemyUpdate内へ敵弾発射処理の追加と、アーカイブのロード、Assemble周りのコード削減です。

(1)

敵弾をAssembleで生成します。 今回はIDをそのまま数値で指定していますが、本来はIDをツールで生成し、 その定数を記述したヘッダファイルをインクルードして使用します。

(2)

生成時に設定したUserDataからプレイヤーを取得します。 UserDataとはアプリケーションが自由に使用できるユーザーポインタで、 ノードに関連したデータを保持するためなどに使用できます。 単にフラグ等の数値をキャストして代入しても問題ありません。

(3)

プレイヤーの座標を取得し、 ArcTan2で角度を取得してその方向に移動するように設定します。

void EnemyHitFunc( ALNode *self, ALCollision * selfcol
                  , ALCollision * targetcol, bool * cancel )
{
    self->AddController( ALFadeoutDestroyController::Create( 5 ) );
    self->SetCollisionEnable( false );
};


void ALMain()
{
    ALCollisionManager::GetDefault()->SetTargetAttribute( 
        ATTRIBUTE_PLAYER_SHOT, ATTRIBUTE_ENEMY );
    ALCollisionManager::GetDefault()->ShowAll();

    ALArchive *ar = ALArchive::Create( "GameData.aar" );            (1)

    ALNode *spr = ALNode::Assemble( 0x10000 );                      (2)
    spr->PlayMotion();                                              (3)

    ALPad * pad = ALPad::GetDevice( 0 );
    while( !ALSystem::IsTerminated() ){
        if( pad->GetDigitalTrig() & ALPAD_A ){
            ALNode *ps = ALNode::Assemble( 0x10100 );
            ps->SetPos( spr->GetPos() );
        };
        if( ALRandInt( 60 ) == 0 ){
            ALNode * enemy = ALNode::Assemble( 0x20000 );           (4)
            enemy->SetUserData( spr );                              (5)
            enemy->PlayMotion();
            enemy->SetPos( 660, ALRandFloat( 400 )+40 );
            enemy->SetUpdateFunc( EnemyUpdate );
            enemy->SetHitFunc( EnemyHitFunc );
        };
        ALYield();
    };
    spr->Destroy();
    ar->Release();                                                  (6)
}

(1)

アーカイブをオープンします。 以降アーカイブに含まれているファイルは各種ファイル読み込み時に自動的に使用されるようになります。

(2)

プレイヤーをアセンブルで生成します。今まで存在したパラメータはすべてオブジェクト定義ファイルに記述されています。

(3)

アセンブルしたノードにはモーションをつけることも可能ですが、モーションの再生自体はアプリケーションから指示します。

(4)

敵をアセンブルで生成します。

(5)

プレイヤーのノードをUserDataとして設定します。これを敵弾発射時に使用します。

(6)

オープンしたアーカイブをクローズします。

なお、敵の生成を見れば分かるように各種コールバックの定義などはプログラムで設定する必要があります。 しかしこのような処理をあらかじめコントローラとして作り、 システムに登録すると組み込みのコントローラと同様にアセンブル生成させることも可能になります。

最後に、プレイヤーの死亡処理とエフェクトを作ります。

第9章 パーティクルによるエフェクト

プレイヤーの死亡処理を作るには敵破壊と同じようにコリジョンコールバックを使用します。 ただし、今回はプレイヤーの場合は一度死亡した後、少し間をあけて次の自機を登場させます。 このときファイバを使用しますが、ファイバは更新処理の中でしか使用できないので、 コリジョンコールバック内で使用するとエラーになります。

今回は、コリジョンコールバック内でプレイヤーのUserDataを利用してフラグを立ててみます。 そのフラグがたっている場合に死亡処理を行います。

エフェクトは今回はパーティクルを使用します。 これは細かな粒子の組み合わせでエフェクトを作るものです。 このパーティクルは設定項目が多いのでプログラムでは生成せず、これもアセンブル処理を使用します。

プレイヤー死亡エフェクトのオブジェクト定義ファイルは以下のようになります。

[0]
Type=Particle
Disp=1
BlendMode=1
Texture0ID=0x030000
ParticleEmitterID=0x324d524e
ParticleBirth=10.000000
ParticleSpeed=6.000000
ParticleSpeedVariation=0.300000
ParticleAge=22.000000
ParticleSize=7.000000
ParticleSizeVariation=0.600000
ParticleEndSize=7.000000
ParticleEndColor=1.000,1.000,1.000,0.300
ParticleAutoDestroy=1
ParticleResist=0.800000
ParticleStart=1
ParticleLength=3
ParticleEmitter.Range=360

項目が多いですが、ほとんどがパーティクルの定義です。このようなたくさんのパラメータ設定により色々なエフェクトを作ることができます。

ソースコードは以下のようになります。

#include "Aqualead.h"

enum {
    ATTRIBUTE_PLAYER        = 1 << 0,
    ATTRIBUTE_PLAYER_SHOT   = 1 << 1,
    ATTRIBUTE_ENEMY         = 1 << 2,
    ATTRIBUTE_ENEMY_SHOT    = 1 << 3
};

void ALInit()
{
    ALStream::SetBasePath( "..\\..\\..\\..\\Data\\" );
}

void EnemyUpdate( ALNode * node )
{
    for( int i=ALRandInt(30)+20; i>=0; --i ){
        node->AddX( -3 );
        ALYield();
    };
    ALYield( ALRandInt(20)+10 );
    ALNode *bul = ALNode::Assemble( 0x20100 );
    ALNode *player = ( ALNode * )node->GetUserData();
    bul->AddController( ALAngleSpeedController::Create( ALArcTan2( 
        player->GetY()-node->GetY(), player->GetX()-node->GetX() ), 10 ) );
    bul->SetPos( node->GetPos() );

    float yp = ALRandFloat( 8 )-4;
    for( int i=ALRandInt(30)+20; i>=0; --i ){
        node->AddX( -3 );
        node->AddY( yp );
        ALYield();
    };
    ALYield( ALRandInt(20)+10 );
};

void EnemyHitFunc( ALNode *self, ALCollision * selfcol
                  , ALCollision * targetcol, bool * cancel )
{
    self->AddController( ALFadeoutDestroyController::Create( 5 ) );
    self->SetCollisionEnable( false );
    ALNode *eff = ALNode::Assemble( 0x30001 );                      (1)
    eff->SetPos( self->GetPos() );
};

void PlayerHitFunc( ALNode *self, ALCollision * selfcol
                   , ALCollision * targetcol, bool * cancel )
{
    self->SetUserData( ( void * )1 );                               (2)
};

void PlayerUpdateFunc( ALNode * self )
{
    if( self->GetUserData() != 0 ){                                 (3)
        self->SetCollisionEnable( false );
        self->Hide();                                               (4)
        ALNode *eff = ALNode::Assemble( 0x30000 );                  (5)
        eff->SetPos( self->GetPos() );
        ALYield( 60 );                                              (6)
        self->SetPos( 64, 240 );
        self->SetUserData( 0 );                                     (7)
        for( Uint32 i=0; i<30; i++ ){                               (8)
            self->Hide();
            ALYield();
            self->Show();
            ALYield();
        };
        self->SetCollisionEnable( true );                           (9)
    };
};

プレイヤー周りのコールバックと、コリジョンマネージャの設定が増えました。

(1)

敵破壊エフェクトを生成します。 オブジェクト定義ファイルにほとんどの記述があるので、座標のみ敵の座標に合わせます。 このエフェクトはオブジェクト定義ファイルの設定で、再生が終わると自殺する設定になっており、 またパーティクルの再生指示も含まれているため、生成するだけで再生・破棄が行われます。

(2)

プレイヤーに敵か弾がヒットした場合、そのフラグとしてUserDataに1を設定します。

(3)

プレイヤーに敵か弾がヒットしたかをUserDataを利用してチェックします。

(4)

次のプレイヤーが登場するまで、いったん表示をオフにします。 表示をオフにしても、更新やコントローラは有効のままですので注意してください。

(5)

プレイヤー死亡エフェクトを生成します。

(6)

1秒そのまま待ち、その後プレイヤーの位置を戻します。

(7)

ヒットしたフラグをクリアします。

(8)

復活後、1秒の無敵時間を作ります。そのために、表示のオンオフのループを行います。

(9)

無敵時間終了後、コリジョンを有効に戻し通常状態に戻します。

void ALMain()
{
    ALCollisionManager::GetDefault()->SetTargetAttribute( 
        ATTRIBUTE_PLAYER_SHOT, ATTRIBUTE_ENEMY );
    ALCollisionManager::GetDefault()->SetTargetAttribute( 
        ATTRIBUTE_ENEMY | ATTRIBUTE_ENEMY_SHOT, ATTRIBUTE_PLAYER ); (1)
    ALCollisionManager::GetDefault()->ShowAll();

    ALArchive *ar = ALArchive::Create( "GameData.aar" );

    ALNode *spr = ALNode::Assemble( 0x10000 );
    spr->SetHitFunc( PlayerHitFunc );                               (2)
    spr->SetUpdateFunc( PlayerUpdateFunc );
    spr->PlayMotion();

    ALPad * pad = ALPad::GetDevice( 0 );
    while( !ALSystem::IsTerminated() ){
        if( ( pad->GetDigitalTrig() & ALPAD_A ) 
                && ( spr->GetUserData() == 0 ) ){                   (3)
            ALNode *ps = ALNode::Assemble( 0x10100 );
            ps->SetPos( spr->GetPos() );
        };
        if( ALRandInt( 60 ) == 0 ){
            ALNode * enemy = ALNode::Assemble( 0x20000 );
            enemy->SetUserData( spr );
            enemy->PlayMotion();
            enemy->SetPos( 660, ALRandFloat( 400 )+40 );
            enemy->SetUpdateFunc( EnemyUpdate );
            enemy->SetHitFunc( EnemyHitFunc );
        };
        ALYield();
    };
    spr->Destroy();
    ar->Release();
}

(1)

プレイヤーに敵と弾がヒットするようにコリジョンマネージャに設定を追加します。

(2)

プレイヤーにヒットコールバックを追加します。

(3)

死亡中に弾を撃てると問題があるので、ヒットフラグが立っていないときのみ弾を撃てるようにします。

このチュートリアルはこれで終わりです。

第10章 最後に

いかがだったでしょうか。 このチュートリアルでは、Aqualeadの基本機能を簡単に使用してみました。 普通であれば必要な処理の大半を記述しなくてもゲームが作れたと思います。

しかも、これらのクラスはすべて簡易機能などではなくプロの使用に耐える能力を持っています。 今回紹介しなかった関数を使用すると、さらに高度な機能がいろいろ使用できます。

また今回使用しなかったクラスにも便利なクラスがたくさんあります。

  • 3Dモデルを表示するためのALModel

  • 画像やアーカイブのロードを自動化するALResource

  • タイトル、メニュー画面、ゲーム画面等の画面遷移を行うALTask

  • 簡易データベース機能を実装するALTable

  • 複数のノードをまとめて扱うALGroupNode

  • 各種パラメータへの動的なアクセス手段を提供するALProp

  • 値の補間を自動的に行うALIPController

  • スクリプトインターフェースを提供するALSquirrel

  • 各種メニューを構築できるALWidget

  • ノードの一括削除や、グループごとのリストアップができるALScene

  • 各種内部パラメータの確認や、ユーザーパラメータの変更ができるALDebugMenu

  • ノードに対しメッセージが送れるALMessage

  • テキストの表示ができるALText

などなど。

これらは、以降のチュートリアルで扱っていきます。

なお、このサンプルを作るにあたり、以下のフリー素材を使わせていただきました。ありがとうございました。

J-JSoft HomePage MACKさま