8. Completing the Game

This chapter explains the process up to completing the design of a 2D shooting game.

Enemy, EnemyBullets, and EnemyCommander Classes

Implement many classes of characters required for a game.

Implementation can be carried out in the same manner as implementing the classes for stars and bullets.

Open sample/Tutorial/Sample08_01/Sample08_01.sln.

Enemy Class

First, implement the enemy class.

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

In the above example, the enemy is designed to fire bullets toward the user plane 60 frames after its appearance.

Directional vector is converted to unit vector by direction=direction.Normalize().

When an enemy moves out of the screen, set a dead flag to it.

EnemyBullet Class

Next, implement the EnemyBullet class.

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

Multiply speed times the directional vector given by the argument to calculate the movement vector. Add the movement vector per frame with Update().

When a bullet moves out of the screen, set a dead flag to it.

EnemyCommander Class

Next, implement the EnemyCommander class, which makes the enemy appear.

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

In the above example, enemy planes are designed to appear from the top of the screen every 30 frames.

Then, configure the actor tree as follows using GameFrameworkSample.cs.

image/program_guide/actor_tree_sample08.JPG

Execute.

image/program_guide/sample08.png

Enemies appear from the screen top and fire bullets toward the user plane.

Since a collision evaluation has not been set yet, the bullets will fall through without hitting anything.

Evaluating Collision and Blow-up Effects

CollisionCheck

Next, set a collision evaluation and create a blow-up effect when a bullet hits the target.

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

Search for the Manager class of each actor for which to make collision evaluation from the actor tree, and make a collision evaluation based on each coordinates using foreach.

By managing the Manager class in a tree format, actors for which collision evaluation are not required (stars, blow-up effects) can be excluded from the beginning to increase processing efficiency.

If the collision evaluation is true, set a dead flag to the actor and generate a blow-up effect.

The blow-up effect can be generated in the same manner as other actors.

Reconstructing the Player Class

While the user plane is blown up, or after that when there are no more enemies, it is no longer necessary to perform a collision evaluation.

To increase efficiency, implement a variable to represent the status of the Player and to perform processing appropriate to each of its status.

sample/Tutorial/Sample08_02/Player.cs

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

public PlayerStatus playerStatus;

Upon collision evaluation, see playerStatus and carry out the appropriate processing.

Class For Managing Gameplay

GameManager

Create a simple start screen, a game over screen, and a score display.

Create a class to manage gameplay.

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

Gameplay is carried out by the GameManager class as follows.

  • Press a button at the start screen to start the game.
  • The game ends when the user's number of planes reaches 0.
  • After the duration of a certain period of time or if a button is pressed at the game over screen, the screen returns to the start screen.

Calls Garbage Collection Between Stages

When the game over screen proceeds to the opening screen, garbage collection will be forcefully called with System.GC.Collect(2), and memory will be recovered. Even if processing delays occur from garbage collection, it will not be very noticeable at scene transitions. In addition, garbage collection occurrences during game play will be reduced by calling it here.

Complete Actor Tree

The completed image of an actor tree is as follows.

image/program_guide/actor_tree_sample08_02.JPG

Completion of the Game

This completes game creation. Press F5 and execute.

image/program_guide/sample08_02.JPG

Settings to create a master package are made to app.xml in sample/Tutorial/Sample08_02. Use it as reference when creating a master package.

Although this is a simple game, all elements required for gameplay have been set. More elements can be added for customization.