Aqualeadチュートリアル 3


目次

1. 概要
2. サウンド
3. テーブル
4. エフェクト
5. テキストとフォント
6. タスクとフェード
7. リソース管理
8. デバッグ機能
9. 最後に

第1章 概要

チュートリアル2では、動作を変えずにソースをAqualead流に書き換えました。 このチュートリアルでは、チュートリアル2のソースを元に、 これを実際にゲームを作るための基礎として使えるような形に拡張を行います。

効果音をつけ、スコア表示を追加し、簡易的なタイトルゲームオーバー処理を作ります。 また、敵の種類を増やし、敵の動きはスクリプトで記述します。 そして、実際に作って行く上で役にたつデバッグ機能を組み込みます。

今までのコードは単なるサンプルに過ぎませんが、 このチュートリアルで作るコードであれば、このまま実際のゲームに使えるように拡張していくことが可能です。

第2章 サウンド

まずは、このサンプルに効果音をつけてみましょう。 効果音を再生するには、ALSoundPlayという関数にサウンドファイル名を渡すだけです。 使用するサウンドファイルはwavファイルです。 ただし、効果音の事前準備がないとその時点で毎回サウンドファイルロードが発生するため非効率です。

サウンドのロードは、ALSoundクラスのCreate関数を使用します。 このインスタンスに対してPlayを実行すれば効率的に効果音を再生することができます。

また、前述のALSoundPlay関数はロード済みの効果音があればそれを優先するため、 ALSoundクラスではロードだけを行い、再生はALSoundPlay関数という方法もあります。

しかし、これでは使用する効果音の数だけセットアップが発生するため、非常に面倒です。 そのため、アーカイブに存在するPrepare機能を使います。

Prepare機能とは、アーカイブロード時にPrepareフラグが付いているリソースを自動的にセットアップし、 アーカイブ破棄時にそのリソースの破棄を行う機能です。 サウンドの他、テクスチャなどでも使用が可能です。

今回はこのPrepare機能と、ALSoundPlay関数を使用します。

まずは、書き換えたsarファイルです。

[Prepare]
Player.atx=0x10000
PlayerShot.atx=0x10100
Enemy.atx=0x20000
Enemy2.atx=0x20001
Bullet.atx=0x20100
PlayerOut.atx=0x30000
EnemyOut.atx=0x30001
tm2_shoot003.wav=0x40000
tm2_gun002.wav=0x40001
tm2_gun007_miniguned.wav=0x40002

[Keep]
Player.aod=0x10000
PlayerShot.aod=0x10100
Enemy.aod=0x20000
Enemy2.aod=0x20001
Bullet.aod=0x20100
PlayerOut.aod=0x30000
EnemyOut.aod=0x30001

今まではIDとファイル名のみの列挙でしたが、 [Prepare]と[Keep]と言うセクションが追加されています。

このうち[Prepare]のセクションに存在するファイルがPrepare対象ファイルで、 アーカイブロード時にメモリ上にインスタンスが生成され、アーカイブ解放時に破棄されます。

今まで説明をしませんでしたが、テクスチャもそのままでは使用の度に生成・解放が行われてしまうため、テクスチャも一緒にPrepareセクションに追加しています。

[Keep]セクションは普通に読み込むファイルです。 セクション指定がない場合は、全てのファイルが[Keep]セクションに存在すると見なされます。

次にソースです。

void EnemyHitFunc( ALNode *self, ALCollision * selfcol
                  , ALCollision * targetcol, bool * cancel )
{
    self->SkipMotionLoop();
    ALSoundPlay( 0x40001 );                             (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();
            ALSoundPlay( 0x40002 );                     (2)
            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() );
        ALSoundPlay( 0x40000 );                         (3)
    };
};

(1)

単なるサウンド再生では、このように1行追加するだけで終わりです。指定しているIDはsarに指定したIDです。

ループする効果音などは再生ハンドルを受け取って使用しますが、その必要がなければ返り値も無視します。

(2)

同様にプレイヤー死亡の効果音の再生を行います。

(3)

同様にプレイヤーショット発射の効果音の再生を行います。

第3章 テーブル

次は、敵の種類を増やしてみましょう。 新たな敵を定義するには、そのままコードで記述する方法もありますが、 今回はテーブル(ALTable)を使用してデータで記述してみましょう。

テーブルとは、各種のユーザー定義のデータを保持する簡易データベースです。 中身は構造体配列のような形になっており、効率的にデータを保持します。

テーブルが保持する値へのアクセスには、フィールド(ALField)というテーブル専用のプロパティを使用します。 また、テーブルデータを変換するツールから、 テーブルの値にダイレクトでアクセスするクラスを自動生成させることもできます。 その自動生成クラスを使うと、構造体配列を使う時と変わらない速度でアクセスができます。

今回は、このテーブルに敵のアセンブルに使うIDと、アップデートに使う関数名を保持させましょう。

テーブルを作るには、プログラムコードでテーブルを生成し、保存する方法と、 テーブル定義ファイルを作り、ツールでコンバートする方法があります。 今回はテーブル定義ファイルをコンバートして作成します。

テーブル定義はxmlで記述し、拡張子はsrdを使用します。

<?xml version="1.0"?>
<TableDefine xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Fields>
    <Field>
      <Name>AssembleID</Name>
      <Caption>生成ID</Caption>
      <FieldType>Int</FieldType>
    </Field>
    <Field>
      <Name>ScriptName</Name>
      <Caption>スクリプト関数名</Caption>
      <FieldType>String</FieldType>
    </Field>
  </Fields>
</TableDefine>

通常はツールを使って生成しますが、 このように識別名であるName、表示名称のCaption、種別のFieldTypeを最低限指定します。

テーブルのデータはcsvフォーマットで記述します。

0x20000,EnemyUpdate
0x20001,EnemyUpdate2

今回はフィールドの値のみを指定していますが、1行目にフィールド識別子のリストを記述することもできます。 そうすると、項目が増えた時などに要素がずれることを防止することができます。

これらをツールでコンバートすると、atbというテーブルファイルができます。 今回はこのテーブルをフィールド経由でアクセスします。

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();
    sq->Load("EnemyScript.nut");
    sq->SetGlobalValue( "ATTRIBUTE_PLAYER", ATTRIBUTE_PLAYER );

    ALTable * tab = ALTable::Create( "EnemyTable.atb" );                (1)

    while( !ALSystem::IsTerminated() ){
        if( ALRandInt( 60 ) == 0 ){
            tab->SetRecordIndex( ALRandInt( tab->GetRecordCount() ) );  (2)
            ALNode * enemy = ALNode::Assemble(                          (3)
                tab->GetFieldAsInt( "AssembleID") );
            enemy->PlayMotion();
            enemy->SetPos( 660, ALRandFloat( 400 )+40 );
            enemy->AddController( ALSquirrelFiberController::Create( 
                sq, tab->GetFieldAsString( "ScriptName") ) );           (4)
            enemy->SetHitFunc( EnemyHitFunc );
        };
        ALYield();
    };
    tab->Release();                                                     (5)
    sq->Release();
    spr->Destroy();
    ar->Release();
}

(1)

テーブルを読み込みます。

(2)

読み込んだテーブルのどのレコードを読み出すかをSetRecordIndex関数で指定します。 テーブルのデータの読み込みは、このようにSetRecordIndexで読み込むレコードを指定してから読み込みます。

今回は2レコードあり、その個数をGetRecordCount関数で取得してランダムで選びます。

(3)

Assembleを行う引数をテーブルから取得します。

今回はこのようにダイレクトにフィールド名を指定して値を取得していますが、 あらかじめフィールドを検索し、そのフィールドから値を取得することなども可能です。繰り返し読み込む場合はその方が高速です。

(4)

アップデートを行う関数名を取得します。

(5)

テーブルを解放します。

なお、今回の修正でスクリプトにアップデート関数が増えました。変更後のスクリプトは以下のようになります。

function CreateBullet( enemy )
{
    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()-enemy.GetY(), player.GetX()-enemy.GetX() ), 10 ) )
            bul.SetPos( enemy.GetPos() )
    }
    na.Release()
}

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

        CreateBullet( this )

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

function EnemyUpdate2()
{
    while( 1 ){
        Pos.x -= 3;
        if( ALRandInt( 30 ) == 0 ){
            CreateBullet( this )
        }
        ALYield()
    }
}

第4章 エフェクト

このチュートリアルの最初で効果音を追加しました。 実際のゲームでは、効果音単体で使うことよりも、表示するエフェクトと同時に使うケースが非常に多いと思います。 Aqualeadでは前述のテーブルを使用して、効果音とエフェクトを同時に再生するエフェクトマネージャ(ALEffectManager)というクラスを使うことができます。

この章ではこのエフェクトマネージャを使用し、効果音とエフェクトを同時に使用できるようにします。

エフェクトマネージャには、テーブルで定義を作りそのデータを渡して使用します。

テーブルにはエフェクト用のID、再生する効果音ID、AssembleするエフェクトのIDを指定します。 エフェクトの再生には今までエフェクトを表示していたのと同様に、Assemble関数を使用します。

エフェクトIDを、AssembleするIDと同一にすることもできるので、 そうするとエフェクト再生コードは一切修正する必要はありません。

今回使用するsrdとcsvファイルは以下のようになります。

<?xml version="1.0"?>
<TableDefine xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Fields>
    <Field>
      <Name>@ID</Name>
      <Caption>エフェクトID</Caption>
      <FieldType>Int</FieldType>
    </Field>
    <Field>
      <Name>@AssembleID</Name>
      <Caption>生成ID</Caption>
      <FieldType>Int</FieldType>
    </Field>
    <Field>
      <Name>@SoundID0</Name>
      <Caption>効果音ID</Caption>
      <FieldType>Int</FieldType>
    </Field>
  </Fields>
</TableDefine>
0x30000,0x30000,0x40002
0x30001,0x30001,0x40001
0x30002,0,0x40000

ソースの変更点は以下のようになります。ただし、このほか従来のALSoundPlayはそのまま削除しています。

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

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();
    sq->Load("EnemyScript.nut");
    sq->SetGlobalValue( "ATTRIBUTE_PLAYER", ATTRIBUTE_PLAYER );

    ALTable * tab = ALTable::Create( "EnemyTable.atb" );

    ALEffectManager *em = ALEffectManager::Create( "EffectTable.atb" ); (2)

    while( !ALSystem::IsTerminated() ){
        if( ALRandInt( 60 ) == 0 ){
            tab->SetRecordIndex( ALRandInt( tab->GetRecordCount() ) );
            ALNode * enemy = ALNode::Assemble( 
                tab->GetFieldAsInt( "AssembleID") );
            enemy->PlayMotion();
            enemy->SetPos( 660, ALRandFloat( 400 )+40 );
            enemy->AddController( ALSquirrelFiberController::Create( 
                sq, tab->GetFieldAsString( "ScriptName") ) );
            enemy->SetHitFunc( EnemyHitFunc );
        };
        ALYield();
    };
    em->Release();                                                      (3)
    tab->Release();
    sq->Release();
    spr->Destroy();
    ar->Release();
}

(1)

元々ALSoundPlay関数を使用していましたが、エフェクトマネージャ経由のAssemble関数に置き換えています。

Assemble関数はダミーのノードを返しますが、これはすぐに自殺するので保持する必要はありません。

(2)

エフェクトマネージャを生成します。エフェクトマネージャはメモリ上に存在する間有効になります。

(3)

エフェクトマネージャを解放します。

第5章 テキストとフォント

次は、文字列を表示できるようにして、スコアを表示しましょう。 文字列表示にはテキスト(ALText)クラスを使用します。

テキストはノードの一種で、座標変更や色設定などはノードと同様に指定が可能です。 また、影や縁取りをつけたり、グラデーションの設定なども行うことができます。

テキストで使用する文字はフォント(ALFont)を使用します。 フォントはあらかじめデータとして作っておく必要があります。

フォントの定義にはsftファイルを使い、ツールを使用してフォントファイルを生成します。 sftファイルは以下のようになります。

<xi:include></xi:include>

大きく三つのセクションがあります。 [Font]セクションで、フォント名、フォントのサイズ、階調のビット数を設定します。 今回は32ドット、16色カラーのフォントを設定しています。 なお、原則フォントはモノクロで、表示時に色をつけます。

[UseLetterGroup]では、使用する文字のグループを設定します。 今回は、半角数字、半角大文字、半角小文字を指定しています。

[UseText]では、[UseLetterGroup]以外で個別に使用する文字を指定します。 [UseLetterGroup]を使用せず、このセクションに必要な文字を列挙してもかまいません。

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

#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,
};

static int Score = 0;                                       (1)

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

void EnemyHitFunc( ALNode *self, ALCollision * selfcol
                  , ALCollision * targetcol, bool * cancel )
{
    self->SkipMotionLoop();
    Score += 100;                                           (2)
};

(1)

スコア用の変数を宣言します。

(2)

敵破壊時にスコアを増加させます。

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();
    sq->Load("EnemyScript.nut");
    sq->SetGlobalValue( "ATTRIBUTE_PLAYER", ATTRIBUTE_PLAYER );

    ALTable * tab = ALTable::Create( "EnemyTable.atb" );

    ALEffectManager *em = ALEffectManager::Create( "EffectTable.atb" );

    ALText *tx = ALText::CreateRelease( ALFont::Create( "Font.aft" ) ); (1)
    tx->SetDrawPrio( 10 );                                              (2)
    tx->Show();

    while( !ALSystem::IsTerminated() ){
        if( ALRandInt( 60 ) == 0 ){
            tab->SetRecordIndex( ALRandInt( tab->GetRecordCount() ) );
            ALNode * enemy = ALNode::Assemble( tab->GetFieldAsInt( "AssembleID") );
            enemy->PlayMotion();
            enemy->SetPos( 660, ALRandFloat( 400 )+40 );
            enemy->AddController( ALSquirrelFiberController::Create(
                sq, tab->GetFieldAsString( "ScriptName") ) );
            enemy->SetHitFunc( EnemyHitFunc );
        };
        tx->Clear();                                                    (3)
        tx->AddPrintf("Score:%8d", Score );                             (4)
        ALYield();
    };
    tx->Destroy();                                                      (5)
    em->Release();
    tab->Release();
    sq->Release();
    spr->Destroy();
    ar->Release();
}

(1)

テキストとフォントを生成します。

フォントはこのテキストでしか使用しない場合、 このようにCreateRelease関数でテキストを生成すると、テキスト破棄時にフォントも一緒に解放されます。

(2)

スコアを手前に表示するために、描画プライオリティを変更します。

(3)

今のテキスト表示をクリアします。

クリアをせずに文字を表示すると追記することになります。

(4)

スコアを表示します。引数はprintfと同様の指定が可能です。

(5)

テキストを破棄します。

第6章 タスクとフェード

前章で文字列の表示ができるようになったので、それを利用してタイトルとゲームオーバー処理を作りましょう。 そのために、タスク(ALTask)という仕組みを使用します。

タスクとは、ある程度大きなゲームの処理を管理するものです。 今回のチュートリアルでは、タイトルタスク、ゲームタスク、ゲームオーバータスクという三つのタスクを作ります。 ゲームはこのタスクを切り替えつつ動作させることになります。

タスクはタスクマネージャ(ALTaskManagr)で管理され、タスクがどのように遷移するかを設定します。 今回であれば、タイトルタスク終了でゲームタスクが起動し、 ゲームタスク終了でゲームオーバータスク起動という流れになります。

簡易的な処理であれば、タスクマネージャには特に遷移を設定せず、 タスクの返り値でタスク遷移をさせることもできます。 今回はこの方式を使用します。

また、タスクは起動時にシーンを作り、終了時にそのシーンを破棄します。 これによりアップデータやノードの破棄を行う必要はなくなります。 このシーンの自動管理はタスク毎の設定によりオフにすることもできます。

その他、タスクには各種リソースの解放処理を任せることもできます。 各種リソースを生成後、解放をタスクに任せると解放忘れをしにくくなります。

タスクの切り替え時にはフェード(ALFade)を使用します。 フェードはフェードインとフェードアウトがあり、それぞれ時間、色等を指定できます。 自殺型で生成すれば破棄する必要もありません。 今回は自殺型で生成し、フェード完了まで待つようにします。

#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,
};

enum {
    TASK_TITLE = 1,                                 (1)
    TASK_MAIN,
    TASK_GAMEOVER
};

static Sint32 Score;
static Sint32 PlayerRest;                           (2)

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

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

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:
        {
            PlayerRest--;                           (3)
            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() );
        ALNode::Assemble( 0x30002 );
    };
};

(1)

タスク用のシンボルを定義します。1以上の整数を使用します。

(2)

プレイヤー残機用の変数を定義します

(3)

プレイヤー死亡時に残機を減らします。

ALTaskResult TitleTask( ALTask * task, size_t param )   (1)
{
    ALFont *fnt = ALFont::Create( "Font.aft" );
    ALFont::SetReleaseDefault( fnt );                   (2)
    task->Consign( fnt );                               (3)

    ALNode::Assemble("Title.aod");                      (4)
    while( ALPad::GetDevice(0)->GetDigitalTrig() == 0 ){
        ALYield();
    }
    ALFade::CreateOutSuicide().WaitTerminate();         (5)
    return TASK_MAIN;                                   (6)
}

ALTaskResult MainTask( ALTask * task, size_t param )    (7)
{
    Score = 0;
    PlayerRest = 3;

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

    ALSquirrel *sq = ALSquirrel::Create();
    sq->Load("EnemyScript.nut");
    sq->SetGlobalValue( "ATTRIBUTE_PLAYER", ATTRIBUTE_PLAYER );
    task->Consign( sq );                                (8)

    ALTable * tab = ALTable::Create( "EnemyTable.atb" );
    task->Consign( tab );

    task->Consign( ALEffectManager::Create( "EffectTable.atb" ) );

    ALText *tx = ALText::CreateRelease( ALFont::Create( "Font.aft" ) );
    tx->SetDrawPrio( 10 );
    tx->Show();

    while( PlayerRest > 0 ){                            (9)
        if( ALRandInt( 60 ) == 0 ){
            tab->SetRecordIndex( ALRandInt( tab->GetRecordCount() ) );
            ALNode * enemy = ALNode::Assemble( tab->GetFieldAsInt( "AssembleID") );
            enemy->PlayMotion();
            enemy->SetPos( 660, ALRandFloat( 400 )+40 );
            enemy->AddController(
                ALSquirrelFiberController::Create( sq, tab->GetFieldAsString( "ScriptName") ) );
            enemy->SetHitFunc( EnemyHitFunc );
        };
        tx->Clear();
        tx->AddPrintf("Score:%8d\nRest: %8d", Score, PlayerRest-1 );
        ALYield();
    }
    spr->Destroy();                                     (10)

    ALFade::CreateOutSuicide( 120 ).WaitTerminate();    (11)
    return TASK_GAMEOVER;
}

(1)

タイトルタスク用の関数です。タスク自身のインスタンスと、起動パラメータを引数として受け取ります。

今回は起動パラメータは使用しません。

(2)

フォントはAssemble処理の中で使用するので、生成したフォントをデフォルトに設定します。 テキストを生成時にフォントの指定がなければ、このデフォルトを使用します。

なお、アーカイブに含めるなどでフォントにIDがあれば、Assemble時にフォントを指定してテキストを生成することも可能です。

(3)

生成したフォントの解放をタスクに任せます。

(4)

タイトル画像を生成します。ノードが返りますが、解放はタスク終了時のシーン解放に任せるので、インスタンスの保持はしません。

(5)

自殺型のフェードアウトをデフォルトパラメータで生成し、終了まで待ちます。

(6)

次に起動するタスクを返り値で渡します。

(7)

ゲームのメインタスクです。

(8)

今まで最後に解放していた各種リソースの解放を、全てタスクに任せます。

(9)

残機がなくなるまでループを行います。

(10)

残機がなくなったので、プレイヤーを破棄してメインタスクを終わります。

(11)

メインタスク終了前は、普通よりゆっくりフェードアウトを行います。

ALTaskResult GameOverTask( ALTask * task, size_t param )    (1)
{
    ALFont *fnt = ALFont::Create( "Font.aft" );
    ALFont::SetReleaseDefault( fnt );
    task->Consign( fnt );

    ALNode *gover = ALNode::Assemble("GameOver.aod");
    gover->PlayMotion();                                    (2)

    while( gover->IsMotionPlay() ){                         (3)
        ALYield();
    }
    return TASK_TITLE;
}

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

    ALTaskManager *tm = ALTaskManager::GetDefault();        (4)
    tm->Add( TASK_TITLE, TitleTask );                       (5)
    tm->Add( TASK_MAIN, MainTask );
    tm->Add( TASK_GAMEOVER, GameOverTask );

    tm->Start( TASK_TITLE );                                (6)

    while( !ALSystem::IsTerminated() ){                     (7)
        ALYield();
    };
    ar->Release();
}

(1)

ゲームオーバータスクです。

(2)

ゲームオーバー表示にはモーションがあるので、そのモーションを再生します。

(3)

モーションが終わるまで待ちます。

(4)

デフォルトで用意されているタスクマネージャを取得します。

個別に生成し、複数のタスクマネージャを使用することもできます。

(5)

タスクマネージャにタスクIDと、タスク実行用関数を登録します。

(6)

タイトルタスクを起動します。

(7)

処理はメインタスクに移ったので、メインループでは単にアプリケーション終了まで待ちます。

タイトルとゲームオーバーの定義は以下のようになります。 まずはタイトルのsodファイルです。

<xi:include></xi:include>

今回はタイトルにモーションは設定していません。 シンプルにテキストのみで表示しています。 また、TextAlignとTextSizeプロパティを使い、文字を中央寄せで表示しています。

次はゲームオーバーのsodファイルです。

<xi:include></xi:include>

ゲームオーバーでは、文字のグラデーションと、影を設定しています。

次に、ゲームオーバーのsmtファイルです。

<xi:include></xi:include>

ゲームオーバーではフェードを使用せず、アルファ値のアニメーションでフェードを表現しています。

第7章 リソース管理

今回のサンプルではあまり必要はありませんが、普通のゲームではタスク毎に違うデータを使います。 その各種データを自動でロード、アンロードする仕組みがリソース(ALResource)です。

リソースに必要な各種データ(アーカイブ、テクスチャ、サウンド等)をあらかじめ登録しておくと、 それらのデータを一括ロードしたり、一括破棄をすることができます。 ロード時にはスレッドを使用して裏読みさせることもできます。

このリソースはタスクに関連づけることができます。 その場合タスク切り替え時にリソースをロードし、タスク終了時にリソースを解放します。 この時、次のタスクでも使用されるリソースは解放されずにそのまま維持されます。

これにより各種データ類のロードミス、解放ミスをほとんど防ぐことができるようになります。

また、前述のPrepare機能も併用するとほぼ全てのロード処理の必要がなくなります。

エフェクトマネージャも、テーブルに固有のカスタムIDというものを設定することにより、 自動ロード、アンロードに対応します。

カスタムID毎にテーブル生成、破棄時にコールバックイベントを追加できるため、 アプリケーション固有の自動ロード処理を作ることも可能です。

まずは、更新したsarファイルです。

<xi:include></xi:include>

テーブルと、スクリプトを追加しました。

テーブルはエフェクトマネージャで使用するテーブルです。 カスタムIDを設定することで、エフェクトマネージャも自動的に生成、破棄されます。

スクリプトはデフォルトに設定されているインスタンスにロードされます。 そのため、プログラム側であらかじめALSquirrelのインスタンスを生成し、デフォルトに設定しています。

次に、カスタムIDを追加したエフェクト用のsrdファイルです。

<?xml version="1.0"?>
<TableDefine xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Fields>
    <Field>
      <Name>@ID</Name>
      <Caption>エフェクトID</Caption>
      <FieldType>Int</FieldType>
    </Field>
    <Field>
      <Name>@AssembleID</Name>
      <Caption>生成ID</Caption>
      <FieldType>Int</FieldType>
    </Field>
    <Field>
      <Name>@SoundID0</Name>
      <Caption>効果音ID</Caption>
      <FieldType>Int</FieldType>
    </Field>
  </Fields>
  <CustomID>@EFM</CustomID>
</TableDefine>

カスタムIDは、ノードIDと同様に32ビット整数か、4文字で指定します。

今回はエフェクトマネージャ固有の識別子、@EFMを指定しています。

最後に、ソースです。

#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,
};

enum {
    TASK_TITLE = 1,
    TASK_MAIN,
    TASK_GAMEOVER
};

enum {
    RESOURCE_MAIN = 1,                          (1)
    RESOURCE_FONT,
};

static Sint32 Score;
static Sint32 PlayerRest;

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

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

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:
        {
            PlayerRest--;
            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() );
        ALNode::Assemble( 0x30002 );
    };
};

(1)

リソース用のシンボルを定義します。1以上の整数を使用します。

ALTaskResult TitleTask( ALTask * task, size_t param )           (1)
{
    ALNode::Assemble("Title.aod");
    while( ALPad::GetDevice(0)->GetDigitalTrig() == 0 ){
        ALYield();
    }
    ALFade::CreateOutSuicide().WaitTerminate();
    return TASK_MAIN;
}

ALTaskResult MainTask( ALTask * task, size_t param )            (2)
{
    Score = 0;
    PlayerRest = 3;

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

    ALTable * tab = task->FindTable( "EnemyTable.atb" );        (3)

    ALText *tx = ALText::Create();
    tx->SetDrawPrio( 10 );
    tx->Show();

    while( PlayerRest > 0 ){
        if( ALRandInt( 60 ) == 0 ){
            tab->SetRecordIndex( ALRandInt( tab->GetRecordCount() ) );
            ALNode * enemy = ALNode::Assemble( tab->GetFieldAsInt( "AssembleID") );
            enemy->PlayMotion();
            enemy->SetPos( 660, ALRandFloat( 400 )+40 );
            enemy->AddController(                               (4)
                ALSquirrelFiberController::Create( tab->GetFieldAsString( "ScriptName") ) );
            enemy->SetHitFunc( EnemyHitFunc );
        };
        tx->Clear();
        tx->AddPrintf("Score:%8d\nRest: %8d", Score, PlayerRest-1 );
        ALYield();
    }
    spr->Destroy();

    ALFade::CreateOutSuicide( 120 ).WaitTerminate();
    return TASK_GAMEOVER;
}

ALTaskResult GameOverTask( ALTask * task, size_t param )        (5)
{
    ALNode *gover = ALNode::Assemble("GameOver.aod");
    gover->PlayMotion();

    while( gover->IsMotionPlay() ){
        ALYield();
    }
    return TASK_TITLE;
}

(1)

タイトルタスクです。フォント周りのロードをPrepareに任せたのでフォントロードのコードを削除します。

(2)

エフェクトマネージャはテーブルに設定したカスタムIDにより自動ロードされるので、生成する必要がなくなりました。

(3)

敵のテーブルはリソースにより読み込み済みなので、その読み込み済みテーブルを取得します。

タスクに関連づけられている場合は、このようにタスクから検索して取得できます。これは解放の必要はありませんん。

(4)

ALSquirrelの生成をメインループに移し、デフォルト設定を行っているので、ALSquirrelの指定を省略します。

(5)

ゲームオーバータスクでもフォントのロードを削除します。

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

    ALSquirrel *sq = ALSquirrel::Create();                                      (1)
    ALSquirrel::SetReleaseDefault( sq );                                        (2)
    sq->SetGlobalValue( "ATTRIBUTE_PLAYER", ATTRIBUTE_PLAYER );

    ALResourceDefine resf = ALResourceManager::AddResource( RESOURCE_FONT );    (3)
    resf.AddFont( "Font.aft" );                                                 (4)

    ALResourceDefine resm = ALResourceManager::AddResource( RESOURCE_MAIN );
    resm.AddArchive( "GameData.aar" );
    resm.AddTable( "EnemyTable.atb" );
    resm.AddDepend( RESOURCE_FONT );                                            (5)

    ALTaskManager *tm = ALTaskManager::GetDefault();
    ALTask *ttsk = tm->Add( TASK_TITLE, TitleTask );
    ttsk->AddResource( RESOURCE_FONT );
    ALTask *mtsk = tm->Add( TASK_MAIN, MainTask );
    mtsk->AddResource( RESOURCE_MAIN );
    ALTask *otsk = tm->Add( TASK_GAMEOVER, GameOverTask );
    otsk->AddResource( RESOURCE_FONT );

    tm->Start( TASK_TITLE );

    while( !ALSystem::IsTerminated() ){
        ALYield();
    };
}

(1)

スクリプトを自動ロードさせるため、このサンプルではALSquirrelの生成をここに移動します。

(2)

生成したインスタンスをデフォルトに設定します。

(3)

フォント用のリソース定義を追加します。

(4)

リソース定義にフォントファイルを登録します。フォントロード時にデフォルト設定がNULLであれば、ロードしたフォントがデフォルトに設定されます。

(5)

メインのリソース定義の依存関係に、先ほど定義したフォントを追加します。

これで、メインリソース読み込み時に、フォントリソースも同時に読み込まれます。

今回の修正で、各種データのロード、アンロードがほとんど自動になり、解放処理がほぼなくなりました。 ゲームとしての機能の組み込みはこれで終わりです。

第8章 デバッグ機能

最後にデバッグ機能を組み込みます。 Aqualeadではデバッグを行いやすくするための各種組み込みデバッグ機能があります。 これらはReleaseコンパイル時には全て消えるため、製品版でのデバッグ機能消し忘れや、 パフォーマンスの問題などは発生しません。

今回は、デバッグメニュー、デバッグウィンドウ、プロファイラを組み込みます。

デバッグメニューは起動等にデバッグコマンドの起動などにも使いますが、 今回はゲーム中のパラメータデバッグに使用します。 Aqualeadでは各種プロパティの値をデバッグメニューから参照、修正できるため、 デバッガを使用しなくてもゲーム中の情報を参照できます。

デバッグウィンドウはゲーム中の各種情報を表示するコンソールウィンドウです。 複数のページを持ち、ページ毎に別の情報を表示することができます。

プロファイラはゲーム内の各種処理の経過時間を測定するものです。 デバッグウィンドウに表示ができるので、必要な時のみ表示することができます。

プロファイルはアプリケーションで追加もできますが、 各クラスには組み込みのプロファイル処理があり、設定することで各クラスの負荷を表示することもできます。

void PlayerUpdateFunc( ALNode * self )
{
    ALProfile("PlayerUpdate");                                          (1)
    if( ( ALPad::GetDevice( 0 )->GetDigitalTrig() & ALPAD_A ) ){
        ALNode *ps = ALNode::Assemble( 0x10100 );
        ps->SetPos( self->GetPos() );
        ALNode::Assemble( 0x30002 );
    };
    ALDebugWrite( "Player", "%f,%f", self->GetX(), self->GetY() );      (2)

};

ALTaskResult TitleTask( ALTask * task, size_t param )
{
    ALNode::Assemble("Title.aod");
    while( ALPad::GetDevice(0)->GetDigitalTrig() == 0 ){
        ALYield();
    }
    ALFade::CreateOutSuicide().WaitTerminate();
    return TASK_MAIN;
}

void PlayerSuicide( ALDebugMenu *dmenu ,void * itemdata )               (3)
{
    ALNodeArray *na = ALScene::GetDefault()->CreateFamilyNodeArray( ATTRIBUTE_PLAYER );
    if( na->GetCount() > 0 ){
        dmenu->PostMessage( na->Get( 0 ), MESSAGE_PLAYER_OUT );
    }
    na->Release();
}

ALTaskResult MainTask( ALTask * task, size_t param )
{
    ALProfile("MainTask");                                              (4)
    Score = 0;
    PlayerRest = 3;

#ifndef NDEBUG                                                          (5)
    ALDebugMenu *dm = ALDebugMenu::Create();                            (6)
    dm->AddPropItemAll( ALPropSet::GetRoot() );                         (7)
    dm->AddUserFuncItem( "自殺", PlayerSuicide );                       (8)
    dm->AddReleasePropSetItem( "敵一覧",                                (9)
        ALScene::GetDefault()->CreateFamilyNodeArray( ATTRIBUTE_ENEMY ) );
    dm->SetOpenCloseButton( ALPAD_D );                                  (10)
    dm->SetFramePos( 0, 0 );                                            (11)
    dm->UseOtherSleepAll( true );                                       (12)
    task->Consign( dm );                                                (13)
#endif

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

    ALTable * tab = task->FindTable( "EnemyTable.atb" );

    ALText *tx = ALText::Create();
    tx->SetDrawPrio( 10 );
    tx->Show();

    while( PlayerRest > 0 ){
        if( ALRandInt( 60 ) == 0 ){
            tab->SetRecordIndex( ALRandInt( tab->GetRecordCount() ) );
            ALNode * enemy = ALNode::Assemble( tab->GetFieldAsInt( "AssembleID") );
            enemy->PlayMotion();
            enemy->SetPos( 660, ALRandFloat( 400 )+40 );
            ALDebugWrite("Enemy", "Type %d (%f,%f)\n",                  (14)
                tab->GetRecordIndex(), enemy->GetX(), enemy->GetY() );
            enemy->AddController(
                ALSquirrelFiberController::Create( tab->GetFieldAsString( "ScriptName") ) );
            enemy->SetHitFunc( EnemyHitFunc );
            enemy->SetFamilyBits( ATTRIBUTE_ENEMY );                    (15)
        };
        tx->Clear();
        tx->AddPrintf("Score:%8d\nRest: %8d", Score, PlayerRest-1 );
        ALYield();
    }
    spr->Destroy();

    ALFade::CreateOutSuicide( 120 ).WaitTerminate();
    return TASK_GAMEOVER;
}

(1)

プレイヤーの更新処理のプロファイルを行います。測定範囲はこの関数からこの関数を含むスコープまでです。

中括弧で囲むことで、範囲を細かく設定することができます。

プロファイラには、この名前で表示されます。

(2)

デバッグウィンドウへプレイヤー座標を表示します。

(3)

デバッグメニューから起動されるユーザー項目です。これは自殺を実行します。

引数はデバッグメニュー自体と、登録時に使用したユーザーデータです。

(4)

メインタスク用のプロファイルを開始します。

(5)

Release時には使用できないので、#ifdefで囲みます。

(6)

デバッグメニューを生成します。ここではゲーム中に特定ボタンで開くタイプとして使用しています。

(7)

デバッグメニューに、ルートプロパティを登録します。

ルートプロパティとは、各種のプロパティ登録の根本となるプロパティで、ほとんどのプロパティはここからたどることができます。

各種デバッグ専用プロパティもここからアクセスができます。

(8)

コールバック型の項目を追加します。

(9)

PropSet型の項目を追加します。ここではファミリービットを使い、敵の一覧となるノード配列を渡しています。

これにより、敵情報へ容易にアクセスが可能になります。

登録時にReleaseも行っているため、デバッグメニューは記事にノード配列も同時に破棄されます。

(10)

デバッグメニューを開くためのボタンを設定します。

特にボタンを設定せず、プログラムから開くこともできます。

(11)

デバッグメニューの位置を調整します。

デバッグメニューはウィジェットというGUIクラスの一種で枠がつきます。そのままでは文字の左上が原点になり上が欠けるので、ここで枠の左上を原点に合わせます。

(12)

デバッグメニューオープン時に、他のオブジェクトを全てスリープさせるかどうか設定します。

(13)

デバッグメニューは独自のデバッグ用シーンに所属し、そのままでは自動開放されないため、タスクに解放を任せます。

(14)

デバッグウィンドウのEnemyページに敵の生成情報を追加します。

デフォルトではページの内容は毎フレーム削除されますが、メイン関数での設定でEnemyページは一定時間書き込みがなかった時にクリアするようにしています。

(15)

デバッグメニューで敵一覧を取得するため、ファミリービットの定義を追加しています。もちろん、データに設定してもかまいません。

ALTaskResult GameOverTask( ALTask * task, size_t param )
{
    ALNode *gover = ALNode::Assemble("GameOver.aod");
    gover->PlayMotion();

    while( gover->IsMotionPlay() ){
        ALYield();
    }
    return TASK_TITLE;
}

void ALMain()
{
#ifndef NDEBUG                                                                  (1)
    ALFont::SetReleaseDebugFont( ALFont::Create( "DebugFont.aft" ));            (2)

    ALDebugWindow::GetDefault()->SetDebugButton( ALPAD_C );                     (3)
    ALDebugPage *ep = ALDebugWindow::GetDefault()->FindCreatePage( "Enemy" );   (4)
    ep->SetAutoClearFrame( 60 );                                                (5)
#endif

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

    ALSquirrel *sq = ALSquirrel::Create();
    ALSquirrel::SetReleaseDefault( sq );
    sq->SetGlobalValue( "ATTRIBUTE_PLAYER", ATTRIBUTE_PLAYER );

    ALResourceDefine resf = ALResourceManager::AddResource( RESOURCE_FONT );
    resf.AddFont( "Font.aft" );

    ALResourceDefine resm = ALResourceManager::AddResource( RESOURCE_MAIN );
    resm.AddArchive( "GameData.aar" );
    resm.AddTable( "EnemyTable.atb" );
    resm.AddDepend( RESOURCE_FONT );

    ALTaskManager *tm = ALTaskManager::GetDefault();
    ALTask *ttsk = tm->Add( TASK_TITLE, TitleTask );
    ttsk->AddResource( RESOURCE_FONT );
    ALTask *mtsk = tm->Add( TASK_MAIN, MainTask );
    mtsk->AddResource( RESOURCE_MAIN );
    ALTask *otsk = tm->Add( TASK_GAMEOVER, GameOverTask );
    otsk->AddResource( RESOURCE_FONT );

    tm->Start( TASK_TITLE );

    while( !ALSystem::IsTerminated() ){
        ALYield();
    };
}

(2)

Release時は使用できないので、初期化のみ#ifdefで囲みます。ALDebugWrite関数は中身のみ消えるのでそのままで大丈夫です。

(1)

デバッグメニューや、デバッグウィンドウで使用するデバッグフォントを設定します。

(3)

デバッグウィンドウを操作するためのボタンを設定します。

デバッグウィンドウは、オープン、クローズ、ページ切り替え等の複数の操作がありますが、SetDebugButtonで設定したボタンでそれらを全て1ボタンで行えるようになります。

(4)

設定を変えるため、Enemyページを生成して取得します。設定変更がなければ、ページは自動で作られます。

(5)

このページは、60フレーム更新がない場合にクリアされるようになります。デフォルトでは毎フレームクリアされます。

第9章 最後に

いかがだったでしょうか。 今回までのチュートリアルで、Aqualeadの特徴的な機能を大体使うことができました。 もちろんまだ紹介していないクラス・機能はたくさんありますが、 大半は今までに紹介したクラス関連だけで十分使えると思います。

また、Aqualeadには各種管理機能が充実していることもわかったと思います。 このようなライブラリに用意されている管理機能では、制限が多くて使いにくいと思う人もいるかもしれませんが、 チュートリアルでは本当に最低限の機能しか使っていません。 各クラスを調べれば、もっと細かな設定があり、色々な用途に使えることがわかるはずです。

その他、今回は2Dしか使っていませんが、3Dでも基本はほとんど変わりません。 モデル、カメラ、メッシュ等のクラスが増えるだけで、あとは一緒です。

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

J-JSoft HomePage MACKさま

ザ・マッチメイカァズ2nd