Introduction
This article is all about my experience of developing a SkyWar game for CodeProject’s Intel AppUp competition. You can download complete source code of game attached with the article. The game is in
validation stage in Microsoft store and you will find the same game soon in store for windows phone 7.5 mango, windows phone 8. It is freely downloadable.
Background
I come across Intel app innovation contest and decided to take part in this competition. It was about creating a windows 8 app, which demonstrates best features of
Intel Ultrabook. I thought lots of ideas and finally decided to create a windows 8 game.
Ultrabook is enabled with feature like touch; sensor, GPS and smart connect etc. So I thought of creating a game which will utilize most of touch and sensors and will have good user experience. I was little aware of xna framework. I thought using
XNA we can create windows 8 game apps. But window 8 does not
support XNA anymore, but Windows Phone 8.0 SDK does. So I started looking for alternatives and found this great article by bob. I found that using Mono Game framework; we can easily create game apps for windows 8 and even port games free of cost to android and iPhone. The best thing about it is, it’s exactly
similar like XNA.
Game Story Line
Before we
write game, we should be clear in our mind what we are going to do. What's the
goal of game, how game will proceed, what will be challenge for player, what
will be ending criteria, etc.
Finally,
I came up with an idea of sky shooting game called Sky War which is inspired by
the old arcade games.
This game
allows the player to control a space fighter ship in a star field.
Player
will face different enemies in galaxy. In each stage, player will face set of
enemies, once player defeat them, will be facing a boss or dragon enemy of
stage.
Player
will be provided with 3 initial life lines and 100% health in start. But later
to defeat such dangerous enemies, player will be provided with amazing life
lines in game.
The more
player play the more complex game will become with difficult dragons or boss
enemies.
We should have even detailed story line, which
texture will do what, how many objects will be there etc. For this article I’ll
just brief story.
Setting Up Environment
Before we
start constructing game for windows 8, it will be good to know little about
Mono Game Framework and how to setup environment for creating games.
MonoGame provides a cross platform XNA Framework
implementation for XNA developers who want to take their code to non-Microsoft
platforms as well as the ability. You can read further about mono game Mono Game Overview. Let me
quickly jump to setting up environment.
How to setup Mono Game Environment?
There are two ways of setting up environment and I will explain easy
way.
1) Install
visual studio 2010. Install its service pack sp1 on it (which is required for
installation of windows mobile sdk 7.1).
2) Install
Windows Mobile SDK 7.1 which will automatically install Xna Game Studio 4.0 on
your machine. (You must be wondering why we have installed vs 2010 because xna
template is not available in vs2012 and further MonoGame framework does not
have content pipeline project, a pre-compiler step in preparation of graphic
and audio use at runtime in xna)
3) Install
Visual studio 2012.
4) Install
Mono Game installer which you can download from here.
This will setup environment for you and create MonoGame template in
Visual studio 2010 and 2012.
Second approach you can read from here. Now we are all set to create our
first Mono Game for windows 8.
Let's Begin
Before creating any game we should have these following things ready for us -
1) Game
story line (detail description of game, stages planning etc)
2) Textures (which we will use in game like any sort of images, sounds etc)
3) Game state management
I have given the brief game
story which you can find above, but for games we should have a detailed story
line. Later I spend lot to time collecting right textures for game from
different free websites. I modified them in Photoshop as per my need.
If you are starter and don't
have any idea at all what’s XNA is? Please find links and books references at
bottom of this article.
Once we have all these
ready, let’s start coding.
Let’s open Visual Studio 2012
and select mono game project template. Give your project a name Sky War. Here
we should notice, we don’t have content project. Best approach to deal with
this is, create a Content folder in your game. Now open Visual Studio 2010, add
all textures there which you want in game, build project and add in your
content folder in sky war game. Now set “copy if newer” to all textures in
content folder. This is how we need to deal with not having content project in
Mono Game framework.
Now we can start
writing classes for each object of game. Below attached snap shots will give
you brief idea of game, what we gone develop –
After
looking at images we can identify below objects of game –
Scrolling
Background (for
creating moving background object), SpaceShip (it’s the ship which
player will be flying in galaxy), Bullet (which will be reused by player
and aliens), Alien (enemy ships), Alien Component ( Number
of aliens visible on screen and initiating them back), AlienBoss (dragon
of stage), AnimatedSprite (for animation of texture), Audio Library
(for playing audios), Alien Type (game have different types of aliens
with different capabilities), Level Data (for level information), Power
Up (which powers currently player have) and main game screen which will
initiate everything. I will discuss now most difficult part of code one by one,
rest of code is fairly simple to understand. You can download the attached
source code.
Microsoft
has provided lots of game state management tutorial and code on their websites
which you can find here. So we need not to discuss it
here. I quickly jumped on link and downloaded code. I modified it for
supporting touch and keyboard both. You find classes attached with article. We
will focus on objects.
Scrolling Background (Moving Background)
I
started writing code by creating a moving background. When you will run the
game and when you closely notice, the background have two images laying over
each other where second image will keep on moving on top of first image. This
gives feel to user that player is moving in galaxy. This happens as user
touches Gas button or press up keyboard key. Below picture will give you fare
bit of idea.
We have two images here, background image and an
image parallel to it, which will move on top of it. Below code I used for
creating moving background.
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SkyWar
{
public class Background
{
Texture2D t2dBackground, t2dParallax;
GameData _gameData = null;
int iViewportWidth = 800;
int iViewportHeight = 600;
int iBackgroundWidth = 1600;
int iBackgroundHeight = 1080;
int iParallaxWidth = 1600;
int iParallaxHeight = 1080;
int iBackgroundOffset;
int iParallaxOffset;
public int BackgroundOffset
{
get { return iBackgroundOffset; }
set
{
iBackgroundOffset = value;
if (iBackgroundOffset < 0)
{
iBackgroundOffset += iBackgroundHeight;
}
if (iBackgroundOffset > iBackgroundHeight)
{
iBackgroundOffset -= iBackgroundHeight;
}
}
}
public int ParallaxOffset
{
get { return iParallaxOffset; }
set
{
iParallaxOffset = value;
if (iParallaxOffset < 0)
{
iParallaxOffset += iParallaxHeight;
}
if (iParallaxOffset > iParallaxHeight)
{
iParallaxOffset -= iParallaxHeight;
}
}
}
bool drawParallax = true;
public bool DrawParallax
{
get { return drawParallax; }
set { drawParallax = value; }
}
public Background(ContentManager content, string sBackground, string sParallax)
{
_gameData = GameData.GetGameDataInstance();
iViewportHeight = _gameData.GameScreenHeight;
iViewportWidth = _gameData.GameScreenWidth;
t2dBackground = content.Load<Texture2D>(sBackground);
_gameData.BackgroundWidth = iBackgroundWidth = t2dBackground.Width;
_gameData.BackgroundHeight = iBackgroundHeight = t2dBackground.Height;
t2dParallax = content.Load<Texture2D>(sParallax);
iParallaxWidth = t2dParallax.Width;
iParallaxHeight = t2dParallax.Height;
}
public Background(ContentManager content, string sBackground)
{
_gameData = GameData.GetGameDataInstance();
iViewportHeight = _gameData.GameScreenHeight;
iViewportWidth = _gameData.GameScreenWidth;
t2dBackground = content.Load<Texture2D>(sBackground);
_gameData.BackgroundWidth = iBackgroundWidth = t2dBackground.Width;
_gameData.BackgroundHeight = iBackgroundHeight = t2dBackground.Height;
t2dParallax = t2dBackground;
iParallaxWidth = t2dParallax.Width;
iParallaxHeight = t2dParallax.Height;
drawParallax = false;
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(t2dBackground, new Rectangle(0, -1 *
iBackgroundOffset, iViewportWidth, iBackgroundHeight), Color.White);
if (iBackgroundOffset > iBackgroundHeight - iViewportHeight)
{
spriteBatch.Draw(t2dBackground, new Rectangle(0, (-1 * iBackgroundOffset) +
iBackgroundHeight, iViewportWidth, iBackgroundHeight), Color.White);
}
if (drawParallax)
{
spriteBatch.Draw(t2dParallax, new Rectangle(0, -1 * iParallaxOffset,
iViewportWidth, iParallaxHeight), Color.SlateGray);
if (iParallaxOffset > iParallaxHeight - iViewportHeight)
{
spriteBatch.Draw(t2dParallax, new Rectangle(0, (-1 * iParallaxOffset) +
iParallaxHeight, iViewportWidth, iParallaxHeight), Color.SlateGray);
}
}
}
}
}
Here, iViewportWidth
and iViewportHeight
is game width and height.
iBackgroundWidth
and iBackgroundHeight is main background width and height.
iParallaxWidth
and iParallaxHeight
is the image height laying over it.
In addition to the variables, I have BackgroundOffset
and ParallalxOffset
properties to set them from outside the class. You will notice that the two
properties above check to see if we have gone off of either end of each texture
and wrap around if necessary. If we don't do this, we can scroll top off the end
of the background image.
When we use this constructor to create an instance of our Background class, we
will pass in a content manager and use it to load the two textures. Our
constructor will set iBackgroundWidth
, iBackgroundHeight
, iParallaxWidth
, and
iParallaxHeight
to the appropriate values from the textures we loaded.
Our Draw method is passed a SpriteBatch
to use, and we will
assume that we are within a SpriteBatch.Begin
and
SpriteBatch.End
call set.
Our first statement draws the background image, offset by the background offset:
spriteBatch.Draw(t2dBackground, new Rectangle(0, -1 * iBackgroundOffset,
iViewportWidth, iBackgroundHeight), Color.White);
When we create the destination rectangle, we set the
left position to "-1 * iBackgroundOffset
", which results in shifting
our image to the left by a number of pixels equal to iBackgroundOffset
.
This works great except when we get to the point where drawing the offset image doesn't fill our entire display. If we don't account for that, we will
end up with a partially filled background and then the XNA Blue Window. This is where the next statement comes in:
if (iBackgroundOffset > iBackgroundHeight - iViewportHeight)
{
spriteBatch.Draw(t2dBackground, new Rectangle(0, (-1 * iBackgroundOffset) +
iBackgroundHeight, iViewportWidth, iBackgroundHeight), Color.White);
}
First we check to see if we need to draw a
second copy of the image. If so, we repeat the above draw call except that we
modify the position of the second destination rectangle by adding the height of
the background image to the call. This will line the second copy of the image
up to start at exactly the point where the first copy ended. We will never need
to draw more than two of these images to fill the screen, since the height of
the background image is greater than the height of the screen. The rest of our
draw function does exactly the same process with the parallax star overlay
after checking to see if we should be drawing it. It uses the same offsetting
and second copy drawing logic as the background. Next, we'll need to actually
initialize the background by running its constructor. In the LoadContent
method
of our game, let’s add the following code:
background = new Background(Content,@"Textures\PrimaryBackground",@"Textures\ParallaxStars");
Here, we
call the Background object's constructor and pass it our Content Manager
object, along with the asset names of the two textures we will be using.
That handles the setup, so all that is left is
to draw our background. Scroll down to the
Draw method and add the following
line Before the draw code, but inside the
spriteBatch.Begin
and
spriteBatch.End
block:
background.Draw(spriteBatch);
Player
(SpaceShip)
I had one of the biggest challenge to provide
game support for all kind of windows 8 devices (touch and non touch based
systems). Ultrabook has awesome touch drivers which works with Windows 8 in
an elegant manner and user does not need to change even single piece of
code. Below is code for player class.
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SkyWar
{
class SpaceShip
{
GameData _gameData;
AnimatedSprite asSprite;
Texture2D t2dSpaceShip;
SoundEffectInstance spaceShipDead;
bool sActive;
public bool IsSpaceShipActive { get { return sActive; } private set { sActive = value; } }
int iX = 604;
int iY = 260;
int iFacing = 1;
bool bThrusting = false;
int iScrollRate = 0;
int iShipAccelerationRate = 1;
int iShipVerticalMoveRate = 3;
float fSpeedChangeCount = 0.0f;
float fSpeedChangeDelay = 0.1f;
float fVerticalChangeCount = 0.0f;
float fVerticalChangeDelay = 0.01f;
TimeSpan _deathDelay, _deathCountDelay;
float[] fFireRateDelay = new float[3] { 0.15f, 0.1f, 0.05f };
float fSuperBombDelayTimer = 2f;
int iMaxSuperBombs = 5;
int iMaxWeaponLevel = 1;
int iShipMaxFireRate = 2;
int iMaxAccelerationModifier = 5;
int iSuperBombs = 0;
int iWeaponLevel = 0;
int iWeaponFireRate = 0;
int iAccelerationModifier = 1;
public int X
{
get { return iX; }
set { iX = value; }
}
public int Y
{
get { return iY; }
set { iY = value; }
}
public int Facing
{
get { return iFacing; }
set { iFacing = value; }
}
public bool Thrusting
{
get { return bThrusting; }
set { bThrusting = value; }
}
public int ScrollRate
{
get { return iScrollRate; }
set { iScrollRate = value; }
}
public int AccelerationRate
{
get { return iShipAccelerationRate; }
set { iShipAccelerationRate = value; }
}
public int VerticalMovementRate
{
get { return iShipVerticalMoveRate; }
set { iShipVerticalMoveRate = value; }
}
public float SpeedChangeCount
{
get { return fSpeedChangeCount; }
set { fSpeedChangeCount = value; }
}
public float SpeedChangeDelay
{
get { return fSpeedChangeDelay; }
set { fSpeedChangeDelay = value; }
}
public float VerticalChangeCount
{
get { return fVerticalChangeCount; }
set { fVerticalChangeCount = value; }
}
public float VerticalChangeDelay
{
get { return fVerticalChangeDelay; }
set { fVerticalChangeDelay = value; }
}
public int ShipWidth { get; private set; }
public int ShipHeight { get; private set; }
public Rectangle BoundingBox
{
get { return new Rectangle(iX, iY, _gameData.SpaceShipWidth, _gameData.SpaceShipHeight); }
}
public int SuperBombs
{
get { return iSuperBombs; }
set
{
iSuperBombs = (int)MathHelper.Clamp(value,
0, iMaxSuperBombs);
}
}
public int FireRate
{
get { return iWeaponFireRate; }
set
{
iWeaponFireRate = (int)MathHelper.Clamp(value,
0, iShipMaxFireRate);
}
}
public float FireDelay
{
get { return fFireRateDelay[iWeaponFireRate]; }
}
public int WeaponLevel
{
get { return iWeaponLevel; }
set
{
iWeaponLevel = (int)MathHelper.Clamp(value,
0, iMaxWeaponLevel);
}
}
public float SuperBombDelay
{
get { return fSuperBombDelayTimer; }
}
public int AccelerationBonus
{
get { return iAccelerationModifier; }
set
{
iAccelerationModifier = (int)MathHelper.Clamp(value,
1, iMaxAccelerationModifier);
}
}
public SpaceShip(ContentManager content, string texture)
{
_gameData = GameData.GetGameDataInstance();
t2dSpaceShip = content.Load<texture2d>(texture);
asSprite = new AnimatedSprite(t2dSpaceShip, 0, 0, t2dSpaceShip.Width, t2dSpaceShip.Height, 1);
ShipWidth = t2dSpaceShip.Width;
ShipHeight = t2dSpaceShip.Height;
iX = _gameData.GameScreenWidth / 2 - t2dSpaceShip.Width / 2;
iY = _gameData.GameScreenHeight / 2 + _gameData.GameScreenHeight / 4 + _gameData.GameScreenHeight / 6;
asSprite.IsAnimating = false;
sActive = true;
spaceShipDead = _gameData.AudioLibarary.BossDead;
}
public void ResetSpaceShip()
{
iX = _gameData.GameScreenWidth / 2 - t2dSpaceShip.Width / 2;
iY = _gameData.GameScreenHeight / 2 + _gameData.GameScreenHeight / 4 + _gameData.GameScreenHeight / 6;
iAccelerationModifier = 1;
iWeaponFireRate = 0;
iWeaponLevel = 0;
iSuperBombs = 0; iScrollRate = 0;
}
public void PlayerHit(int power = 1)
{
_gameData.PlayerHealth -= 20;
if (_gameData.PlayerHealth == 0)
{
spaceShipDead.Play();
_gameData.PlayerHealth = 0;
sActive = false;
_gameData.PlayerLives--;
if (_gameData.PlayerLives == 0)
_gameData.IsGameOver = true;
else
{
_deathDelay = TimeSpan.FromSeconds(3);
_deathCountDelay = TimeSpan.Zero;
}
}
else if (_gameData.PlayerHealth < 0)
_gameData.PlayerHealth = 0;
}
public void Update(GameTime gametime)
{
if ((!sActive) && _deathDelay > TimeSpan.Zero)
{
if ((_deathCountDelay += gametime.ElapsedGameTime) > _deathDelay)
{
sActive = true;
_gameData.PlayerHealth = 100;
ResetSpaceShip();
_deathDelay = TimeSpan.Zero;
}
}
}
public void Draw(SpriteBatch sb)
{
if (sActive)
asSprite.Draw(sb, iX, iY, false);
}
}
}
AnimatedSprite
will, of course, be used to house the image
above. We will be using the bAnimating
feature of our animated sprite class to
prevent it from animating automatically so that we can control which frame of
the ship "animation" is displayed.
iX
and iY
determine the
location of the ship on the screen. In the game we will be producing for this
tutorial series, the ship will never leave the center of the
screen (horizontally) but will move freely vertically.
iFacing
determines which direction the player is
currently facing. 0=Right, 1=Left.
bThrusting
is set to true when the player is actively
moving in a direction (as opposed to coasting in that direction).
The next few variables are all related to how our ship/screen moves.
We'll get into more detail below when we add the ship to the screen and handle
movement.
iScrollRate
determines the speed and direction that the
ship is actually moving (this is not related to the Facing, as it is
possible to be moving one direction while facing the other). Positive values
indicate rightward movement, negative values leftwards. The magnitude of the
number determines the number of pixels per update frame that the screen will
scroll.
iShipAccelerationRate
sets how fast iScrollRate
can change. A value of 1 means that every time the speed changes it changes by
1.
iShipVerticalMovementRate
is the number of pixels the
ship moves vertically when the player presses up or down on the
gamepad/keyboard.
fSpeedChangeCount
and fSpeedChangeDelay
determine
how rapidly iShipAccelerationRate
can be applied. fSpeedChangeCount
accumulates
the time since the last speed change. When it is greater than
fSpeedChangeDelay
, the speed is allowed to change and will be reset to 0 if it
does.
The BoundingBox
property simply returns a new rectangle based on
the position and size of our ship. We will be adding similar properties to
other objects in our game, some of them more complex than this to account for
objects position within the "world map".
fBoardUpdateDelay
and fBoardUpdateInterval
will be used to
control how fast the screen can scroll overall (if we rely simply on calls
to Update we can potentially get inconsistent speeds).
Constructor and Draw method of class is fairly simple; we are passing
texture in constructor and SpriteBatch in Draw method.
We can now integrate it in our game. This is how we can create a player
object by passing content manager and texture path.
spaceShipObj = new SpaceShip(content, "images/plane");
Now we need to update player ship based on sensors (when user tilt the Ultrabook or use keyboard player plan should move). So we will write update code in update method of game.
spaceShipObj.Update(gameTime);
float gameElapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
if (spaceShipObj.IsSpaceShipActive)
{
float movement = 0.0f;
if (System.Math.Abs(acceloVelocity.X) > accelThreshold)
{
movement = acceloVelocity.X * acceleoSpeed;
}
spaceShipObj.X += (int)movement;
spaceShipObj.Update(gameTime);
if (spaceShipObj.X <= iPlayAreaLeft)
spaceShipObj.X = iPlayAreaLeft;
if (spaceShipObj.X + spaceShipObj.ShipWidth >= iPlayAreaRight)
spaceShipObj.X = iPlayAreaRight - spaceShipObj.ShipWidth;
spaceShipObj.SpeedChangeCount += gameElapsedTime;
spaceShipObj.VerticalChangeCount += gameElapsedTime;
TouchCollection touchCollection = TouchPanel.GetState();
foreach (TouchLocation tl in touchCollection)
{
if ((tl.State == TouchLocationState.Pressed) || (tl.State == TouchLocationState.Moved))
{
if (GasTouchBound.Intersects(new Rectangle((int)tl.Position.X, (int)tl.Position.Y, 80, 80)))
{
_touchGasFlag = true;
if (spaceShipObj.SpeedChangeCount > spaceShipObj.SpeedChangeDelay)
CheckVerticalMovementKeys(Keyboard.GetState(), GamePad.GetState(PlayerIndex.One));
}
else
UpdateShipSpeed();
if (FireTouchBound.Intersects(new Rectangle((int)tl.Position.X, (int)tl.Position.Y, 80, 80)))
{
CheckOtherKeys();
}
}
}
if (touchCollection.Count == 0)
{
UpdateShipSpeed();
}
KeyboardState keyboardState = Keyboard.GetState();
if (spaceShipObj.SpeedChangeCount > spaceShipObj.SpeedChangeDelay)
{
if (keyboardState.IsKeyDown(Keys.Up))
CheckVerticalMovementKeys(Keyboard.GetState(), GamePad.GetState(PlayerIndex.One));
else
UpdateShipSpeed();
}
if (spaceShipObj.VerticalChangeCount > spaceShipObj.VerticalChangeDelay)
{
CheckHorizontalMovementKeys(keyboardState, GamePad.GetState(PlayerIndex.One));
}
if (keyboardState.IsKeyDown(Keys.Space))
CheckOtherKeys();
}
Supporting Methods of Update Method
protected void CheckVerticalMovementKeys(KeyboardState ksKeys, GamePadState gsPad)
{
bool bResetTimer = false;
spaceShipObj.Thrusting = false;
if (spaceShipObj.ScrollRate > -iMaxHorizontalSpeed)
{
spaceShipObj.ScrollRate -= spaceShipObj.AccelerationRate;
if (spaceShipObj.ScrollRate < -iMaxHorizontalSpeed)
spaceShipObj.ScrollRate = -iMaxHorizontalSpeed;
bResetTimer = true;
}
spaceShipObj.Thrusting = true;
spaceShipObj.Facing = 1;
if (bResetTimer)
spaceShipObj.SpeedChangeCount = 0.0f;
}
protected void UpdateShipSpeed()
{
if (_touchGasFlag)
{
spaceShipObj.ScrollRate = spaceShipObj.ScrollRate + 1;
if (spaceShipObj.ScrollRate >= iMinHorizontalSpeen)
spaceShipObj.ScrollRate = iMinHorizontalSpeen;
}
}
protected void CheckHorizontalMovementKeys(KeyboardState ksKeys, GamePadState gsPad)
{
if (ksKeys.IsKeyDown(Keys.Left))
{
spaceShipObj.X -= 5;
if (spaceShipObj.X < iPlayAreaLeft)
spaceShipObj.X = iPlayAreaLeft;
}
if (ksKeys.IsKeyDown(Keys.Right))
{
spaceShipObj.X += 5;
if (spaceShipObj.X + spaceShipObj.ShipWidth > iPlayAreaRight)
spaceShipObj.X = iPlayAreaRight - spaceShipObj.ShipWidth;
}
}
The above code is for updating player spaceship when player touch, tilt
ultrabook or if press navigation arrow keys. Touch collection object will give
you all touch location user have touched. We can loop over it and quickly
identify as it interests the button on left and right or not and make appropriate
action accordingly. If TouchCollection
count is zero and if keyboard key is
pressed then we again move player. In both the cases, we will update player
speed move him forward in world map.
Sprite , Alien and AlienCompnenet
Aliens are
bad guys who will try to protest player by proceed further in stage. Alien can
fire bullets. We can create alien and boss(dragon) in same class or we can
follow OOPS by creating a class which will hold all generic properties of alien
and further we can inherit that and create two different classes – Alien
and BossAlien
. This is what I did
in game. I created a generic class sprite, which can be reused for any object.
It holds most generic properties. Below is code for sprite class.
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SkyWar
{
class Sprite
{
public Texture2D Texture { get; private set; }
public Vector2 Position;
public Vector2 Velocity;
public Vector2 Origin;
public bool Active = true;
public float Scale = 1;
public float Rotation;
public float ZLayer;
public Color Color = Color.White;
public int TotalFrames { get; private set; }
public TimeSpan AnimationInterval;
public bool OneShotAnimation;
public bool DeactivateOnAnimationOver = true;
private Rectangle[] _rects;
private int _currentFrame;
private TimeSpan _animElapsed;
public int FrameWidth { get { return _rects == null ? Texture.Width : _rects[0].Width; } }
public int FrameHeight { get { return _rects == null ? Texture.Height : _rects[0].Height; } }
public Sprite(Texture2D texture, Rectangle? firstRect = null,
int frames = 1, bool horizontal = true, int space = 0)
{
Texture = texture;
TotalFrames = frames;
if (firstRect != null)
{
_rects = new Rectangle[frames];
Rectangle first = (Rectangle)firstRect;
for (int i = 0; i < frames; i++)
_rects[i] = new Rectangle(first.Left + (horizontal ? (first.Width + space) * i : 0),
first.Top + (horizontal ? 0 : (first.Height + space) * i), first.Width, first.Height);
}
else
_rects = new Rectangle[] { new Rectangle(0, 0, texture.Width, texture.Height) };
}
public virtual void Update(GameTime gameTime)
{
if (Active)
{
if (TotalFrames > 1 && (_animElapsed += gameTime.ElapsedGameTime) > AnimationInterval)
{
if (++_currentFrame == TotalFrames)
{
_currentFrame = 0;
if (OneShotAnimation)
{
_currentFrame = TotalFrames - 1;
if (DeactivateOnAnimationOver)
Active = false;
}
}
_animElapsed -= AnimationInterval;
}
Position += Velocity;
}
}
public virtual void Draw(GameTime gameTime, SpriteBatch batch)
{
if (Active)
{
batch.Draw(Texture, Position, _rects == null ? null : (Rectangle?)_rects[_currentFrame],
Color, Rotation, Origin, Scale, SpriteEffects.None, ZLayer);
}
}
public int ActualWidth
{
get { return (int)(FrameWidth * Scale); }
}
public int ActualHeight
{
get { return (int)(FrameHeight * Scale); }
}
public Rectangle BoundingRect
{
get
{
return new Rectangle(
(int)(Position.X - Origin.X * Scale), (int)(Position.Y - Origin.Y * Scale),
(int)(_rects[0].Width * Scale), (int)(_rects[0].Height * Scale));
}
}
public virtual bool Collide(Sprite other)
{
return Active && other.Active && BoundingRect.Intersects(other.BoundingRect);
}
public virtual bool Collide(Rectangle other)
{
return Active && BoundingRect.Intersects(other);
}
}
}
This will check collision of sprite with respect of other sprite or a rectangle.
Rectangle
class will have a inbuilt method called interest which returns true of
false if object interest. The first thing we’ll do is create a custom Alien
class, that extends the Sprite
class and adds some specific alien attributes.
This will make our lives a little bit easier when we managed all those aliens.
Add a new class named Alien
and derive it from Sprite
. Please find below code –
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SkyWar
{
class Alien : Sprite
{
public readonly bool IsBoss;
public readonly AlienType Type;
GameData _gameData = null;
public int HitPoints;
SoundEffectInstance alienDead;
public Alien(AlienType type, bool isBoss)
: base(type.Texture, type.FirstFrame, type.Frames, type.IsHorizontal, type.Space)
{
IsBoss = isBoss;
Type = type;
Origin.X = type.FirstFrame.Value.Width / 2;
AnimationInterval = type.AnimationRate;
_gameData = GameData.GetGameDataInstance();
HitPoints = type.Bullets;
alienDead = _gameData.AudioLibarary.AlienDead;
}
public override void Update(GameTime gameTime)
{
if (Active && !_isDead)
{
if (Position.X < 0 || Position.X > _gameData.GameScreenWidth)
Velocity.X = -Velocity.X;
if (Position.Y > _gameData.GameScreenHeight + 40)
{
Active = false;
if (OutOfBounds != null)
OutOfBounds(this);
}
}
else if (_isDead)
{
if (Color.R > 4)
Color.R -= 4;
else
{
Active = false;
}
return;
}
base.Update(gameTime);
}
public event Action<Alien> OutOfBounds;
public event Action<Alien> Killed;
private bool _isDead;
public bool IsDead { get { return _isDead; } }
public virtual void Hit(int power = 1)
{
if ((HitPoints -= power) <= 0)
{
alienDead.Play();
_isDead = true;
Color = new Color(255, 0, 0);
if (Killed != null)
Killed(this);
}
}
}
}
The code is simple as we will just Draw and Update alien, if it’s in screen it
will be active and else we will make active false. We will update it possible
and velocity to update it. But still work in not done yet.
Now we’ll create an array of Alien
references
in the AlienComponent
class.
Which will manage set of aliens on screen. In this case, however, as every alien
may look different, we may have to create aliens dynamically as needed. Another
possible approach would be to allow the Texture of a sprite to be changed after
the sprite has been initialized. Currently, the Texture of a sprite is passed
via the constructor, and is read only since.
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SkyWar
{
class AliensComponent
{
GameplayScreen _gameScreen;
GameData _gameData;
BossAlien bossAlien = null;
Alien[] _aliens = new Alien[40];
int _currentAlienIndex;
Random _rnd = new Random();
TimeSpan _elapsedTime;
int _currentLiveAliens, _totalKilledAliens;
private TimeSpan _delayCount, _delay;
public TimeSpan Delay
{
get { return _delay; }
set
{
_delay = value;
_delayCount = TimeSpan.Zero;
}
}
internal IEnumerable<Alien> Aliens { get { return _aliens; } }
public BossAlien BossAlien { get { return bossAlien; } }
Sprite[] _bullets = new Sprite[40];
Texture2D _texture;
int _totalBullets, _currentBullet;
public AliensComponent(GameplayScreen gpScreen, ContentManager content)
{
_gameScreen = gpScreen;
_gameData = GameData.GetGameDataInstance();
_texture = content.Load<Texture2D>("images/laser1");
}
public void Update(GameTime gameTime)
{
LevelData data = _gameScreen.GetCurrentLevelData();
if (data == null)
{
_gameData.IsGameCompleted = true;
return;
}
foreach (var bullet in _bullets)
{
if (bullet != null && bullet.Active)
{
bullet.Update(gameTime);
if (bullet.Position.Y > _gameData.GameScreenHeight + 30)
{
bullet.Active = false;
_totalBullets--;
}
else if (bullet.Collide(_gameScreen.SpaceShipComponent.BoundingBox))
{
_gameScreen.SpaceShipComponent.PlayerHit();
bullet.Active = false;
_totalBullets--;
}
}
}
if (Delay > TimeSpan.Zero)
{
if ((_delayCount += gameTime.ElapsedGameTime) >= Delay)
Delay = TimeSpan.Zero;
}
if (_delay == TimeSpan.Zero && (_elapsedTime +=
gameTime.ElapsedGameTime) > data.AlienGenerationTime)
{
_elapsedTime = TimeSpan.Zero;
var alien = CreateAlien(data);
if (alien != null)
{
_currentLiveAliens++;
while (_aliens[_currentAlienIndex] != null && _aliens[_currentAlienIndex].Active)
_currentAlienIndex = (_currentAlienIndex + 1) % _aliens.Length;
_aliens[_currentAlienIndex] = alien;
}
}
foreach (var alien in _aliens)
if (alien != null && alien.Active)
{
alien.Update(gameTime);
if (!alien.IsDead && _gameScreen.SpaceShipComponent.IsSpaceShipActive &&
alien.Collide(_gameScreen.SpaceShipComponent.BoundingBox))
{
alien.Hit();
_gameScreen.SpaceShipComponent.PlayerHit();
if (_totalKilledAliens < data.TotalAliensToFinish)
_totalKilledAliens = 0;
}
if (_delay == TimeSpan.Zero && _totalBullets <
data.MaxAlienBullets && _rnd.Next(100) < data.FireChance)
CreateBullet(alien);
}
if (bossAlien != null && bossAlien.Active && (!bossAlien.IsDead))
{
bossAlien.Update(gameTime);
if (!bossAlien.IsDead && _gameScreen.SpaceShipComponent.IsSpaceShipActive &&
bossAlien.Collide(_gameScreen.SpaceShipComponent.BoundingBox))
{
bossAlien.Hit();
_gameScreen.SpaceShipComponent.PlayerHit();
}
if (_delay == TimeSpan.Zero && _totalBullets <
data.MaxAlienBullets && _rnd.Next(100) < data.FireChance)
CreateBullet(bossAlien);
}
}
private Alien CreateAlien(LevelData data)
{
if (_currentLiveAliens > data.MaxActiveAliens) return null;
if (_totalKilledAliens == data.TotalAliensToFinish)
{
_totalKilledAliens++;
bossAlien = CreateBoss(data);
bossAlien.Killed += delegate
{
_currentLiveAliens--;
_gameScreen.InitLevel(_gameScreen.Level + 1);
_totalKilledAliens = 0;
};
return null;
}
int chance = 0;
int value = _rnd.Next(100);
foreach (var sd in data.SelectionData)
{
if (value < sd.Chance + chance)
{
Alien alien = new Alien(sd.Alien, false);
alien.Position = new Vector2((float)(_rnd.NextDouble() * _gameData.GameScreenWidth), -50);
alien.Velocity = new Vector2((float)(_rnd.NextDouble() * alien.Type.MaxXSpeed * 2 -
alien.Type.MaxXSpeed), (float)(_rnd.NextDouble() * alien.Type.MaxYSpeed + 2));
alien.Scale = 0.7f;
_aliens[_currentAlienIndex] = alien;
alien.OutOfBounds += a =>
{
_currentLiveAliens--;
};
alien.Killed += a =>
{
_currentLiveAliens--;
_totalKilledAliens++;
};
return alien;
}
chance += sd.Chance;
}
return null;
}
private BossAlien CreateBoss(LevelData data)
{
BossAlien alien = new BossAlien(data.Boss);
alien.Position = new Vector2((float)(_rnd.NextDouble() * _gameData.GameScreenWidth), 10);
alien.Velocity = new Vector2((float)(_rnd.NextDouble() *
alien.Type.MaxXSpeed * 4 - alien.Type.MaxXSpeed), 0.0f);
alien.Scale = 2.3f;
return alien;
}
private void CreateBullet(Sprite alien)
{
while (_bullets[_currentBullet] != null && _bullets[_currentBullet].Active)
_currentBullet = (_currentBullet + 1) % _bullets.Length;
Sprite bullet = _bullets[_currentBullet];
if (bullet == null)
{
bullet = new Sprite(_texture, new Rectangle(0, 0, 16, 46));
bullet.Scale = 0.5f;
bullet.ZLayer = .5f;
_bullets[_currentBullet] = bullet;
}
bullet.Position = alien.Position + new Vector2(-2, alien.ActualHeight / 2);
bullet.Velocity.Y = alien.Velocity.Y > 0 ? 1.8f * alienVelocity.Y : _rnd.Next(10) + 3;
bullet.Active = true;
_totalBullets++;
}
public void Draw(GameTime gameTime, SpriteBatch spbatch)
{
foreach (var alien in _aliens)
if (alien != null && alien.Active)
alien.Draw(gameTime, spbatch);
foreach (var bullet in _bullets)
if (bullet != null)
bullet.Draw(gameTime, spbatch);
if (bossAlien != null && bossAlien.Active && (!bossAlien.IsDead))
bossAlien.Draw(gameTime, spbatch);
}
}
}
The class is very simple we have Alien[] _aliens = new Alien[40]
here in class
and we will dynamically create aliens and move it on screen. This class also
take care of _currentLiveAliens
,
_totalKilledAliens
to update score and level. There is also a property for
looping over alien array.
internal IEnumerable<Alien> Aliens { get { return _aliens; } }
Its Update method is most important one which is responsible for all action. It
waits for alien generation period and generates new alien new aliens. If alien
is killed it will update count and score. If total alien killed will reach to
level count, it will load new level.
Let’s discuss now game stage planning, score, how many aliens need to be killed
to proceed further, how many bullets alien can fire, alien generation time etc
stuff.
Game
Levels and Difficulty Level Control
I tried to make the game dynamic as much as possible. I tried controlling game
difficulty, total number of aliens need to be killed, total bullets alien can
fire, total aliens active on screen etc using xml file. So tomorrow if we feel
game is too easy or too difficult, we just need to modify xml and rest of game
will be untouched.
Below xml is used for configuration of different aliens of game. As you can it
clearly specify what is the name of alien, how many points it will provide to
player, what is the max x and y axis moving speed, then texture path, how many
frames are present in texture and how many bullets required to kill this alien.
="1.0" ="utf-8"
<AlienTypes>
<AlienType Name="alien1" Score="10" MaxXSpeed="2" MaxYSpeed="3"
Texture="Images/alien1" Frames="1" Space="1" FirstFrame="0,0,50,50" Bullets="1"
AnimationRate="500" />
<AlienType Name="alien2" Score="15" MaxXSpeed="3" MaxYSpeed="4"
Texture="Images/alien2" Frames="1" Space="1" FirstFrame="0,0,42,67" Bullets="1"
AnimationRate="400" />
<AlienType Name="alien3" Score="20" MaxXSpeed="4" MaxYSpeed="5"
Texture="Images/alien3" Frames="1" Space="1" FirstFrame="0,0,41,64" Bullets="1"
AnimationRate="400" />
<AlienType Name="alien4" Score="25" MaxXSpeed="4" MaxYSpeed="4"
Texture="Images/alien4" Frames="1" Space="1" FirstFrame="0,0,40,52" Bullets="1"
AnimationRate="400" />
</AlienTypes>
Same way to control level we have level xml. As you can see it clearly
specify how many levels are there, in which level how many maximum active
aliens should be present on screen, how many aliens player need to kill to
proceed to next level, which kind of dragon or boss enemy will come in end of
stage, what is new alien generation time, how many bullets player need to hit
to boos in order to kill him.
="1.0" ="utf-8"
<Levels>
<Level Number="1" MaxActiveAliens="12" TotalAliensToFinish="10"
Boss="boss1" AlienGenerationTime="1200"
ChangeDirChance="2" FireChance="7"
MaxAlienBullets="7">
<AlienTypes>
<AlienType Name="alien1" Chance="25" />
<AlienType Name="alien2" Chance="20" />
<AlienType Name="alien3" Chance="20" />
<AlienType Name="alien4" Chance="15" />
<AlienType Name="alien5" Chance="25" />
</AlienTypes>
</Level>
<Level Number="2" MaxActiveAliens="16" TotalAliensToFinish="14"
Boss="boss2" AlienGenerationTime="1000"
ChangeDirChance="2" FireChance="14"
MaxAlienBullets="10">
<AlienTypes>
<AlienType Name="alien1" Chance="20" />
<AlienType Name="alien2" Chance="20" />
<AlienType Name="alien3" Chance="20" />
<AlienType Name="alien4" Chance="15" />
<AlienType Name="alien5" Chance="25" />
<AlienType Name="alien6" Chance="25" />
</AlienTypes>
</Level>
</Levels>
I have uploaded complete source code but will not be able to upload textures
as its size is way above then code project limit. When game studio convert
textures into .xnb file, its size increase a lot.
You can find by old App Innovation article here.
Hope this will give you complete guideline of game. Enjoy Gaming.
Point of Interest
I won first round and after receiving an Ultrabook, I created an application on time and uploaded to the Microsoft app store. But it was stuck in validation stage and
is still pending (as I fell sick so could not proceed further in completion).
I learned a lot from Intel AppUp competition. I
tried something new first time (MonoGame).
References
Before I finish this article, I would like to add some references to article
which helped me learning MonoGame and XNA.
Books : XNA 3.0 Game Programming from Novice to Professional, Xna Game
Programming Recepies
Links :