Aqualeadチュートリアル 2


目次

1. 概要
2. プロパティと補間コントローラ
3. メッセージ
4. マーカ
5. シーンとファミリービット
6. ジェネレータとグループノード
7. スクリプト
8. 最後に

第1章 概要

このチュートリアルでは、チュートリアル1で作ったサンプルシューティングを元にします。 チュートリアル1のサンプルシューティングは、一部の機能だけを説明する都合上、あまりスマートじゃない実装になっている箇所があります。 このチュートリアルでは、今まで説明しなかったAqualeadの機能を使い、機能は変更せずコードを書き直します。

チュートリアル1に比べると難易度は上がります。 このチュートリアルの内容が理解できなくてもAqualeadを使うことはできますが、 使えるようになると、より短く、簡単にゲームを作ることができるようになります。

また、チュートリアル1は完全にサンプル用のコードですが、 このチュートリアルの修正によって、比較的実践的なコードになります。

第2章 プロパティと補間コントローラ

まずは、プレイヤーの処理周りを修正しましょう。 今はプレイヤーの処理がPlayerUpdateFuncによる死亡処理と、メインループによる弾発射処理に分かれています。 これを、プレイヤー処理として独立させることにします。

まず修正が必要なのは、プレイヤー死亡処理です。 ここはファイバにより処理が作られているため、このため弾発射処理を同時に処理することができません。

まずは、現在forループを行っている点滅処理を修正します。 これにはALIPControllerという補間コントローラを使用します。 これは指定したパラメータを、指定した補間方法で繰り返し補間を行うコントローラです。 補間には、指定した2値のみを使用する補間なし、線形補間、ベジエ補間、Sin波、2次補間などがあります。 今回は表示フラグに対して、補間なしでtrueとfalseを交互に設定します。 ちなみに、繰り返しではなく指定した値へ補完する場合には、ALIPMoveControllerというコントローラなどもあります。

パラメータを指定するには、Aqualeadの特徴的な機能の一つでもあるプロパティ(ALProp)を使用します。 プロパティとは、ALNodeやALUpdater等の各種パラメータに文字列経由でアクセスする仕組みです。 組み込みのプロパティだけではなく、ユーザーが任意のプロパティを追加することも可能です。 モーションの仕組みもこのプロパティを使用しています。

補間コントローラはこのプロパティの仕組みを使用することで、 いろいろなパラメータに対し処理を行うことが可能になります。

このコントローラを設定したままではずっと点滅をしてしまうため、 無敵の時間が終わった後にはそのコントローラを削除します。

コントローラを削除するには、コントローラのインスタンスをReleaseします。 このために変数にコントローラのインスタンスを保持する方法もありますが、 今回はコントローラを検索して削除してみます。

各コントローラには種別を区別するためのALCONTROLLER_TYPEという定義があり、 今回の補間コントローラではALCONTROLLER_TYPE_IPという定数か、 ALIPController::CONTROLLER_TYPEというどちらかの定数を使います。 ちなみに、生成済みのインスタンスからその種別を取得するにはGetControllerType()関数を使います。

ノードのClearControllerByType()という関数で、指定した種別のコントローラを一斉削除できるので、 今回はこの関数を使用します。

なお、他に一定時間後に他のコントローラを停止させるALDelayStopControllerを使う方法などもあります。

void PlayerUpdateFunc( ALNode * self )
{
    if( self->GetUserData() != 0 ){
        self->SetCollisionEnable( false );
        self->Hide();
        ALNode *eff = ALNode::Assemble( 0x30000 );
        eff->SetPos( self->GetPos() );
        ALYield( 60 );
        self->SetPos( 64, 240 );
        self->SetUserData( 0 );
        self->AddController( ALIPController::Create(                    (1)
            self->FindProp("Disp"), ALIP_TYPE_NONE, true, false, 2 ) );
        ALYield( 60 );
        self->ClearControllerByType( ALIPController::CONTROLLER_TYPE ); (2)
        self->Show();                                                   (3)
        self->SetCollisionEnable( true );
    };
};

(1)

補間コントローラを生成します。引数は対象プロパティ、補間種別、補間する値1、補完する値2、周期です。 今回は、Dispプロパティにtrueとfalseの値を交互に設定することになります。

このほか、補間処理を前半のみ行ったり、一度のみ実行する指定などがあります。

対象のプロパティの指定は、主にこのようなFindProp関数で検索して指定します。

(2)

指定した種別のコントローラを一括削除します。

(3)

補間コントローラ削除時は、その時点での値がそのまま維持されるため、期間の指定によっては非表示のままになります。

そのため、ここで再表示を行います。

第3章 メッセージ

先ほどの修正で、プレイヤー死亡処理はそれぞれタイミングの違う死亡直後、無敵開始、無敵終了の三つに分かれました。 この処理を、メッセージ(ALMessage)を使って書き直します。

メッセージとはALUpdaterやALNode等のアップデータから別のオブジェクトに通知を送るものです。 通常の関数呼び出しとは違い、処理は送り先のアップデータのアップデート処理で行われます。 このため、送り元と送り先のアップデートプライオリティの設定により、 そのフレームに実行されるか、次のフレームに実行されるかが変わります。

もう一つの特徴として、一定時間後に届く用にメッセージを送ることができます。 これを利用すると、一定フレーム後に実行したい処理を簡単に記述することができます。 今回は、この遅延メッセージ処理を使用してプレイヤー死亡処理を書き直してみます。

届いたメッセージはオーバライドしたDoMessage関数か、 SetMessageFunc関数で設定したコールバック関数で受信することができます。 これらの設定がない場合は、受信したメッセージはそのまま捨てられます。 このため、送り側は受信側がそのメッセージを処理できるかを気にする必要はなくなります。

メッセージには任意に設定するメッセージIDと、二つの整数、もしくはポインタを設定することができます。 今回は特にパラメータの必要はないので、メッセージIDのみを使用します。

#include "Aqualead.h"

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

enum {
    MESSAGE_PLAYER_OUT,                             (1)
    MESSAGE_PLAYER_INVINCIBILITY_END,
};

(1)

メッセージID用のシンボルを定義します。メッセージIDには任意の正の数値が使用できます。


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

void PlayerMessageFunc( ALNode * self, ALMessage * mes )                            (2)
{
    switch( mes->GetMessageID() ){                                                  (3)
        case MESSAGE_PLAYER_OUT:
        {
            self->SetCollisionEnable( false );
            self->Hide();
            ALNode *eff = ALNode::Assemble( 0x30000 );
            eff->SetPos( self->GetPos() );
            ALYield( 60 );                                                          (4)
            self->SetPos( 64, 240 );
            self->Show();
            self->AddController( 
                ALIPController::Create( self->FindProp("Disp"), ALIP_TYPE_NONE, true, false, 2 ) );
            self->PostDelayMessage( self, MESSAGE_PLAYER_INVINCIBILITY_END, 60 );   (5)
            break;
        }
        case MESSAGE_PLAYER_INVINCIBILITY_END:                                      (6)
        {
            self->ClearControllerByType( ALIPController::CONTROLLER_TYPE );
            self->Show();
            self->SetCollisionEnable( true );
            break;
        }
    }
}

(1)

今まではUserDataをフラグとして使用していましたが、その代わりにメッセージを送信します。今回は自分自身へのメッセージ送信です。

(2)

メッセージ処理用コールバック関数です。自分自身と受信したメッセージを引数として受け取ります。

(3)

メッセージIDで処理を分岐します。各処理は分離しただけで従来と同一です。

(4)

死亡して復活までの間はALYieldで停止します。

メッセージ処理はアップデート処理の一部でもあるので、この間はPlayerUpdateFuncも呼び出されなくなります。

(5)

無敵処理終了のために、遅延メッセージを自分自身に送ります。この例では60フレーム後にメッセージが届きます。

(6)

遅延メッセージが届いた時の処理です。処理の中身は前と変わらず無敵の終了処理です。


void PlayerUpdateFunc( ALNode * self )
{
    if( ( ALPad::GetDevice( 0 )->GetDigitalTrig() & ALPAD_A ) ){    (1)
        ALNode *ps = ALNode::Assemble( 0x10100 );
        ps->SetPos( self->GetPos() );
    };
};
void ALMain()
{
    ALCollisionManager::GetDefault()->SetTargetAttribute( 
        ATTRIBUTE_PLAYER_SHOT, ATTRIBUTE_ENEMY );
    ALCollisionManager::GetDefault()->SetTargetAttribute( 
        ATTRIBUTE_ENEMY | ATTRIBUTE_ENEMY_SHOT, ATTRIBUTE_PLAYER );
    ALCollisionManager::GetDefault()->ShowAll();

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

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

    while( !ALSystem::IsTerminated() ){
        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)

メッセージ処理内でALYieldを実行するため、プレイヤー死亡中はここは呼び出されません。

そのため、従来UserDataで判断していた死亡フラグのチェックが必要なくなりました。

(2)

メッセージ処理用コールバックを登録します。

これにより、プレイヤー死亡の処理がメッセージで行われるようになったため、 プレイヤーの弾発射処理をアップデート処理で行えるようになりました。

今回はこのような方法をとりましたが、ユーザー関数を実行できるコントローラを使用し、 弾の発射処理をそのコントローラに任せるという方法もあります。

第4章 マーカ

メッセージでプレイヤーの処理を整理しましたが、処理コード自体は増えました。 この章ではマーカを使用して処理をデータに移してコードを削減してみます。

マーカとは、モーションで使用するものでモーションのフレームに目印をつけるものです。 プログラムからは特定のフレームへ移動する際、 フレーム数ではなくマーカを使えばモーションの長さが変化しても正しく動作させることができます。

また、特殊なマーカを使うとマーカ間をループさせることができるようになり、 一つのモーションを

開始モーション -> ループするメインモーション -> 終了モーション

として構成することができます。

今回のプレイヤー処理では、無敵部分を開始モーション、 メインモーションは今まで通り、終了モーションにプレイヤー死亡処理を入れることで、 プレイヤー死亡処理を数行まで縮めることが可能になります。

まず、sodファイルにコントローラ定義を追加します。

[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
Controller=IP,0.Disp,0,Disp(1),Disp(0),2,0,0,0

IPコントローラを追加します。

コントローラはモーション中に生成することはできないため、sodで生成し、モーション中はSleepプロパティを使用して動作・停止を切り替えます。

次に、smtファイルにマーカ処理を追加します。

[Global]
RepeatFlg=false
[0:0]
CollisionEnable=0
IPController.Sleep=0
[0:60]
CollisionEnable=1
IPController.Sleep=1
PatternNo=0
Marker=LoopStart
[0:72]
PatternNo=1
[0:84]
PatternNo=0
Marker=LoopEnd

マーカ処理、CollisionEnableフラグ、IPController.Sleepが追加されました。

マーカは今回ループ処理を使うので、LoopStartとLoopEndを設定します。ループを使用しない場合は、任意の16ビットの数値が使用できます。

また、コントローラのプロパティはIPController.Sleepのような形で指定します。ただし、本来はIP.Sleepという形で指定しますが補間コントローラのみの特例でIPControllerとController付きで指定します。

以下はソース側の変更点です。

#include "Aqualead.h"

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

enum {
    MESSAGE_PLAYER_OUT                  (1)
};

(1)

メッセージを1種類のみ定義します。

void PlayerMessageFunc( ALNode * self, ALMessage * mes )
{
    switch( mes->GetMessageID() ){
        case MESSAGE_PLAYER_OUT:                    (1)
        {
            self->Hide();
            ALNode *eff = ALNode::Assemble( 0x30000 );
            eff->SetPos( self->GetPos() );
            ALYield( 60 );
            self->JumpMotionFrame( 0 );             (2)
            self->SetPos( 64, 240 );
            break;
        }
    }
}

void PlayerUpdateFunc( ALNode * self )
{
    if( ( ALPad::GetDevice( 0 )->GetDigitalTrig() & ALPAD_A ) ){
        ALNode *ps = ALNode::Assemble( 0x10100 );
        ps->SetPos( self->GetPos() );
    };
};
void ALMain()
{
    ALCollisionManager::GetDefault()->SetTargetAttribute( 
        ATTRIBUTE_PLAYER_SHOT, ATTRIBUTE_ENEMY );
    ALCollisionManager::GetDefault()->SetTargetAttribute( 
        ATTRIBUTE_ENEMY | ATTRIBUTE_ENEMY_SHOT, ATTRIBUTE_PLAYER );
    ALCollisionManager::GetDefault()->ShowAll();

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

    ALNode *spr = ALNode::Assemble( 0x10000 );
    spr->SetHitFunc( PlayerHitFunc );
    spr->SetUpdateFunc( PlayerUpdateFunc );
    spr->SetMessageFunc( PlayerMessageFunc );
    spr->JumpMotionMarkerIndex( 1 );                (3)
    spr->PlayMotion();

    while( !ALSystem::IsTerminated() ){
        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)

今回の処理でメッセージが一つになりました。そのため、今回のみであればswitch処理を外すことも可能です。

(2)

今まではモーションはループしっぱなしでしたが、今回からモーションを0フレームに戻す必要があります。

(3)

モーションの頭には無敵処理が入っているので、初回はそのマーカをスキップします。

今回の修正で、コードは減りましたがデータの定義は増えました。このチュートリアルではデータ定義ファイルは手動で生成しましたが、本来はGUIツールで生成します。

第5章 シーンとファミリービット

前回までの修正で、プレイヤーの処理の修正はほぼ終わりました。

今回のチュートリアルでは特に必要のない変更ですが、この章ではプレイヤーの取得処理を変更します。 今の処理では、敵が生成時に渡されたプレイヤーのアドレスを保持しているため、 プレイヤーを再生成することがあるとそのアドレスが無効になり、プレイヤーへのアクセスができなくなります。

通常は何かしらのマネージャ処理がプレイヤーのアドレスを保持しますが、 今回はAqualeadのシーンとファミリービットを使用して、プレイヤーを取得してみます。

シーン(ALScene)とは、ALUpdaterやALNode等のアップデータを管理するためのクラスです。 システム起動時にデフォルトとなるシーンが生成され、 特に指定しなければ生成したアップデータはデフォルトシーンに所属します。

シーンを削除すると、所属するアップデータは全て削除されます。 そのため、必要に応じてシーンを生成し、デフォルトに設定して使い、 その後シーンを削除すれば各種アップデータの後始末をしなくても、シーンが削除を行ってくれます。 例えば、メニューを開く時などに独立シーンを使えば、メニュー関連の解放ミスがなくなります。

また、シーン単位で一斉にアップデータをスリープさせることも可能です。 これはシーン一括の他、ファミリービットというものを使用すると、一部のグループのみスリープなどができるようになります。

ファミリービットとはアップデータにある32ビットのフラグで、 ビットフィールドとして使用します。 このファミリービットはスリープだけではなく、指定したファミリービットのノードを列挙することもできます。 このノードのリストは生成時だけではなく、ノードの追加削除に応じて更新されます。

また、今回は使用しませんがスクリーン(ALScreen)を使用すると、 ファミリービットを使用して一部ノードを非表示にすることができます。 たとえば、一時的に敵のみ非表示にするなどが、簡単に可能になります。

今回はこのファミリービットをプレイヤー取得の為に使用します。 ファミリービットはSetFamilyBits関数で設定します。

void EnemyUpdate( ALNode * node )
{
    for( int i=ALRandInt(30)+20; i>=0; --i ){
        node->AddX( -3 );
        ALYield();
    };
    ALYield( ALRandInt(20)+10 );
    ALNodeArray *na = ALScene::GetDefault()->CreateFamilyNodeArray( ATTRIBUTE_PLAYER ); (1)
    if( na->GetCount() > 0 ){                                                           (2)
        ALNode *bul = ALNode::Assemble( 0x20100 );
        ALNode *player = na->Get( 0 );                                                  (3)
        bul->AddController( ALAngleSpeedController::Create( ALArcTan2( 
            player->GetY()-node->GetY(), player->GetX()-node->GetX() ), 10 ) );
        bul->SetPos( node->GetPos() );
    }
    na->Release();                                                                      (4)

    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 );
};

(1)

デフォルトシーンから、指定したファミリービットのノード配列を生成します。

今回は必要がある度にノード配列を生成していますが、生成したノード配列をそのまま保持する方法もあります。 その場合、対象ノードの増減があればノード配列の中身も同時に更新されます。

(2)

ノード配列の個数を確認します。今回のチュートリアルでは常に1になります。

(3)

ノード配列からノードを取得します。

(4)

ノード配列を削除します。

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

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

    ALNode *spr = ALNode::Assemble( 0x10000 );
    spr->SetHitFunc( PlayerHitFunc );
    spr->SetUpdateFunc( PlayerUpdateFunc );
    spr->SetMessageFunc( PlayerMessageFunc );
    spr->JumpMotionMarkerIndex( 1 );
    spr->SetFamilyBits( ATTRIBUTE_PLAYER );                                 (1)
    spr->PlayMotion();

    while( !ALSystem::IsTerminated() ){
        if( ALRandInt( 60 ) == 0 ){
            ALNode * enemy = ALNode::Assemble( 0x20000 );
            enemy->PlayMotion();
            enemy->SetPos( 660, ALRandFloat( 400 )+40 );
            enemy->SetUpdateFunc( EnemyUpdate );
            enemy->SetHitFunc( EnemyHitFunc );
        };
        ALYield();
    };
    spr->Destroy();
    ar->Release();
}

(1)

プレイヤーにファミリービットを設定します。

今回はコリジョンアトリビュートと同じ値を使用していますが、別の値にすることも可能です。

sodファイルに埋め込むことも可能です。

これにより、プレイヤーのインスタンスを一度破棄しても問題がなくなりました。

なお、今回は扱いませんが敵に誘導弾を撃たせたい場合など、 新規にプレイヤーのアドレスを取得する必要はないが、消滅すると困る場合などがあればハンドル(ALHandle)を使うこともできます。 このハンドル経由でアドレスを取得すると、対象が消滅するとNULLになるため不正なメモリにアクセスすることを防ぐことができます。

第6章 ジェネレータとグループノード

今までの修正で、ゲーム処理のかなりの部分をデータで記述できるようになりました。 次は、エフェクト処理もデータで記述してみましょう。

エフェクトなど、生成して自殺するタイプのノードをモーションから生成するにはジェネレータ(ALGenerator)を使用します。 ジェネレータは名前の通り、指定したIDのノードを生成するノードです。 生成するノードはAssembleできる形式で、基本は自殺するタイプを使用します。

ただし、ジェネレータは生成したノードは全て保持しているので、 プログラムから生成したノードを取得し、制御を行ったり削除をすることも可能です。

ジェネレータで生成するノードは、ジェネレータ自体の位置と向きがコピーされて生成されます。 そのため、ジェネレータを移動することでエフェクトの発生位置や向きを制御することが可能です。

なお、ジェネレータは1フレームに一つしかノードを生成できないため、 同時に複数のノードを生成したい場合は、ジェネレータも複数用意する必要があります。

このジェネレータを使う場合、sodやsmtにジェネレータを追加する必要があります。 その時必要になるのがグループノード(ALGroupNode)です。

グループノードとは、複数のノードをグループ化し、擬似的に一つのノードとして扱うノードです。 複数スプライトで構成するキャラクターや、3Dモデルなどはこのグループノードを使用してグループ化します。

通常、グループノードに登録したグループはグループノード破棄時に一緒に破棄されます。 また、デフォルトでは親子関係や色、表示などはグループノードの設定を引き継ぐため、 細かな制御が必要な場合をのぞいて、対象がグループか、単一ノードかを意識する必要はありません。

ノードにはそれぞれ32ビットのIDをつけて、それで識別を行います。

まず、sodファイルを書き換えます。

[0]
Type=GroupNode
Controller=PadMove8,0,4,0,0,640,480,0
Controller=IP,0.Disp,0,Disp(1),Disp(0),2,0,0,0
FamilyBits=1
Disp=1
Position=64,240,0

[1]
Type=Sprite
Texture0ID=0x10000
Collision[0].ShapeID=2CIR
Collision[0].Radius=16.0
Collision[0].Attribute=1

[2]
Type=Generator

この用に、ID0のグループノード、ID1のスプライト、ID2ジェネレータの三つのノードを定義します。なお、グループノードの定義がなくても、複数ノードを定義すると自動的にグループノードが生成されます。

今までスプライトに定義していた、コントローラ、座標情報などをグループノードに移動します。このようにしないと、コントローラ等の対象がスプライトのみになってしまい、ジェネレータの座標が連動しなくなります。なお、ついでにファミリービットの定義も追加します。

コリジョンはスプライトに設定したままです。これは処理の作り方にもよりますが、グループノードに移動してもしなくてもどちらでもかまいません。

次に、smtファイルを書き換えます。

[Global]
RepeatFlg=false

[0:0]
IPController.Sleep=0
[0:60]
IPController.Sleep=1

[1:0]
CollisionEnable=0
[1:60]
CollisionEnable=1
PatternNo=0
Marker=LoopStart
[1:72]
PatternNo=1
[1:84]
PatternNo=0
Marker=LoopEnd

[2:84]
GenerateID=0x30000

今までスプライトの処理は[0:~]になっていた箇所が[1:~]になっている点に注意してください。sodでスプライトのIDは1になったのでそれに合わせます。

コントローラはグループノードに移動したので、コントローラ関連はグループノードに移動します。

マーカは、どのノードに指定しても常にグループ全体に作用するので、どこのノードに指定してもかまいません。

そして、最後にジェネレータの指定を追加します。この例では108フレーム目、つまりマーカループが終了した時に、0x30000で指定するエフェクトを生成します。

なお、同様に敵にもエフェクト生成を埋め込みます。

[0]
Type=GroupNode
Controller=AreaOverDestroy,0,0,640,480,1
Controller=FadeoutDestroy,5

[1]
Type=Sprite
Texture0ID=0x20000
Disp=1
Collision[0].ShapeID=2CIR
Collision[0].Radius=16
Collision[0].Attribute=4

[2]
Type=Generator
[Global]
RepeatFlg=false

[0:0]
FadeoutDestroy.Sleep=1
[0:12]
FadeoutDestroy.Sleep=0

[1:0]
PatternNo=0
Marker=LoopStart
CollisionEnable=1
[1:6]
PatternNo=1
[1:12]
PatternNo=0
Marker=LoopEnd
CollisionEnable=0

[2:12]
GenerateID=0x30001

ソース側は、エフェクトの生成処理をそのまま削ります。

void EnemyHitFunc( ALNode *self, ALCollision * selfcol
                  , ALCollision * targetcol, bool * cancel )
{
    self->SkipMotionLoop();                                     (1)
};

void PlayerHitFunc( ALNode *self, ALCollision * selfcol
                   , ALCollision * targetcol, bool * cancel )
{
    self->PostMessage( self, MESSAGE_PLAYER_OUT );
};

void PlayerMessageFunc( ALNode * self, ALMessage * mes )
{
    switch( mes->GetMessageID() ){
        case MESSAGE_PLAYER_OUT:
        {
            self->SkipMotionLoop();                             (2)
            self->Hide();
            ALYield( 60 );
            self->JumpMotionFrame( 0 );
            self->PlayMotion();
            self->SetPos( 64, 240 );
            break;
        }
    }
}

(1)

エフェクトの生成処理を削り、SkipMotionLoopを呼び出すようにしています。

SkipMotionLoopは現在実行中のマーカループを瞬時に終了し、ループ終了フレームに移動する関数です。

ALFadeoutDestroyControllerもデータに定義したので、コードからは削除しています。

(2)

敵処理と同様に、マーカループを抜け出します。抜け出すと同時にジェネレータで指定したエフェクトが生成されます。

第7章 スクリプト

これでプレイヤー周りのコードはほとんどなくなりました。 次は敵周りに手を入れてみます。

しかし、敵は独自の動きを行うため単純にはいきません。 そのため、この敵の動きをスクリプトとして実装してみます。

使用するスクリプト言語はSquirrelです。 言語仕様はCに似ており、Aqualeadの関数はほぼ全てがSquirrelからもそのまま使えるため、 少々の変更でC++からSquirrelへ移植することが可能です。

スクリプトで記述することにより、動作速度は低下しますが、 プログラムをコンパイルしなくても敵の動きを変更することができるようになり、 調整作業が行いやすくなります。

Squirrelはそのまま使うこともできますが、コントローラの形で動作させることもできます。 今回のようにアップデート処理を置き換える場合は、ALSquirrelFiberControllerを使用します。

これがSquirrelに移植した敵アップデート処理です。

function EnemyUpdate()
{
    while( 1 ){
        for( local i=ALRandInt(30)+20; i>=0; --i ){
            Pos.x -= 3
            ALYield()
        }
        ALYield( ALRandInt(20)+10 )

        local na = ALScene.GetDefault().CreateFamilyNodeArray( ATTRIBUTE_PLAYER )
        if( na.GetCount() > 0 ){
            local bul = ALNode.Assemble( 0x20100 )
            local player = na.Get( 0 )
            bul.AddController( ALAngleSpeedController.Create( ALArcTan2( 
                player.GetY()-GetY(), player.GetX()-GetX() ), 10 ) )
            bul.SetPos( GetPos() )
        }
        na.Release()
        local yp = ALRandFloat( 8 )-4
        for( local i=ALRandInt(30)+20; i>=0; --i ){
            Pos.x -= 3
            Pos.y += yp
            ALYield()
        }
        ALYield( ALRandInt(20)+10 )
    }
}

C++のソースとそっくりなことが確認できると思います。

大きな違いは、関数定義にfunctionを記述、::や->は全て.に、変数定義は全てlocalになることです。

また、座標の移動がAddPos関数ではなくなっています。スクリプトでは、プロパティに直接読み書きを行うことが可能になっており、それを利用して書き換えています。ただし、元の関数もそのまま使用可能なので、好きな方法を使うことができます。

それとは別に、全てがwhileループの中に入っています。これはALSquirrelFiberControllerの仕様です。 このコントローラは関数が終了するまで実行を行い、通常のアップデートのように何度も呼び出したりはしません。 そのためこのようにwhileを使い、無限ループを構成しています。 ちなみに、C++のアップデート処理をこのように無限ループで構成することもできます。

プログラムコードは以下のようになります。この章で最後なので、全ソースを表示します。

#include "Aqualead.h"

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

enum {
    MESSAGE_PLAYER_OUT,
};

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

void EnemyHitFunc( ALNode *self, ALCollision * selfcol
                  , ALCollision * targetcol, bool * cancel )
{
    self->SkipMotionLoop();
};

void PlayerHitFunc( ALNode *self, ALCollision * selfcol
                   , ALCollision * targetcol, bool * cancel )
{
    self->PostMessage( self, MESSAGE_PLAYER_OUT );
};

void PlayerMessageFunc( ALNode * self, ALMessage * mes )
{
    switch( mes->GetMessageID() ){
        case MESSAGE_PLAYER_OUT:
        {
            self->SkipMotionLoop();
            self->Hide();
            ALYield( 60 );
            self->JumpMotionFrame( 0 );
            self->PlayMotion();
            self->SetPos( 64, 240 );
            break;
        }
    }
}

void PlayerUpdateFunc( ALNode * self )
{
    if( ( ALPad::GetDevice( 0 )->GetDigitalTrig() & ALPAD_A ) ){
        ALNode *ps = ALNode::Assemble( 0x10100 );
        ps->SetPos( self->GetPos() );
    };
};

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

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

    ALNode *spr = ALNode::Assemble( 0x10000 );
    spr->SetHitFunc( PlayerHitFunc );
    spr->SetUpdateFunc( PlayerUpdateFunc );
    spr->SetMessageFunc( PlayerMessageFunc );
    spr->JumpMotionMarkerIndex( 1 );
    spr->PlayMotion();

    ALSquirrel *sq = ALSquirrel::Create();                      (1)
    sq->Load("EnemyScript.nut");                                (2)
    sq->SetGlobalValue( "ATTRIBUTE_PLAYER", ATTRIBUTE_PLAYER ); (3)

    while( !ALSystem::IsTerminated() ){
        if( ALRandInt( 60 ) == 0 ){
            ALNode * enemy = ALNode::Assemble( 0x20000 );
            enemy->PlayMotion();
            enemy->SetPos( 660, ALRandFloat( 400 )+40 );
            enemy->AddController(                               (4)
                ALSquirrelFiberController::Create( sq, "EnemyUpdate" ) );
            enemy->SetHitFunc( EnemyHitFunc );
        };
        ALYield();
    };
    sq->Release();                                              (5)
    spr->Destroy();
    ar->Release();
}

(1)

Squirrelを処理クラスを生成します。

(2)

スクリプトを読み込みます。関数定義以外の記述があればこの時点で実行されます。

(3)

ATTRIBUTE_PLAYER定数をスクリプトから使えるように定義します。

(4)

アップデート関数の代わりに、ALSquirrelFiberControllerを追加します。引数は使用するALSquirrelのインスタンスと、呼び出す関数名です。

(5)

ALSquirrelのインスタンスを破棄します。

第8章 最後に

いかがだったでしょうか。 このチュートリアルでは、Aqualeadの機能を最大限に使うことを目標にしているため、実際には別の組み方をした方がいい箇所もあります。

しかし、最終的なコードではほとんどが初期化コードになり、ゲームのメインとなるC++のコードは圧倒的に少なくなりました。 もちろん、一部はスクリプトを使用しているため、その分を合計すればもっと増えますが、 C++のコードを少なくすることで、バグの発生をかなり防ぐことができます。 その上、チュートリアル1に比べ、実際のゲームに使うための拡張も容易な形になっています。

このチュートリアルではゲーム自体の動作は一切変更しませんでしたが、 次のチュートリアルではこのソースをベースに、もっとゲームらしい形に拡張を行います。

そのチュートリアルで作ったソースは、実際のゲームを作るひな形として十分に使用することができるはずです。

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

J-JSoft HomePage MACKさま