CHAPTER 9

image

Building Your First 2D Game

After completing this chapter, you will be able to:

  • Begin the designing of a 2D game
  • Enumerate and specify functionality of elements in your game
  • Translate the gaming element specification into C# classes
  • Appreciate and work with the accuracy supported by the GameTime class
  • Synthesize the preceding elements into a final fun game
  • Approach the evaluation of your own game
  • Begin investigation into the procedures to publish your final game

This chapter covers the process and implementation details of creating your own simple game using the concepts and techniques discussed throughout this book. It will demonstrate how you can leverage the code you have already produced in implementing simple games, such as the one described in this chapter, with straightforward efforts. In fact, the game described in this chapter was created over one weekend. This type of rapid development allows you to create quick prototypes of your ideas or simply focus on creating a fun and challenging experience for your target audience. Finally, at the end of this chapter is a discussion of some considerations for publishing your games with some relevant references for further investigations.

Simple Game: Fish Food

In this game, the player plays as a fish that swims along the seabed in an attempt to travel as far as it can. As it travels, the fish will encounter several enemy fish. The player will have to avoid these fish in order to continue traveling along the seabed. The fish will also encounter worms while traveling that it can consume in order to grow in size. If the player’s fish is caught, it will lose a size and if it is of size one the game will end and the distance it traveled will be displayed. The player’s fish can also shoot bubbles at the enemy fish in order to stun them for a few seconds in its attempt to avoid them.

The Fish Food project

This project demonstrates how to create a simple game by utilizing and modifying the content from this book. In the game, the player controls a hero character and attempts to travel as far as possible before getting caught by enemies. You can see the initial splash screen of this project running in Figure 9-1.

9781430266044_Fig09-01.jpg

Figure 9-1. Running the Fish Food game

The project’s controls are as follows:

  • Left thumbstick (WSAD keys) Moves that hero character
  • Button A (K key) Fires a bubble or starts the game

The goals of this project are as follows:

  • To experience the entire game development process with a non-trivial game
  • To experience mapping of gameplay description to specifications of game elements
  • To iterate and specify all elements in a non-trivial game
  • To experience the coding and modification of classes for each gaming element
  • To synthesize all gaming elements in the GameState class to support gameplay

The first step in building a complete game is actually not about programming; rather, it is all about design. For the Fish Food game, we begin by specifying the complete functionality of the game, followed by enumerating the functionality of every element in the game, including game window, environment, hero, food, enemies, and camera behaviors. This complete specification of the game is referred to as the game design documentation. This documentation should completely describe how the game will be played. It is vital that this documentation be in a consistent state before you begin the actual programming. This is because it is much easier to change the behaviors of gaming elements by modifying the description than by changing programming code.

After the enumeration of the game design, the steps for creating the project are as follows:

  1. Building the object with the most straightforward behavior, in this case the BubbleShot class.
  2. Create the Hero class to represent the fish that the player will control.
  3. Modify the PatrolEnemy class to allow enemies the ability to utilize sprite sheets and other customized behaviors.
  4. Create the enemies: JellyFish, BlowFish, and FightingFish classes.
  5. Create the PatrolEnemySet class to allow convenient working with collections of enemies.
  6. Create the FishFood class for feeding the hero fish.
  7. Create the EnvironmentGenerator class to furnish the gaming environment.
  8. Modify the GameState class to incorporate all of the preceding elements in implementing the final game.

Add the following resources, which can be found in the Chapter9SourceCodeResources folder, into your content project before you begin:

  • Break.xnb
  • Bubble.xnb
  • BUBBLE_1.xnb
  • Chomp.xnb
  • Eat.xnb
  • ENEMY_1.xnb
  • ENEMY_2.xnb
  • ENEMY_3.xnb
  • GAMEOVERSCREEN_1.xnb
  • GROUND_1.xnb
  • HERO_1.xnb
  • Hit.xnb
  • MindMeld.xnb
  • SEAWEEDSMALL_1.xnb
  • SEAWEEDTALL_1.xnb
  • SIGN_1.xnb
  • SPLASHSCREEN_1.xnb
  • Stun.xnb
  • Touch.xnb
  • WORM_1.xnb

Fish Food Game Design

Before creating or implementing a game or prototype, it is always a good idea to write down the game’s concept and functionality. This document can then become the foundation for your game’s design document. It is generally important to have a design document when developing your game, as it provides you with an overview of the game objects and their interactions or behaviors among each other. This is especially important for larger projects or projects with multiple team members. For the purposes of this simple game project, because of its limited size and scope, a small summary and outline of the base functionality will suffice as the design document.

image Note  Please refer to GameDesignDocTemplate.doc, located in Chapter 9 of the source code, for an example of what a full-fledged game design documentation template might look like.

Game functionality outline

  • Goals and Objectives:
  • Travel as far as you can
  • Avoid being caught by other fish
  • Consume food to increase the hero’s size
  • Game
  • Side-scrolling to the right endlessly
  • Increases in difficulty the further you travel by spawning more fish
  • Displays distance traveled and size of the hero
  • Provides a starting and ending splash screen
  • Environment
  • Generates a simplistic endless terrain by repeating a seabed and a few pieces of seaweed as the camera pans right
  • Generates a sign displaying the distance traveled at fixed intervals
  • Hero, under player control
  • Can be moved up, down, left and right
  • Can shoot a projectile to stun enemies
  • Can change in size
  • Food
  • Periodically falls to the seabed at semi-random intervals
  • Can be eaten by the hero to increase its size
  • Enemies
  • Jellyfish: can patrol in all directions and stun the hero when they touch
  • Blowfish: patrols in the vertical direction and decreases hero size when touched
  • Fighting Fish: patrols in the horizontal direction and decreases hero size when touched except when hero is larger in size
  • All enemies chase the hero when approached
  • Camera
  • Bounds the hero to the screen
  • Pans to the right when the hero crosses the center of the screen

You can start by creating the simplest object. The preceding design outline states that the hero character can shoot a projectile. The projectile or bubble shot is quite basic and derives all of its behaviors from the base class.

Creating the BubbleShot class

  1. Begin by creating a new class called BubbleShot that inherits from GameObject. As you may remember, the GameObject class you implemented earlier provides behavior for game objects. Add a constant variable for its size and speed. Initialize the proper variables and use the bubble image provided.
    public class BubbleShot : GameObject
    {
        private const float kBubbleShotWidth = 7f;
        private const float kBubbleShotHeight = 7f;
        private const float kBubbleShotSpeed = 1.8f;
     
        public BubbleShot(Vector2 position, int facing)
            : base("BUBBLE_1", position,
                   new Vector2(kBubbleShotWidth, kBubbleShotHeight), null)
        {
            Speed = kBubbleShotSpeed;
            mVelocityDir = new Vector2(facing, 0);
        }
    }
  2. Now simply add a function that returns whether the BubbleShot is currently on screen by utilizing your previously made Camera class.
    public bool IsOnScreen()
    {
        // take advantage of the camera window bound check
        Camera.CameraWindowCollisionStatus status = Camera.CollidedWithCameraWindow(this);
        return (Camera.CameraWindowCollisionStatus.InsideWindow == status);
    }

Creating the Hero class

  1. Create a new class called Hero that inherits from SpritePrimitive. Add the following variables and functions in order to enable the Hero with particle support.
    public class Hero : SpritePrimitive
    {
      #region Particle Stuff
      private const float kCollideParticleSize = 4f;
      private const int kCollideParticleLife = 20;
     
      ParticleSystem mCollisionEffect = new ParticleSystem();
      // to support particle system
      private ParticlePrimitive CreateParticle(Vector2 pos)
      {
        return new ParticlePrimitive(pos, kCollideParticleSize, kCollideParticleLife);
      }
      #endregion
      ...
    }
  2. Next, add the following constants for the hero size, the time between shots, the offset position for the shots, the time that hero can be stunned, and the maximum growth size of the hero. Also add the following instance variables. Note that the hero size accessor also performs the size recalculation.
    //Constants
    private const float kHeroWidth = 20f;
    private const float kTimeBetweenBubbleShot = 1.5f;     // number of seconds between shots
    private const float kBubbleShotOffset = 0.35f * kHeroWidth;
    private const float kStunTimer = 1.5f;
    private const int kMaxHeroSize = 2;
    private float mTimeSinceLastShot = 0;
    private float mStunTimer;
    private int mHeroCurrentSize;
    public int HeroSize
    {
        get { return mHeroCurrentSize; }
        set
        {
            mHeroCurrentSize = value;
            this.Size = new Vector2(
                    kHeroWidth + kHeroWidth * (mHeroCurrentSize - 1) / 3,
                    kHeroWidth + kHeroWidth * (mHeroCurrentSize - 1) / 3);
        }
    }
  3. Now create an enum for the hero’s current state and add a list and accessor for the BubbleShot.
    private enum HeroState
    {
        Playing,
        Stunned,
        Unstunnable,
        Lost
    }
    private HeroState mCurrentHeroState;
    private List<BubbleShot> mBubbleShots;
    public List<BubbleShot> AllBubbleShots() { return mBubbleShots; }
  4. You can now create a constructor in order to initialize the needed instance variables. Remember to pass in the proper sprite sheet and initialize it to the starting frame.
    public Hero(Vector2 position)
        : base("HERO_1", position, new Vector2(kHeroWidth, kHeroWidth), 2, 2, 0)
    {
        mHeroCurrentSize = 1;
        mStunTimer = 0;
        mCurrentHeroState = HeroState.Playing;
        mBubbleShots = new List<BubbleShot>();
        mTimeSinceLastShot = kTimeBetweenBubbleShot;
     
        SetSpriteAnimation(0, 0, 1, 1, 10);
        SpriteCurrentRow = 1;
    }
  5. For the update function, create a switch statement that updates according to the hero’s current state as shown in the following.
    public void Update(GameTime gameTime, Vector2 delta, bool shootBubbleShot)
    {
        switch(mCurrentHeroState)
        {
            case HeroState.Playing:
                UpdatePlayingState(gameTime, delta, shootBubbleShot);
                break;
            case HeroState.Stunned:
                UpdateStunnedState(gameTime);
                break;
            case HeroState.Unstunnable:
                UpdateUnstunnableState(gameTime);
                UpdatePlayingState(gameTime, delta, shootBubbleShot);
                break;
            case HeroState.Lost:
                mCurrentHeroState = HeroState.Lost;
                break;
            default:
                break;
        }
    }
  6. For HeroState.Playing, create and call a function that
    • • bounds the hero to the screen
    • • applies player input to the hero’s position
    • • faces the sprite and BubbleShot in the correct direction
    • • updates the particles
    • • calculates the time between shots and shoots the bubble when able
    • • updates all the bubbles that have been shot
    public void UpdatePlayingState(GameTime gameTime, Vector2 delta, bool shootBubbleShot)
    {
        base.Update();
        // take advantage of the camera window bound check
        BoundObjectToCameraWindow();
        
        // Player control
        mPosition += delta;
     
        // Sprite facing direction
        if (delta.X > 0)
            SpriteCurrentRow = 1;
        else if (delta.X < 0)
            SpriteCurrentRow = 0;
     
        // BubbleShot direction
        int bubbleShotDir = 1;
        if (SpriteCurrentRow == 0)
            bubbleShotDir = -1;
     
        mCollisionEffect.UpdateParticles();
     
        float deltaTime = gameTime.ElapsedGameTime.Milliseconds;
        mTimeSinceLastShot += deltaTime / 1000;
        
        // Can the hero shoot a BubbleShot?
        if (mTimeSinceLastShot >= kTimeBetweenBubbleShot)
        {
            if (shootBubbleShot)
            {
                BubbleShot j = new BubbleShot(
                            new Vector2(Position.X + kBubbleShotOffset * bubbleShotDir,
                             Position.Y), bubbleShotDir);
                mBubbleShots.Add(j);
                mTimeSinceLastShot = 0;
                AudioSupport.PlayACue("Bubble");
            }
        }
     
        // now update all the BubbleShots out there ...
        int count = mBubbleShots.Count;
        for (int i = count - 1; i >= 0; i--)
        {
            if (!mBubbleShots[i].IsOnScreen())
            {
                // outside now!
                mBubbleShots.RemoveAt(i);
            }
            else
                mBubbleShots[i].Update();
        }
    }
  7. For HeroState.Stunned, create and call a function that implements the stun timer. This is easily done by incrementing the timer by the elapsed time each update. The action of stunning the hero comes from the fact that this update is being called rather than the UpdatePlayingState() function.
    public void UpdateStunnedState(GameTime gameTime)
    {
        float deltaTime = gameTime.ElapsedGameTime.Milliseconds;
        mStunTimer += deltaTime / 1000;
        if (mStunTimer >= kStunTimer)
        {
            mStunTimer = 0;
            mCurrentHeroState = HeroState.Unstunnable;
        }
    }
  8. For HeroState.Unstunnable, create and call a function that transitions the hero from the stunned state to the playing state. This state is required to ensure that the hero is not stunned permanently. This can be accomplished by reusing the stun timer as shown in the following code.
    public void UpdateUnstunnableState(GameTime gameTime)
    {
        float deltaTime = gameTime.ElapsedGameTime.Milliseconds;
        mStunTimer += deltaTime / 1000;
        if (mStunTimer >= kStunTimer)
        {
            mStunTimer = 0;
            mCurrentHeroState = HeroState.Playing;
        }
    }

    image Note  The update functions have used a parameter type of GameTime. This is provided via the base update function. Up until now for all time-based calculations, you have used the concept of update ticks. That is the number of update function calls between particular actions. While this works well, it is not always ideal due to the fact that the time between each update function call can vary. GameTime remedies the variable duration in-between update function calls by providing you with the ability to get the ElapsedGameTime to accurately account for time between update function calls. For more information on GameTime refer to http://msdn.microsoft.com/library/microsoft.xna.framework.gametime(v=xnagamestudio.40).aspx.

  9. You can now implement Draw() by overriding the base draw function and draw the hero, the bubbles, and the particles.
    public override void Draw()
    {
        base.Draw();
        foreach (var j in mBubbleShots)
            j.Draw();
        mCollisionEffect.DrawParticleSystem();
    }
  10. Last, functions are needed to provide simple functionality such as adjusting size of the hero, stunning the hero, checking whether the player has lost or not, and feeding the hero. The implementation of these functions is straightforward, as you can see in the following code. Remember to set the current hero’s state to lost when its size becomes less than one.
    public void AdjustSize(int incAdjustment)
    {
        if (incAdjustment + HeroSize > kMaxHeroSize)
            return;
        HeroSize += incAdjustment;
        MathHelper.Clamp(HeroSize, 0, 3);
        if (HeroSize <= 0)
        {
            mCurrentHeroState = HeroState.Lost;
        }
    }
     
    public void StunHero()
    {
        if (mCurrentHeroState != HeroState.Unstunnable && mCurrentHeroState != HeroState.Stunned)
        {
            mCurrentHeroState = HeroState.Stunned;
            AudioSupport.PlayACue("Stun");
            AdjustSize(-1);
        }
    }
     
    public bool HasLost()
    {
        if (mCurrentHeroState == HeroState.Lost)
            return true;
        else
            return false;
    }
     
    public void Feed()
    {
        AdjustSize(1);
        AudioSupport.PlayACue("Chomp");
    }

With the hero completed, you can now begin working on the base class for the enemies. In the design outline for the game, each enemy has their own patrolling style; because of this commonality, a heavily modified PatrolEnemy class can be used as the base class. The extensiveness of the modifications is to allow enemies the ability to utilize sprite sheets and other customized behavior. Due to the variety of changes, we will provide a quick breakdown of the entire class.

Modifying the PatrolEnemy class

  1. Recall that the PatrolEnemy class was introduced in Chapter 6 to support semiautonomous behaviors. Here we start by changing the PatrolEnemy class to inherit from the SpritePrimitive class. Then, add a new state called StunState to the PatrolState enum. Also create a new enum called PatrolType.
    public class PatrolEnemy : SpritePrimitive
    {
        protected enum PatrolState
        {
            PatrolState,
            ChaseHero,
            StuntState
        }
     
        protected enum PatrolType
        {
            FreeRoam,
            LeftRight,
            UpDown
        }
        ...
    }
  2. Now add the following constants and color variables shown in the following code for keeping track of the enemies’ various properties such as speed, aggro radius, and size.
     
        // Constants  ...
        private const float kPatrolSpeed = 0.2f;
        private const float kCloseEnough = 20f; // distance to trigger next patrol target
        private const float kDistToBeginChase = 40f; // distance to trigger patrol chasing of hero
        private const int kStateTimer = 60 * 5; // this is about 5 seconds
        private const int kStunCycle = kStateTimer / 2; // half of regular state timer
        private const float kChaseSpeed = 0.3f;
        protected const float kEnemyWidth = 10f;
        protected const int kInitFishSize = 1;
     
        private Color kPatrolTint = Color.White;
        private Color kChaseTint = Color.OrangeRed;
        private Color kStuntTint = Color.LightCyan;
  3. You can now add the instance variables and successors for the state timer, destroy flag, and fish size.
     
        private Vector2 mTargetPosition;            // Target position we are moving towards
        private PatrolState mCurrentState;          // Current State
        protected PatrolType mCurrentPatrolType;    // Current Patrol Type
        protected EnemyType mCurrentEnemyType;
        protected bool mAllowRotate;
     
        private int mStateTimer;    // interestingly, with "gradual" velocity changing, we cannot
                                    // guarantee that we will ever reach the mTargetPosition
                                    // (we may ended up orbiting the target), so we set a timer
                                    // when timer is up, we transit
     
        private bool mDestoryFlag;
        public bool DestoryFlag { get { return mDestoryFlag; } }
        
        protected int mFishSize;
        public int FishSize
        {
            get { return mFishSize; }
            set
            {
                mFishSize = value;
                this.Size = new Vector2(mFishSize * kEnemyWidth + kEnemyWidth,
                                        mFishSize * kEnemyWidth + kEnemyWidth);
            }
        }
  4. Next is the constructor and parameter support for initializing the sprite sheet by utilizing the base class and for initializing the variables to their default values as shown in the following code. Do not forget to set the sprite sheets’ current frame and tick rate. Additionally, many of the values shown will be overridden by classes that inherit from PatrolEnemy.
    public PatrolEnemy(String image, Vector2 position, Vector2 size, int rowCounts, int columnCount, int padding) :
            base(image, position, size, rowCounts, columnCount, padding)
        {
            // causes update state to always change into a new state
            mTargetPosition = Position = Vector2.Zero;
            Velocity = Vector2.UnitY;
            mTintColor = kPatrolTint;
            mCurrentPatrolType = PatrolType.FreeRoam;
            Position = RandomPosition(true);
            mDestoryFlag = false;
            mAllowRotate = false;
            SetSpriteAnimation(0, 0, 1, 1, 10);
            FishSize = kInitFishSize;
            mCurrentEnemyType = EnemyType.BlowFish;
        }
  5. Now we can implement the update function. In the function, perform the common operations such as updating position and velocity as well as the facing direction of the enemy if the enemy is not stunned. Additionally, utilize a switch case to call update functions for when the enemy is patrolling and chasing the hero.
    public bool UpdatePatrol(Hero hero, out Vector2 caughtPos)
    {
        bool caught = false;
        caughtPos = Vector2.Zero;
     
        mStateTimer--;
     
        // perform operation common to all states ...
        if (mCurrentState != PatrolState.StuntState)
        {
            base.Update();
            Vector2 toHero = hero.Position - Position;
            toHero.Normalize();
            Vector2 toTarget = mTargetPosition - Position;
            float distToTarget = toTarget.Length();
            toTarget /= distToTarget; // this is the same as normalization
            ComputeNewDirection(toTarget, toHero);
     
            switch (mCurrentState)
            {
                case PatrolState.PatrolState:
                     UpdatePatrolState(hero, distToTarget);
                     break;
     
                case PatrolState.ChaseHero:
                     caught = UpdateChaseHeroState(hero, distToTarget, out caughtPos);
                     break;
            }
        }
        else
        {
            UpdateStuntState(hero);
        }
        return caught;
    }
    1. Create the UpdatePatrolState() function, which updates the enemy’s state depending upon its PatrolType. If the destination has been reached or the state timer expires, you can see that a different target generation function is called depending upon the PatrolType. Additionally, a check needs to occur to see if the enemy should chase the hero.
      private void UpdatePatrolState(GameObject hero, float distToTarget)
      {
           if ((mStateTimer < 0) || (distToTarget < kCloseEnough))
           {
               switch (mCurrentPatrolType)
               {
                  case PatrolType.FreeRoam:
                       RandomNextTarget();
                       break;
                  case PatrolType.LeftRight:
                       GenerateLeftRightTarget();
                       break;
                  case PatrolType.UpDown:
                       GenerateUpDownTarget();
                       break;
              }
          }
          DetectHero(hero); // check if we should transit to ChaseHero
      }
    2. Now create the UpdateChaseHeroState() function, which handles the reaction of the enemy when colliding with the hero. You can see that depending upon the enemy type, different reactions occur. For BlowFish, the hero size is decreased and the BlowFish is destroyed; for JellyFish, the hero is stunned; and for FightingFish, a size comparison is done and either the hero feeds on the FightingFish or vice versa. Additionally, if the state timer has expired and the fish is not caught a new random target is selected.
      private bool UpdateChaseHeroState(Hero hero, float distToHero, out Vector2 pos)
      {
          bool caught = false;
          caught = PixelTouches(hero, out pos);
          mTargetPosition = hero.Position;
       
          if (caught)
          {
              switch (mCurrentEnemyType)
              {
                   case EnemyType.BlowFish:
                       hero.AdjustSize(-1);
                       this.FishSize--;
                       this.mDestoryFlag = true;
                       break;
                  case EnemyType.JellyFish:
                       hero.StunHero();
                       break;
                  case EnemyType.FightingFish:
                       if (hero.HeroSize > this.FishSize)
                       {
                          this.FishSize--;
                          this.mDestoryFlag = true;
                          hero.Feed();
                       }
                       else if (hero.HeroSize <= this.FishSize)
                       {
                          this.FishSize--;
                          this.mDestoryFlag = true;
                          hero.AdjustSize(-1);
                       }
                       break;
                  default:
                       break;
              }
          }
          else if (mStateTimer < 0)
              RandomNextTarget();
       
          return caught;
      }
    3. In the UpdateStuntState() function, simply wait for the state timer to expire and then transition to the chasing state.
      private void UpdateStuntState(Hero hero)
      {
          if (mStateTimer < 0)
              SetToChaseState(hero);
      }
  6. Next, add functions for changing between states that simply modify the enemy’s current tint color, play, and audio cue and set the current state.
    public void SetToStuntState()
    {
        mTintColor = kStuntTint;
        mStateTimer = kStunCycle;
        mCurrentState = PatrolState.StuntState;
        AudioSupport.PlayACue("Stun");
    }
  7. Create a simple function for detecting whether the hero is within chasing distance. If so, change the enemy’s state.
    private void DetectHero(GameObject hero)
    {
        Vector2 toHero = hero.Position - Position;
        if (toHero.Length() < kDistToBeginChase)
            SetToChaseState(hero);
    }
  8. The ComputeNewSpeedAndResetTimer() function remains unchanged from the previous version of patrol enemy.
    private void ComputeNewSpeedAndResetTimer()
    {
        Speed = kPatrolSpeed * (0.8f + (float)(0.4 * Game1.sRan.NextDouble()));
                                                     // speed: ranges between 80% to 120
        mStateTimer = (int)(kStateTimer * (0.8f + (float)(0.6 * Game1.sRan.NextDouble())));
    }
  9. In the ComputeNewDirection() function, due to the side-scrolling nature of the game, if we allowed full rotation for enemies their faces would look odd. In order to avoid this, a new case is added which only allows the enemy to look left or right depending upon its facing direction. This is done by utilizing sprite sheet. For enemies without faces such as the JellyFish, the existing code still works.
    private void ComputeNewDirection(Vector2 toTarget, Vector2 toHero)
    {
        if (mAllowRotate)
        {
            // figure out if we should continue to adjust our direction ...
            double cosTheta = Vector2.Dot(toTarget, FrontDirection);
            float theta = (float)Math.Acos(cosTheta);
            if (theta > float.Epsilon)
            {
                Vector3 frontDir3 = new Vector3(FrontDirection, 0f);
                Vector3 toTarget3 = new Vector3(toTarget, 0f);
                Vector3 zDir = Vector3.Cross(frontDir3, toTarget3);
                RotateAngleInRadian -= Math.Sign(zDir.Z) * 0.03f * theta;
                                            // rotate 5% at a time towards final direction
                VelocityDirection = FrontDirection;
            }
        }
        else
        {
            VelocityDirection = toTarget;
            if (VelocityDirection.X > 0)
                SpriteCurrentRow = 1;
            else if (VelocityDirection.X < 0)
                SpriteCurrentRow = 0;
        }
    }
  10. The next set of functions is all related to generating a new random target. In the previous version of PatrolEnemy, you implemented RandomNextTarget(), which remains largely unchanged except for the addition of changing the enemy’s tint color. However, two new variations of this concept are added. One generates targets vertically and the second generates targets horizontally. In these functions, if you examine the following code, you will notice that the camera is referenced in order to keep the patrol targets on screen except when patrolling to the left and right.
    private void RandomNextTarget()
    {
        mStateTimer = kStateTimer;
        mCurrentState = PatrolState.PatrolState;
        mTintColor = kPatrolTint;
        // Generate a random begin state
        double initState = Game1.sRan.NextDouble();
        ...
        ComputeNewSpeedAndResetTimer();
    }
     
    private void GenerateUpDownTarget()
    {
        mStateTimer = kStateTimer;
        mCurrentState = PatrolState.PatrolState;
        mTintColor = kPatrolTint;
        float posY;
        float distToTopOfScreen = Camera.CameraWindowUpperLeftPosition.Y - PositionY;
        float distToBottomOfScreen = PositionY - Camera.CameraWindowLowerLeftPosition.Y;
        if (distToTopOfScreen >= distToBottomOfScreen)
        {
            posY = (float)Game1.sRan.NextDouble() *
                    distToTopOfScreen / 2 * 0.80f + PositionY + distToTopOfScreen / 2;
        }
        else
        {
            posY = (float)Game1.sRan.NextDouble() *
                    -distToBottomOfScreen / 2 * 0.80f + PositionY - distToBottomOfScreen / 2;
        }
     
        mTargetPosition = new Vector2(PositionX, posY);
        ComputeNewSpeedAndResetTimer();
    }
     
    private void GenerateLeftRightTarget()
    {
        mStateTimer = kStateTimer;
        mCurrentState = PatrolState.PatrolState;
        mTintColor = kPatrolTint;
        float posX;
        if (Velocity.X <= 0)
        {
            posX = (float)Game1.sRan.NextDouble() * Camera.Width /2 + PositionX;
        }
        else
        {
            posX = (float)Game1.sRan.NextDouble() * -Camera.Width /2 + PositionX;
        }
        mTargetPosition = new Vector2(posX, PositionY);
        ComputeNewSpeedAndResetTimer();
    }
  11. The ComputePoint() function ensures that enemies can generate targets offscreen. You’ll notice that the Y value is always within the camera bounds but the X value varies depending upon the enemy’s position.
    private const float sBorderRange = 0.55f;
    private Vector2 ComputePoint(double xOffset, double yOffset)
    {
        Vector2 min = new Vector2(PositionX - Camera.Width/2,
                                  Camera.CameraWindowLowerLeftPosition.Y);
        Vector2 max = new Vector2(PositionX + Camera.Width / 2,
                                  Camera.CameraWindowUpperLeftPosition.Y);
        Vector2 size = max - min;
        float x = min.X + size.X * (float)(xOffset + (sBorderRange * Game1.sRan.NextDouble()));
        float y = min.Y + size.Y * (float)(yOffset + (sBorderRange * Game1.sRan.NextDouble()));
        return new Vector2(x, y);
    }
  12. The random position functions all received few or no changes. You can see this shown in the following code.
    const float kMinOffset = -0.05f;
    private Vector2 RandomBottomRightPosition()
    {
        return ComputePoint(0.5, kMinOffset);
    }
    ...
  13. The last function you need to implement is used to generate a random position off-camera for spawning enemies. This is easily achieved by getting a random position within camera view and then shifting the position to the right.
    public Vector2 RandomPosition(bool offCamera)
    {
        Vector2 position;
        float posX = (float)Game1.sRan.NextDouble() * Camera.Width * 0.80f
                                    + Camera.Width * 0.10f;
        float posY = (float)Game1.sRan.NextDouble() * Camera.Height * 0.80f
                                    + Camera.Height * 0.10f;
      
        if(offCamera)
            posX += Camera.CameraWindowUpperRightPosition.X;
     
        position = new Vector2(posX, posY);
        return position;
    }

Now that the PatrolEnemy class has been modified to support multiple behaviors, it can serve as the base class for the game’s enemy types. In the game design outline, defined earlier, there were three enemy types, each with their own behavior. These three enemy types can now easily be represented by changing the value of mCurrentEnemyType variable defined in PatrolEnemy class.

Creating the JellyFish, BlowFish and FightingFish classes

  1. Begin by creating a new class called JellyFish that inherits from PatrolEnemy. Initialize the following variables in order to give the JellyFish the desired behavior. Remember to utilize the correct sprite sheet image and corresponding settings by calling the base constructor. The sprite sheet consists of two sprites; therefore, the row count is set to two and the column count is set to one.
    public class JellyFish : PatrolEnemy
    {
        public JellyFish() :
            base("ENEMY_3", Vector2.Zero,
                 new Vector2(kInitFishSize * kEnemyWidth + kEnemyWidth,
                 kInitFishSize * kEnemyWidth + kEnemyWidth), 2, 1, 0)
        {
            mAllowRotate = true;
            mInitFrontDir = Vector2.UnitY;
            mCurrentPatrolType = PatrolType.FreeRoam;
            FishSize = kInitFishSize;
            mCurrentEnemyType = EnemyType.JellyFish;
        }
    }
  2. Now create a new class called BlowFish that inherits from PatrolEnemy. Initialize its behavior variables to the following values. Remember to utilize the BlowFish sprite sheet. Notice that in the base constructor, the column count is now set to two. This is because the BlowFish sprite sheet consists of four sprites.
    public class BlowFish : PatrolEnemy
    {
        public BlowFish() :
            base("ENEMY_1", Vector2.Zero,
                  new Vector2(kInitFishSize * kEnemyWidth + kEnemyWidth,
                  kInitFishSize * kEnemyWidth + kEnemyWidth), 2, 2, 0)
        {
            mAllowRotate = false;
            mInitFrontDir = Vector2.UnitX;
            mCurrentPatrolType = PatrolType.UpDown;
            FishSize = kInitFishSize;
            mCurrentEnemyType = EnemyType.BlowFish;
        }
    }
  3. Lastly, create a new class called FightingFish that inherits from PatrolEnemy. Initialize its behaviors to the following values and pass in the correct sprite sheet to the base constructor.
    public class FightingFish : PatrolEnemy
    {
        public FightingFish() :
            base("ENEMY_2", Vector2.Zero,
                 new Vector2(kInitFishSize * kEnemyWidth + kEnemyWidth,
                 kInitFishSize * kEnemyWidth + kEnemyWidth), 2, 2, 0)
        {
            mAllowRotate = false;
            mInitFrontDir = Vector2.UnitX;
            mCurrentPatrolType = PatrolType.LeftRight;
            FishSize = kInitFishSize;
            mCurrentEnemyType = EnemyType.FightingFish;
        }
    }

Now that all of the enemy types have been created, the class to organize the enemies by controlling their creation and destruction can be implemented. This is achieved by creating a class to contain the enemies in a set in the same way that you have done in previous projects. This concept of combining objects to form a more complex object is known as composition in object-oriented programming.

Creating the PatrolEnemySet class

  1. Create a new class called PatrolEnemySet. Similarly to the Hero, add static variables and functions for supporting particles. Next, add a constant for the number of enemies to spawn initially. Lastly, add a list to hold all the enemies and a distance interval at which a new enemy will be added.
    public enum EnemyType
    {
        BlowFish = 0,
        JellyFish = 1,
        FightingFish = 2
    }
     
    public class PatrolEnemySet
    {
        #region Particles
        private const float kCollideParticleSize = 3f;
        private const int kCollideParticleLife = 80;
        private static ParticleSystem sCollisionEffect = new ParticleSystem();
        // to support particle system
        static private ParticlePrimitive CreateRedParticle(Vector2 pos)
        {
            return new ParticlePrimitive(pos, kCollideParticleSize, kCollideParticleLife);
        }
        static private ParticlePrimitive CreateDarkParticle(Vector2 pos)
        {
            return new DarkParticlePrimitive(pos,
                    kCollideParticleSize, kCollideParticleLife);
        }
        #endregion
     
        private List<PatrolEnemy> mTheSet = new List<PatrolEnemy>();
        private float mAddEnemyDistance = 100f;
     
        //Constants
        private const int kNumEnemies = 5;
     
        ...
    }

    image Note  An enum has been added above the PatrolEnemySet called EnemyType; this is to provide namespace wide access to the type. Also notice that each of the types has a distinct integer associated with it.

  2. Next, in the constructor, simply create the enemies by calling SpawnRandomPatrolEnemy() and add them to the set.
    public PatrolEnemySet()
    {
        // Create many ...
        for (int i = 0; i < kNumEnemies; i++)
        {
            PatrolEnemy e = SpawnRandomPatrolEnemy();
            mTheSet.Add(e);
        }
    }
  3. Now create the SpawnRandomPatrolEnemy() function. You can easily do this by getting a random number and instantiating the corresponding enemy type depending upon its value.
    public PatrolEnemy SpawnRandomPatrolEnemy()
    {
        int randNum = (int)(Game1.sRan.NextDouble() * 3);
        PatrolEnemy enemy = null;
        switch (randNum)
        {
            case (int)EnemyType.BlowFish:
                enemy = new BlowFish();
                break;
            case (int)EnemyType.JellyFish:
                enemy = new JellyFish();
                break;
            case (int)EnemyType.FightingFish:
                enemy = new FightingFish();
                break;
            default:
                break;
     
        }
        return enemy;
    }
  4. Now create the update function. In it, first calculate whether an additional enemy should be added and then remove destroyed enemies, respawn new enemies, update existing enemies, and check for collision with the bubbles shot by the hero. Lastly, remember to update the particles and respawn off-camera enemies.
    public int UpdateSet(Hero hero)
    {
        int count = 0;
        Vector2 touchPos;
     
        //Add an enemy at 100m and every 50 after
        //Should an additional enemy be added?
        if (hero.PositionX / 20 > mAddEnemyDistance)
        {
            PatrolEnemy e = SpawnRandomPatrolEnemy();
            mTheSet.Add(e);
            mAddEnemyDistance += 50;
        }
     
        // destroy and respawn, update and collide with bubbles
        for (int i = mTheSet.Count - 1; i >= 0; i--)
        {
            if (mTheSet[i].DestoryFlag)
            {
                mTheSet.Remove(mTheSet[i]);
                mTheSet.Add(SpawnRandomPatrolEnemy());
                continue;
            }
     
            if (mTheSet[i].UpdatePatrol(hero, out touchPos))
            {
                sCollisionEffect.AddEmitterAt(CreateRedParticle, touchPos);
                count++;
            }
     
            List<BubbleShot> allBubbleShots = hero.AllBubbleShots();
            int numBubbleShots = allBubbleShots.Count;
            for (int j = numBubbleShots - 1; j >= 0; j--)
            {
                if (allBubbleShots[j].PixelTouches(mTheSet[i], out touchPos))
                {
                    mTheSet[i].SetToStuntState();
                    allBubbleShots.RemoveAt(j);
                    sCollisionEffect.AddEmitterAt(CreateRedParticle, touchPos);
                }
            }
        }
     
        sCollisionEffect.UpdateParticles();
        RespawnEnemies();
        return count;
    }
  5. Create a function to respawn the enemies that are off to the left side of the camera. This is easily done by checking the X position of the enemy versus the left side position of the camera.
    // Respawn enemies that are off to the left side of the camera
    public void RespawnEnemies()
    {
        for (int i = mTheSet.Count - 1; i >= 0; i--)
        {
            if (mTheSet[i].PositionX < (Camera.CameraWindowLowerLeftPosition.X - mTheSet[i].Width))
            {
                mTheSet.Remove(mTheSet[i]);
                mTheSet.Add(SpawnRandomPatrolEnemy());
            }
        }
    }
  6. Lastly, create the draw function to draw each of the enemies and the particles.
    public void DrawSet()
    {
        foreach (var e in mTheSet)
            e.Draw();
     
        sCollisionEffect.DrawParticleSystem();
    }

With the three enemy types now created, the last interactive object needed is the food. The design states that the food can be eaten by the hero fish and will periodically fall from the top of the screen. This behavior is similar to the Simple Physics project from Chapter 5, where you are simply applying a downward vertical velocity to the object.

Creating the FishFood class

  1. Start by creating a new class called FishFood that inherits from SpritePrimitive. Add a constant for the food’s size and a bool flag to keep track of the food’s move state.
    public class FishFood : SpritePrimitive
    {
        private const float kFoodSize = 8;
        private bool mCanMove;
     
        ...
    }
  2. Now add a constructor to initialize the necessary variables to the following values. Utilize the worm sprite sheet provided. Remember to set the sprite animation’s starting frame and tick timer.
    public FishFood() :
        base("WORM_1", Vector2.Zero, new Vector2(kFoodSize, kFoodSize), 2, 1, 0)
    {
        Position = RandomPosition(true);
        SetSpriteAnimation(0, 0, 1, 1, 10);
        mCanMove = true;
        Speed = 0.2f;
    }
  3. Next, create its update function. During each update, first check if the food has entered the camera’s view from the right side of the screen. If so, apply movement in a downward direction. Then, check if the food has exited the left side of the screen. If so, move it to a new position off-camera. The last portion of the update function is to check its collisions. If it collides with the hero, feed the hero and respawn the food off-camera. If it collides with the seabed or seaweed, simply stop its movement.
    public void Update(Hero hero, List<Platform> floor, List<Platform> seaweed)
    {
        if (Camera.CameraWindowUpperRightPosition.X > PositionX && mCanMove)
        {
            VelocityDirection = new Vector2(0, -1);
            Speed = 0.2f;
            base.Update();
        }
     
        if (Camera.CameraWindowUpperLeftPosition.X > PositionX)
        {
            Position = RandomPosition(true);
            mCanMove = true;
        }
     
        Vector2 vec;
        if (hero.PixelTouches(this, out vec))
        {
            Stop();
            hero.Feed();
            Position = RandomPosition(true);
            mCanMove = true;
        }
        for (int i = 0; i < floor.Count; i++)
        {
            if (floor[i].PixelTouches(this, out vec))
            {
                Stop();
            }
        }
        for (int i = 0; i < seaweed.Count; i++)
        {
            if (seaweed[i].PixelTouches(this, out vec))
            {
                Stop();
            }
        }
    }
  4. Add a function for calculating the random position to spawn the food off-camera. This function is similar to the random position function you created in the patrol enemy class, except that since you want the food to fall from the top of the screen, the Y position is always set to the camera’s max Y value.
    private Vector2 RandomPosition(bool offCamera)
    {
        Vector2 position;
        float posX = (float)Game1.sRan.NextDouble() * Camera.Width * 0.80f + Camera.Width * 0.10f;
        float posY = Camera.CameraWindowUpperRightPosition.Y;
     
        if (offCamera)
            posX += Camera.CameraWindowUpperRightPosition.X + Camera.Width*2;
     
        position = new Vector2(posX, posY);
        return position;
    }
  5. The last function needed is the Stop() function; this simply prohibits the food from moving until it is enabled elsewhere.
    private void Stop()
    {
        mCanMove = false;
        Velocity = Vector2.Zero;
        Speed = 0;
    }

With all the objects for the game created, you now need a method of building an environment and filling it with enemies. In the game outline, it states that the environment will go on endlessly. The concept of endlessly might seem daunting at first glance; however in a simple game such as this, it can be quite easy accomplished.

By now, we have implemented hero's self-protection mechanism (BubbleShot), the Hero, enemy behaviors (PatrolEnemy), the enemies (JellyFish, BlowFish, and FightingFish), the collection set for the enemies (PatrolEnemySet), and the hero’s food (FishFood), all we need to do is to define the background (EnvironmentGenerator) and the game logic (GameState) to tie all of these classes together.

Creating the EnvironmentGenerator class

  1. Start by creating a class called EnvironmentGenerator. Add a list of Platform objects for the floor and the small and tall seaweed. Additionally, add variables for the sign, the enemies, and the food. Lastly, create the constants shown in the following code.
    public class EnvironmentGenerator
    {
        private List<Platform> mTheFloorSet;
        private List<Platform> mTheSeaweedTallSet;
        private List<Platform> mTheSeaweedSmallSet;
        private Platform mTheSign;
        private PatrolEnemySet mEnemies;
        private FishFood mFishFood;
     
        private int mOffsetCounter;
        private const int kSectionSize = 800;
        private const int kFloorAndRoofSize = 40;
        private const int kSignSize = 30;
        private const int kInitialSignPosX = 100;
        private const int kSignUnitScaler = 20;
     
        ...
    }
  2. Now create a constructor which is responsible for setting the environment to its initial state. You can accomplish this by creating the floor, seaweed, enemies, sign, and food. Notice that the floor is laid out across the bottom of the screen, and five tall and small seaweeds are dispersed randomly across it.
    public void InitializeEnvironment()
    {
        Camera.SetCameraWindow(Vector2.Zero, 300);
        mOffsetCounter = -20;
        mTheFloorSet = new List<Platform>();
        mTheSeaweedTallSet = new List<Platform>();
        mTheSeaweedSmallSet = new List<Platform>();
     
        mEnemies = new PatrolEnemySet();
     
        for (int i = 0; i < kSectionSize / kFloorAndRoofSize; i++)
        {
            mOffsetCounter += kFloorAndRoofSize;
            mTheFloorSet.Add(new
            Platform("GROUND_1",
                    new Vector2(mOffsetCounter, 20),
                    new Vector2(kFloorAndRoofSize, kFloorAndRoofSize)));
        }
     
        mTheSign = new Platform("SIGN_1",
                    new Vector2(kInitialSignPosX, kFloorAndRoofSize / 2 + kSignSize / 2),
                    new Vector2(kSignSize, kSignSize));
     
        float randNum;
        for (int i = 0; i < 5; i++)
        {
            randNum = (float)Game1.sRan.NextDouble() *
                            mTheFloorSet[mTheFloorSet.Count - 1].PositionX + kInitialSignPosX * 2;
            mTheSeaweedTallSet.Add(new Platform("SEAWEEDTALL_1",
                            new Vector2(randNum, kFloorAndRoofSize),
                            new Vector2(kFloorAndRoofSize / 1.5f, kFloorAndRoofSize * 1.5f)));
        }
     
        for (int i = 0; i < 5; i++)
        {
            randNum = (float)Game1.sRan.NextDouble() *
                            mTheFloorSet[mTheFloorSet.Count - 1].PositionX;
            mTheSeaweedSmallSet.Add(new Platform("SEAWEEDSMALL_1",
                            new Vector2(randNum, kFloorAndRoofSize / 2 - 5),
                            new Vector2(kFloorAndRoofSize / 2, kFloorAndRoofSize / 2)));
        }
     
        mFishFood = new FishFood();
    }
  3. You can now create an update function to support the endless regeneration of the terrain. There are several ways to achieve this, but one of the most straightforward is to simply replace objects that are moved off the left side of the camera to a new off-camera position to the right side of the screen. The length of the floor is longer than the camera’s width, and when the last floor tile is reached by the right side of the camera, the entire floor array is moved forward. This can be done without any noticeable artifacts because all of the floor tiles are the same. Lastly, the sign is reposted at various intervals along the seabed.
    public void Update(Hero theHero)
    {
        mFishFood.Update(theHero, mTheFloorSet, mTheSeaweedTallSet);
        mEnemies.UpdateSet(theHero);
        
        if (Camera.CameraWindowLowerRightPosition.X >
                    mTheFloorSet[mTheFloorSet.Count - 1].Position.X)
        {
            for (int i = 0; i < mTheFloorSet.Count; i++)
            {
                mTheFloorSet[i].PositionX += kFloorAndRoofSize * 10;
            }
     
            float randNum;
            for (int i = 0; i < mTheSeaweedTallSet.Count; i++)
            {
                if (mTheSeaweedTallSet[i].PositionX <
                            Camera.CameraWindowLowerLeftPosition.X - mTheSeaweedTallSet[i].Width)
                {
                    randNum = (float)Game1.sRan.NextDouble() * kSectionSize / 2
                                    + Camera.CameraWindowLowerRightPosition.X;
                    mTheSeaweedTallSet[i].PositionX = randNum;
                }
            }
     
            for (int i = 0; i < mTheSeaweedSmallSet.Count; i++)
            {
                if (mTheSeaweedSmallSet[i].PositionX <
                            Camera.CameraWindowLowerLeftPosition.X - mTheSeaweedTallSet[i].Width)
                {
                    randNum = (float)Game1.sRan.NextDouble() * kSectionSize / 2
                                    + Camera.CameraWindowLowerRightPosition.X;
                    mTheSeaweedSmallSet[i].PositionX = randNum;
                }
            }
        }
     
        if ((Camera.CameraWindowLowerLeftPosition.X - mTheSign.Width) > mTheSign.PositionX)
        {
            if (mTheSign.PositionX == kInitialSignPosX)
                mTheSign.PositionX = 0;
            mTheSign.PositionX += 500;
        }
    }
  4. Now you can create the draw function. This is simply a matter of calling each of this environment object’s Draw() methods including the enemies’ and the food’s. In addition, notice that the sign’s position is printed via its label.
    public void Draw()
    {
        for (int i = 0; i < mTheFloorSet.Count; i++)
            mTheFloorSet[i].Draw();
     
        mTheSign.Draw();
        String msg = (mTheSign.Position.X / 20).ToString() + "m";
     
        FontSupport.PrintStatusAt(mTheSign.Position, msg, Color.Black);
     
        for (int i = 0; i < mTheSeaweedTallSet.Count; i++)
            mTheSeaweedTallSet[i].Draw();
     
        for (int i = 0; i < mTheSeaweedSmallSet.Count; i++)
            mTheSeaweedSmallSet[i].Draw();
     
        mEnemies.DrawSet();
        mFishFood.Draw();
    }

With all the necessary objects for the game now created, it is time to modify the GameState in order to utilize them. The game state will handle the logic for the controls, the panning camera, and the swap between game screens.

Modifying the GameState class

  1. Start by adding in enum for the three different states of the GameState object. Then, add instance variables for the splash screens, the environment generator, the hero, and the distance traveled.
    public class GameState
    {
        public enum GameStates
        {
            StartScreen,
            Playing,
            Dead
        }
     
        private int mDistantTraveled = 0;
        private GameStates mCurrentGameState;
        private TexturedPrimitive mSplashScreen;
        private TexturedPrimitive mGameOverScreen;
        private EnvironmentGenerator mEnvironment;
        private Hero mHero;
     
        ...
     
    }
  2. Next, in the constructor, set the current game state to StartScreen, start the background music, and initialize the start menu. In the start menu’s initialization, set the splash screen to the camera’s view and notify the user of how they can start the game.
    public GameState()
    {
        mCurrentGameState = GameStates.StartScreen;
        AudioSupport.PlayBackgroundAudio("Mind_Meld", 0.5f);
        InitializeStartMenu();
    }
     
    public void InitializeStartMenu()
    {
        float centerX = Camera.CameraWindowUpperRightPosition.X - Camera.Width/2;
        float centerY = Camera.CameraWindowUpperRightPosition.Y- Camera.Height/2;
     
        mSplashScreen = new TexturedPrimitive("SPLASHSCREEN_1",
                    new Vector2(centerX, centerY), new Vector2(Camera.Width, Camera.Height));
        String msg = "Press the 'K' key to start.";
        mSplashScreen.Label = msg;
        mSplashScreen.LabelColor = Color.Black;
    }
  3. Next, create functions for initializing the gameplay and the game over screen. When gameplay is initialized, simply instantiate the hero and the environment. When the game over screen is initialized, instantiate the game over splash screen and notify the player how far they have managed to travel.
    public void InitializeGamePlay()
    {
        mHero = new Hero(new Vector2(20f, 30f));
        mEnvironment = new EnvironmentGenerator();
    }
     
    public void InitializeGameOverScreen()
    {
        float centerX = Camera.CameraWindowUpperRightPosition.X - Camera.Width/2;
        float centerY = Camera.CameraWindowUpperRightPosition.Y- Camera.Height/2;
     
        mGameOverScreen = new TexturedPrimitive("GAMEOVERSCREEN_1",
                            new Vector2(centerX, centerY),
                            new Vector2(Camera.Width, Camera.Height));
        String msg = mDistantTraveled +  "m traveled. Press the 'K' key to try agian.";
        mGameOverScreen.Label = msg;
        mGameOverScreen.LabelColor = Color.Black;
    }
  4. Now create an update function that updates the game depending upon the game’s current state.
    public void UpdateGame(GameTime gameTime)
    {
        switch(mCurrentGameState)
        {
            case GameStates.StartScreen:
                 UpdateStartScreen();
                 break;
            case GameStates.Playing:
                 UpdateGamePlay(gameTime);
                 break;
            case GameStates.Dead:
                 UpdateGameOverScreen();
                 break;
        }
    }
    1. For the UpdateStartScreen() function, poll whether the A button has been pressed. If so, change the current game state and initialize the gameplay.
      public void UpdateStartScreen()
      {
          if (InputWrapper.Buttons.A == ButtonState.Pressed)
          {
              mSplashScreen = null;
              mCurrentGameState = GameStates.Playing;
              InitializeGamePlay();
          }
      }
    2. The behavior of the UpdateGameOverScreen() function essentially mirrors that of the UpdateStartScreen() function. In fact, another function could easily be extracted out of them to reduce redundancy.
      public void UpdateGameOverScreen()
      {
          if (InputWrapper.Buttons.A == ButtonState.Pressed)
          {
              mGameOverScreen = null;
              mCurrentGameState = GameStates.Playing;
              InitializeGamePlay();
          }
      }
    3. For the UpdateGamePlay() function:
      • • record the distance traveled
      • • check whether the hero has lost
      • • check whether the player has shot a bubble
      • • update the hero and environment
      • • allow the hero to pan the camera to the right
      public void UpdateGamePlay(GameTime gameTime)
      {
          mDistantTraveled = (int)mHero.PositionX / 20;
          if (mHero.HasLost())
          {
              mCurrentGameState = GameStates.Dead;
              AudioSupport.PlayACue("Break");
              InitializeGameOverScreen();
              return;
          }
       
          bool shootBubbleShot = (InputWrapper.Buttons.A == ButtonState.Pressed);
          mHero.Update(gameTime, InputWrapper.ThumbSticks.Left, shootBubbleShot);
          mEnvironment.Update(mHero);
              
          #region hero moving the camera window
          float kBuffer = mHero.Width * 5f;
          float kHalfCameraSize = Camera.Width * 0.5f;
          Vector2 delta = Vector2.Zero;
          Vector2 cameraLL = Camera.CameraWindowLowerLeftPosition;
          Vector2 cameraUR = Camera.CameraWindowUpperRightPosition;
          const float kChaseRate = 0.05f;
       
          if (mHero.PositionX > (cameraUR.X - kHalfCameraSize))
          {
              delta.X = (mHero.PositionX + kHalfCameraSize - cameraUR.X) * kChaseRate;
          }
       
          Camera.MoveCameraBy(delta);
          #endregion
      }
  5. Finally, implement the draw function to draw the game depending upon its current state. During the gameplay state, remember to print out the distance traveled and size of the hero.
    public void DrawGame()
    {
        switch (mCurrentGameState)
        {
            case GameStates.StartScreen:
                 if(mSplashScreen != null)
                     mSplashScreen.Draw();
                 break;
            case GameStates.Playing:
                 mEnvironment.Draw();
                 mHero.Draw();
                 FontSupport.PrintStatus("Distance: " + mDistantTraveled
                                            + "  Size: " + mHero.HeroSize, null);
                 break;
            case GameStates.Dead:
                 if (mGameOverScreen != null)
                     mGameOverScreen.Draw();
                 break;
        }
    }

Congratulations! You have completed the entire Fish Food game! As described earlier in this chapter, this game is non-trivial, but it was not too complicated either. Figure 9-2 shows an example of what you should see when you play the Fish Food game.

9781430266044_Fig09-02.jpg

Figure 9-2. Example screen of the completed Fish Food game

Game criticisms and expansion

Now that the entire game has been implemented, build and run the code in order to try it out. Try and see how far you can travel and then see if you can beat that distance. What kind of impression did the game give? Was it too simplistic? How was the difficulty? How about the fun factor? Your answers to these questions are all relevant to the creation of your game. Overall, we hope you saw that the game is far from perfect. In fact, there are many areas of the game that could be improved. This includes areas such as gameplay, interface, controls, effects, theme, art, sound, and even the code itself.

If you have an idea that can improve the game in any of the areas, go ahead and implement it! If you’re struggling with thinking of an area to improve, give one of the following suggestions a try.

  • Content! Games can always use more content, such as enemy types, food types, hero character types, or even environment types.
  • Gameplay and functionality! This can include things like camera zoom, hero or enemy abilities, or food bonuses.
  • Effects! Often the best games have great effects such as satisfying sound or particle behaviors.
  • Controls! Do the controls feel intuitive? Is there a more natural way to play the game? Awkward controls often dampen the game’s fun factor.
  • Code quality and understandability! Code can always be improved! Can something be re-factored or polished to provide a simpler approach?

Remember that some changes may have effects on other portions of the game. For example, if you are to implement the camera zooming feature, upon zooming out you may find that the environment no longer behaves as you expect. However, we encourage you to expand and implement your ideas, if not in this game perhaps in your own. There may be something in this game that is applicable to a game you have been thinking about. That’s great! Cannibalize that section and or take what you have learned and start creating fun and challenging 2D games!

Device deployment and publishing your games

The MonoGame framework that we have worked with throughout this book is designed specifically to be a cross-platform library. With the philosophy of “Write Once, Play Everywhere,” the framework currently supports most of the popular operating systems including devices that run on Linux, Windows, iOS, and Android. This means that once you have developed a game, you will be able to build and run the same game on all of these devices.

Ultimately, games are built for gamers and everyone out there who may appreciate our efforts. Publishing a game used to be reserved for absolute professionals. Fortunately, the recent opening up of marketplaces by all of the major vendors means that anyone can participate and self-publish their own games!

Together with MonoGame’s cross-platform support, this means that you will eventually be able to publish your games on all of the platforms supported by MonoGame. As at the time of writing this book, though perfectly operational, the MonoGame cross-platform deployment mechanism is still undergoing significant changes. We encourage interested readers to refer to MonoGame website for up-to-date information at http://www.monogame.net.

Additionally, interested readers should also consult the various vendors’ self-publication guides:

Each vendor typically has separate and different license agreements that they would require you to sign, user interface design guidelines that they want your game to adhere to (e.g., font type, size), levels of testing that they expect you to perform on your game, documentations of machine capabilities and device sensors that your game requires, and for games on mobile devices, documentation of the kinds of player privacy information (e.g., location information, etc) that your game would access and how that information would be used. Please consult the preceding web sites for detailed requirements from each vendor.

Lastly, we should mention once again that this book focused only on the technical aspects of building games. As discussed in Chapter 1, technical know-how is a vital prerequisite for building fun games, but it is not the only factor. It is our opinion that, in general, it is a good idea to become knowledgeable with game design topics (please refer to Chapter 1 for some recommended references) before attempting to build games for self-publication, as the ideas and concepts presented are valuable tools that will help you make the most of your ideas. In fact, the reason you probably picked up this book is because you have many game ideas floating around in your head. The game design process is meant to assist you in documenting these ideas and making the jump from idea to the content and mechanics of a real game.

Finally, remember to enjoy the process! Building games is not a trivial task but if you enjoy the process of seeing your ideas come to life, it is well worth the time and effort. We hope that using the knowledge you have gained from this book, you can now begin the journey of creating your own 2D games. Good luck and have fun building and playing your first game!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset