8. ゲームの完成

この文書では2Dシューティングゲームの完成まで説明します。

敵・敵弾・敵のコマンダー

ではゲームに必要なキャラクターのクラスをどんどん実装していきましょう。

クラスの実装は星や自機の弾と同じ要領でおこなえば大丈夫です。

sample/Tutorial/Sample08_01/Sample08_01.slnを開いてください。

Enemyクラス

まずは敵のクラスです。

public class Enemy : GameActor
{
    static int idNum=0;

    float speed=4;
    Int32 cnt=0;

    public Enemy(GameFrameworkSample gs, string name, Texture2D textrue, Vector3 position) : base (gs, name)
    {
        Name = name + idNum.ToString();
        this.sprite = new SimpleSprite( gs.Graphics, textrue );
        this.sprite.Center.X = 0.5f;
        this.sprite.Center.Y = 0.5f;

        idNum++;

        this.sprite.Position = position;
    }

    public override void Update()
    {
        sprite.Position.Y += speed;

        if (sprite.Position.Y > gs.rectScreen.Height + sprite.Height )
        {
            this.Status = Actor.ActorStatus.Dead;//画面外にでたら死亡。
        }

        // 弾発射。
        if(cnt == 60)
        {
            GameActor player =(GameActor)gs.Root.Search("Player");

            Vector2 direction;
            direction.X= player.Sprite.Position.X - this.sprite.Position.X;
            direction.Y= player.Sprite.Position.Y - this.sprite.Position.Y;
            direction=direction.Normalize();

            gs.Root.Search("enemyBulletManager").AddChild(new EnemyBullet(gs, "enemyBullet", gs.textureEnemyBullet,
                new Vector3(this.sprite.Position.X,this.sprite.Position.Y, 0.5f), direction));
        }

        ++cnt;

        base.Update();
    }
}

出現から60フレーム経過したら、自機に向かって弾を発射するようにしています。

direction=direction.Normalize()で方向ベクトルを単位ベクトルにしています。

画面外に出たら死亡フラグを立てます。

EnemyBulletクラス

次に敵弾のクラスです。

public class EnemyBullet : GameActor
{
    static int idNum=0;

    float speed=6;

    Vector3 trans;

    public EnemyBullet(GameFrameworkSample gs, string name, Texture2D textrue, Vector3 position, Vector2 direction) : base (gs, name)
    {
        Name = name + idNum.ToString();
        this.sprite = new SimpleSprite( gs.Graphics, textrue );
        this.sprite.Center.X = 0.5f;
        this.sprite.Center.Y = 0.5f;

        idNum++;

        this.trans.X=direction.X*speed;
        this.trans.Y=direction.Y*speed;
        this.trans.Z=0.0f;

        this.sprite.Position = position;
    }

    public override void Update()
    {
        sprite.Position += trans;

        if (sprite.Position.X < 0 -sprite.Width ||
            sprite.Position.Y < 0 -sprite.Height||
            gs.rectScreen.Width + sprite.Width < sprite.Position.X ||
            gs.rectScreen.Height + sprite.Height < sprite.Position.Y  )
        {
            this.Status = Actor.ActorStatus.Dead;
        }

        base.Update();
    }
}

引数の方向ベクトルに速度を乗算して移動ベクトルを算出し、Update()で毎フレーム移動ベクトルを加算しています。

画面外に出たら死亡フラグを立てます。

EnemyCommanderクラス

次は敵を出現させる管理クラスです。

public class EnemyCommander : GameActor
{
    public EnemyCommander(GameFrameworkSample gs, string name) : base (gs, name)    {}

    public override void Update()
    {
        if( gs.appCounter % 30 == 0)
        {
            Vector3 position;

            position.X = (int)(gs.rectScreen.Width * gs.rand.NextDouble());
            position.Y = 0.0f;
            position.Z = 0.2f;

            gs.Root.Search("enemyManager").AddChild(new Enemy(gs, "enemy", gs.textureEnemy, position));
        }

        base.Update();
    }
}

30フレームに一度、画面上方向から敵を出現させるようにしています。

あとはGameFrameworkSample.csで次のようにアクターツリーを構成します。

image/program_guide/actor_tree_sample08.JPG

では実行してみましょう。

image/program_guide/sample08.png

敵が画面上から出現し、自機に向けて弾を撃ってきます。

まだあたり判定をつけていないので弾が素通りしていますね。

あたり判定と爆発エフェクト

CollisionCheck

次はあたり判定をつけ、命中したら爆発エフェクトを出すようにしましょう。

sample/Tutorial/Sample08_01/Sample08_02.slnを開いてください。

sample/Tutorial/Sample08_02/CollisionCheck.cs

//@j あたり判定をおこなうクラス。
public class CollisionCheck : GameActor
{
    public CollisionCheck(GameFrameworkSample gs, string name) : base (gs, name){}

    public override void Update()
    {
        Player player =(Player)gs.Root.Search("Player");
        Actor bulletManager=gs.Root.Search("bulletManager");
        Actor enemyManager =gs.Root.Search("enemyManager");
        Actor bulletEnemyManager=gs.Root.Search("enemyBulletManager");
        Actor effectManager =gs.Root.Search("effectManager");

        //@j 弾と敵のあたり判定。
        foreach( Bullet bullet in  bulletManager.Children)
        {
            if(bullet.Status == Actor.ActorStatus.Action)
            {
                foreach( Enemy enemy in  enemyManager.Children)
                {
                    if(enemy.Status ==  Actor.ActorStatus.Action &&
                       Math.Abs(bullet.Sprite.Position.X -enemy.Sprite.Position.X ) < 30 &&
                        Math.Abs(bullet.Sprite.Position.Y -enemy.Sprite.Position.Y ) < 30
                            )
                    {
                        bullet.Status = Actor.ActorStatus.Dead;
                        enemy.Status = Actor.ActorStatus.Dead;
                        effectManager.AddChild(new Explosion(gs, "explosion", gs.textureExplosion,
                            new Vector3(enemy.Sprite.Position.X, enemy.Sprite.Position.Y, 0.3f)));

                        gs.Score += 100;
                        gs.soundPlayerExplosion.Play();
                    }
                }
            }
        }

        if(player.playerStatus== Player.PlayerStatus.Normal)
        {
            //@j 自機と敵のあたり判定。
            foreach( Enemy enemy in  enemyManager.Children)
            {
                if(enemy.Status ==  Actor.ActorStatus.Action &&
                   Math.Abs(player.Sprite.Position.X -enemy.Sprite.Position.X ) < 42 &&
                    Math.Abs(player.Sprite.Position.Y -enemy.Sprite.Position.Y ) < 42
                        )
                {

                    effectManager.AddChild(new Explosion(gs, "explosion", gs.textureExplosion,
                        new Vector3(player.Sprite.Position.X, player.Sprite.Position.Y, 0.3f)));

                    player.playerStatus = Player.PlayerStatus.Explosion;

                    gs.soundPlayerExplosion.Play();

                    gs.NumShips--;
                }
            }


            //@j 自機と敵弾のあたり判定。
            foreach( EnemyBullet enemyBullet in  bulletEnemyManager.Children)
            {
                if(enemyBullet.Status ==  Actor.ActorStatus.Action &&
                   Math.Abs(player.Sprite.Position.X -enemyBullet.Sprite.Position.X ) < 26 &&
                    Math.Abs(player.Sprite.Position.Y -enemyBullet.Sprite.Position.Y ) < 26
                        )
                {
                    enemyBullet.Status = Actor.ActorStatus.Dead;
                    effectManager.AddChild(new Explosion(gs, "explosion", gs.textureExplosion,
                        new Vector3(player.Sprite.Position.X, player.Sprite.Position.Y, 0.3f)));

                    player.playerStatus = Player.PlayerStatus.Explosion;
                    gs.soundPlayerExplosion.Play();
                    gs.NumShips--;
                }
            }
        }

        base.Update();
    }
}

あたり判定を行う各アクターのManagerクラスをアクターツリーの中から検索し、foreachでそれぞれの座標をもとに命中判定をおこなっています。

Managerクラスでツリー状に管理しておくと、あたり判定を行わなくてよいアクター(星、爆発エフェクト)を始めから除外できるので、処理効率がよいといえるでしょう。

命中判定が真ならそのアクターに死亡フラグを立て、爆発エフェクトを発生させます。

爆発エフェクトの発生も、他と同じ要領で行えばよいでしょう。

Playerクラスの改造

ところで、自機が爆発したときやその後の無敵時間のときは、あたり判定を行う必要がありません。

そのためPlayerの状態を表す変数を実装しておき、処理の場合分けを行いましょう。

sample/Tutorial/Sample08_02/Player.cs

public enum PlayerStatus
{
    Normal,
    Explosion,
    Invincible,
    GameOver,
};

public PlayerStatus playerStatus;

あとはあたり判定時にplayerStatusを見て、処理を行うとよいでしょう。

ゲームの進行を管理するクラス

GameManager

あとは、簡単なオープニング画面とゲームオーバー画面、点数表示をつくっておきましょう。

ここではゲームの進行を管理するクラスをつくります。

sample/Tutorial/Sample08_02/GameManager.cs

public class GameManager : GameActor
{
    Int32 cnt=0;

    SimpleSprite spriteTitle;
    SimpleSprite spritePressStart;
    SimpleSprite spriteGameOver;

    public GameManager(GameFrameworkSample gs, string name) : base (gs, name)
    {
        spriteTitle=new SimpleSprite(gs.Graphics, gs.textureTitle);
        spriteTitle.Position.X = gs.rectScreen.Width/2-gs.textureTitle.Width/2;
        spriteTitle.Position.Y = gs.rectScreen.Height/2-gs.textureTitle.Height;

        spritePressStart=new SimpleSprite(gs.Graphics, gs.texturePressStart);
        spritePressStart.Position.X = gs.rectScreen.Width/2-gs.texturePressStart.Width/2;
        spritePressStart.Position.Y = gs.rectScreen.Height/2+gs.texturePressStart.Height;

        spriteGameOver=new SimpleSprite(gs.Graphics, gs.textureGameOver);
        spriteGameOver.Position.X = gs.rectScreen.Width/2-gs.textureGameOver.Width/2;
        spriteGameOver.Position.Y = gs.rectScreen.Height/2-gs.textureGameOver.Height;
    }

    public override void Update()
    {
        gs.debugStringScore.Clear();
        gs.debugStringShip.Clear();

        string str = String.Format("Score: {0:d8}", gs.Score);
        gs.debugStringScore.SetPosition(new Vector3( gs.rectScreen.Width/2-(str.Length*10/2), 2, 0));
        gs.debugStringScore.WriteLine(str);

        str = String.Format("Ships:{0}", gs.NumShips);
        gs.debugStringShip.SetPosition(new Vector3( gs.rectScreen.Width-(str.Length*10), 2, 0));
        gs.debugStringShip.WriteLine(str);

        switch(gs.Step)
        {
        case GameFrameworkSample.StepType.Opening:
            gs.debugString.WriteLine("Opening");

            if((gs.PadData.ButtonsDown & GamePadButtons.Start) != 0  )
            {
                Player player =(Player)gs.Root.Search("Player");
                player.Status = Actor.ActorStatus.Action;
                player.Initilize();

                gs.GameCounter=0;
                gs.NumShips=3;
                gs.Score=0;
                gs.soundPlayerButton.Play();
                gs.bgmPlayer.Play();
                gs.Step= GameFrameworkSample.StepType.GamePlay;
            }

            break;
        case GameFrameworkSample.StepType.GamePlay:
            gs.debugString.WriteLine("GamePlay");

            //@j 残機が0になったらゲームオーバーへ。
            //@e Game over when player stock falls to zero.
            if(gs.NumShips <= 0)
            {
                gs.Step= GameFrameworkSample.StepType.GameOver;
                gs.bgmPlayer.Stop();
                cnt=0;
            }

            ++gs.GameCounter;

            break;
        case GameFrameworkSample.StepType.GameOver:
            gs.debugString.WriteLine("GameOver");

            if((gs.PadData.ButtonsDown & GamePadButtons.Start) != 0 || ++cnt > 300)
            {
                gs.Step= GameFrameworkSample.StepType.Opening;
                System.GC.Collect(2);
            }

            break;
        default:
            throw new Exception("default in StepType.");

        }

        base.Update();
    }

    public override void Render ()
    {
        gs.debugStringScore.Render();
        gs.debugStringShip.Render();

        switch(gs.Step)
        {
        case GameFrameworkSample.StepType.Opening:
            spriteTitle.Render();
            spritePressStart.Render();
            break;
        case GameFrameworkSample.StepType.GamePlay:

            break;
        case GameFrameworkSample.StepType.GameOver:
            spriteGameOver.Render();
            break;
        default:
            throw new Exception("default in StepType.");

        }

        base.Render ();
    }
}

GameManager クラスでは、以下の流れでゲームを進行させます。

  • オープニング画面で、ボタンを押してゲーム開始。
  • ゲーム中、残機が0になったらゲームオーバー。
  • ゲームオーバー画面で一定時間経過、もしくはボタンが押されたら、再びオープニング画面へ。

ステージの合間にガベージコレクションを呼び出す

ゲームオーバー画面からオープニング画面に移行するとき、System.GC.Collect(2)でガベージコレクションを強制的に呼び出し、メモリを回収しています。画面が遷移する場所ならガベージコレクションによる処理落ちが発生してもそれほど気になりません。また、ここで呼び出しておくことでプレイ中のガベージコレクションの発生を低減させることができます。

アクターツリーの完成形

アクターツリーの完成形は以下のようなイメージになります。

image/program_guide/actor_tree_sample08_02.JPG

ゲームの完成

ようやくゲームの完成です。F5を押して実行してみてください。

image/program_guide/sample08_02.JPG

sample/Tutorial/Sample08_02 は、app.xmlにマスターパッケージを作成するための設定がなされています。マスターパッケージを作成するときの参考にしてみてください。

シンプルなゲームですが、ゲームの進行に必要な構造はひと通りそろっているので、あとは好みに応じてどんどん改造していくとよいでしょう。