Building Your First 2D Game
After completing this chapter, you will be able to:
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.
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.
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.
Figure 9-1. Running the Fish Food game
The project’s controls are as follows:
The goals of this project are as follows:
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:
Add the following resources, which can be found in the Chapter9SourceCodeResources folder, into your content project before you begin:
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.
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.
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.
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);
}
}
public bool IsOnScreen()
{
// take advantage of the camera window bound check
Camera.CameraWindowCollisionStatus status = Camera.CollidedWithCameraWindow(this);
return (Camera.CameraWindowCollisionStatus.InsideWindow == status);
}
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
...
}
//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);
}
}
private enum HeroState
{
Playing,
Stunned,
Unstunnable,
Lost
}
private HeroState mCurrentHeroState;
private List<BubbleShot> mBubbleShots;
public List<BubbleShot> AllBubbleShots() { return mBubbleShots; }
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;
}
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;
}
}
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();
}
}
public void UpdateStunnedState(GameTime gameTime)
{
float deltaTime = gameTime.ElapsedGameTime.Milliseconds;
mStunTimer += deltaTime / 1000;
if (mStunTimer >= kStunTimer)
{
mStunTimer = 0;
mCurrentHeroState = HeroState.Unstunnable;
}
}
public void UpdateUnstunnableState(GameTime gameTime)
{
float deltaTime = gameTime.ElapsedGameTime.Milliseconds;
mStunTimer += deltaTime / 1000;
if (mStunTimer >= kStunTimer)
{
mStunTimer = 0;
mCurrentHeroState = HeroState.Playing;
}
}
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.
public override void Draw()
{
base.Draw();
foreach (var j in mBubbleShots)
j.Draw();
mCollisionEffect.DrawParticleSystem();
}
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
public class PatrolEnemy : SpritePrimitive
{
protected enum PatrolState
{
PatrolState,
ChaseHero,
StuntState
}
protected enum PatrolType
{
FreeRoam,
LeftRight,
UpDown
}
...
}
// 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;
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);
}
}
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;
}
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;
}
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
}
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;
}
private void UpdateStuntState(Hero hero)
{
if (mStateTimer < 0)
SetToChaseState(hero);
}
public void SetToStuntState()
{
mTintColor = kStuntTint;
mStateTimer = kStunCycle;
mCurrentState = PatrolState.StuntState;
AudioSupport.PlayACue("Stun");
}
private void DetectHero(GameObject hero)
{
Vector2 toHero = hero.Position - Position;
if (toHero.Length() < kDistToBeginChase)
SetToChaseState(hero);
}
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())));
}
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;
}
}
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();
}
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);
}
const float kMinOffset = -0.05f;
private Vector2 RandomBottomRightPosition()
{
return ComputePoint(0.5, kMinOffset);
}
...
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
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;
}
}
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;
}
}
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
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;
...
}
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.
public PatrolEnemySet()
{
// Create many ...
for (int i = 0; i < kNumEnemies; i++)
{
PatrolEnemy e = SpawnRandomPatrolEnemy();
mTheSet.Add(e);
}
}
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;
}
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;
}
// 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());
}
}
}
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
public class FishFood : SpritePrimitive
{
private const float kFoodSize = 8;
private bool mCanMove;
...
}
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;
}
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();
}
}
}
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;
}
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
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;
...
}
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();
}
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;
}
}
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.
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;
...
}
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;
}
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;
}
public void UpdateGame(GameTime gameTime)
{
switch(mCurrentGameState)
{
case GameStates.StartScreen:
UpdateStartScreen();
break;
case GameStates.Playing:
UpdateGamePlay(gameTime);
break;
case GameStates.Dead:
UpdateGameOverScreen();
break;
}
}
public void UpdateStartScreen()
{
if (InputWrapper.Buttons.A == ButtonState.Pressed)
{
mSplashScreen = null;
mCurrentGameState = GameStates.Playing;
InitializeGamePlay();
}
}
public void UpdateGameOverScreen()
{
if (InputWrapper.Buttons.A == ButtonState.Pressed)
{
mGameOverScreen = null;
mCurrentGameState = GameStates.Playing;
InitializeGamePlay();
}
}
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
}
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.
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.
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!