Creating Games with Silverlight: A Simple Shooter
This article will take you through some of the steps to creating the basics for writing games with Silverlight. This is a simple shooter style game that contains some of the building blocks for games that require abilities such as vectors, collision detection, a game loop, movement, and keyboard input. Please refer to the full source code for this project for any areas of the code that are not covered in detail in this article.
Inspiration
There are a number of examples and tutorials out there to help get started creating games. You will find the basics of this game to be very similar to a Visual C# Coding4Fun article here. This article will show you how to create a shooter style game in Silverlight. My first Silverlight game experience was playing with a Asteroids style game by Bill Reiss. My usage of vectors and how to do a game loop came from great examples like his and others. There are a number of sites and blogs where you can get good info on game development with Silverlight. Here are just a few.
Basic Layout
To begin, Open Visual Studio and create a new Silverlight Application in either C# or VB.net. First, we will start with the layout of the screen. First, I added a canvas to the grid inside the Page.xaml that is created for you. I set it's background to black, and named it 'gameRoot'.
<UserControl x:Class="SimpleShooter.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<Grid x:Name="LayoutRoot">
<Canvas x:Name="gameRoot" Width="500" Height="400" Background="Black" >
</Canvas>
</Grid>
</UserControl>
Next we will start adding controls to our project for things like game entities, and information displays. In this example. To complete layout of the game, add user controls to the progect called: Info, LivesRemaining, Score, and WaveInfo. In those controls, To begin I added basic TextBlocks to display information on the state of the game. Please note there are better ways to arrange things in a user control, beyond what I show here. I am keeping to a simple canvas and direct positioning to introduce the concept of Canvas.Top and Canvas.Left. I have now placed these controls on Page.xaml and assigned an x:Name to each. At this point, the controls have only a TextBlock in them.
<UserControl x:Class="SimpleShooter.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:SimpleShooter="clr-namespace:SimpleShooter"
Width="500" Height="400">
<Grid x:Name="LayoutRoot">
<Canvas x:Name="gameRoot" Width="500" Height="400" Background="Black">
<SimpleShooter:RemainingLives x:Name="ctlLives" Canvas.Top="380" Canvas.Left="10" />
<SimpleShooter:Score x:Name="ctlScore" Canvas.Top="10" Canvas.Left="10" />
<SimpleShooter:WaveInfo x:Name="ctlWaveInfo" Canvas.Left="440" Canvas.Top="10" />
<SimpleShooter:Info x:Name="ctlInfo" Canvas.Top="10"/>
</Canvas>
</Grid>
</UserControl>
To finish our layout lets add a star field. To do this, we will write function to generate random numbers, and another that will randomly distribute ellipses on our base canvas. We will need to inherit from System.Security.Cryptography for this:
C#
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
GenerateStarField(350);
}
void GenerateStarField(int numberOfStars)
{
for (int i = 0; i < numberOfStars; i++)
{
Ellipse star = new Ellipse();
double size = GetRandInt(10, 800) * .01;
star.Width = size;
star.Height = size;
star.Opacity = GetRandInt(1, 5) * .1;
star.Fill = new SolidColorBrush(Colors.White);
int x = GetRandInt(0, (int)Math.Round(gameRoot.Height, 0));
int y = GetRandInt(0, (int)Math.Round(gameRoot.Width, 0));
star.SetValue(Canvas.TopProperty, (double)x);
star.SetValue(Canvas.LeftProperty, (double)y);
gameRoot.Children.Add(star);
}
}
public int GetRandInt(int min, int max)
{
Byte[] rndBytes = new Byte[10];
RNGCryptoServiceProvider rndC = new RNGCryptoServiceProvider();
rndC.GetBytes(rndBytes);
int seed = BitConverter.ToInt32(rndBytes, 0);
Random rand = new Random(seed);
return rand.Next(min, max);
}
}
VB
Partial Public Class Page
Inherits UserControl
Public Sub New()
InitializeComponent()
GenerateStarField(350)
End Sub
Private Sub GenerateStarField(ByVal numberOfStars As Integer)
For i As Integer = 0 To numberOfStars - 1
Dim star As New Ellipse()
Dim size As Double = GetRandInt(10, 800) * 0.01
star.Width = size
star.Height = size
star.Opacity = GetRandInt(1, 5) * 0.1
star.Fill = New SolidColorBrush(Colors.White)
Dim x As Integer = GetRandInt(0, CInt(Math.Round(gameRoot.Height, 0)))
Dim y As Integer = GetRandInt(0, CInt(Math.Round(gameRoot.Width, 0)))
star.SetValue(Canvas.TopProperty, CDbl(x))
star.SetValue(Canvas.LeftProperty, CDbl(y))
gameRoot.Children.Add(star)
Next
End Sub
Public Function GetRandInt(ByVal min As Integer, ByVal max As Integer) As Integer
Dim rndBytes As [Byte]() = New [Byte](9) {}
Dim rndC As New RNGCryptoServiceProvider()
rndC.GetBytes(rndBytes)
Dim seed As Integer = BitConverter.ToInt32(rndBytes, 0)
Dim rand As New Random(seed)
Return rand.[Next](min, max)
End Function
End Class
We now have a function called GenerateStarField. Note that it is adding each ellipse to the 'Children' of our base gameRoot canvas, and how the Top and Left properties determine the positions of those ellipses. Now we have a basic layout of our game and a background.
Sprites and Vectors
I am not going to go in to great detail on sprites or vectors. There are a number of great resources (see other articles linked to in this post), as well as another good Coding4Fun article on this topic. We are however, going to add a class to represent a sprite and a vector to our code:
C#
public abstract class Sprite
{
public double Width { get; set; }
public double Height { get; set; }
public Vector Velocity { get; set; }
public Canvas SpriteCanvas { get; set; }
private Point _position;
public Point Position
{
get
{
return _position;
}
set
{
_position = value;
SpriteCanvas.SetValue(Canvas.TopProperty, _position.Y - (Height / 2));
SpriteCanvas.SetValue(Canvas.LeftProperty, _position.X - (Width / 2));
}
}
public Sprite(Double width, Double height, Point position)
{
Width = width;
Height = height;
SpriteCanvas = RenderSpriteCanvas();
SpriteCanvas.Width = width;
SpriteCanvas.Height = height;
//NOTE: because the setter for Position uses both Height and Width, it is important this comes after they are set.
Position = position;
}
public abstract Canvas RenderSpriteCanvas();
public Canvas LoadSpriteCanvas(string xamlPath)
{
System.IO.Stream s = this.GetType().Assembly.GetManifestResourceStream(xamlPath);
return (Canvas)XamlReader.Load(new System.IO.StreamReader(s).ReadToEnd());
}
public virtual void Update(TimeSpan elapsedTime)
{
Position = (Position + Velocity * elapsedTime.TotalSeconds);
}
}
VB
Public MustInherit Class Sprite
Private _Width As Double
Public Property Width() As Double
Get
Return _Width
End Get
Set(ByVal value As Double)
_Width = value
End Set
End Property
Private _Height As Double
Public Property Height() As Double
Get
Return _Height
End Get
Set(ByVal value As Double)
_Height = value
End Set
End Property
Private _Velocity As Vector
Public Property Velocity() As Vector
Get
Return _Velocity
End Get
Set(ByVal value As Vector)
_Velocity = value
End Set
End Property
Private _SpriteCanvas As Canvas
Public Property SpriteCanvas() As Canvas
Get
Return _SpriteCanvas
End Get
Set(ByVal value As Canvas)
_SpriteCanvas = value
End Set
End Property
Private _position As Point
Public Property Position() As Point
Get
Return _position
End Get
Set(ByVal value As Point)
_position = value
SpriteCanvas.SetValue(Canvas.TopProperty, _position.Y - (Height / 2))
SpriteCanvas.SetValue(Canvas.LeftProperty, _position.X - (Width / 2))
End Set
End Property
Public Sub New(ByVal initialWidth As [Double], ByVal initialHeight As [Double], ByVal initialPosition As Point)
Width = initialWidth
Height = initialHeight
SpriteCanvas = RenderSpriteCanvas()
SpriteCanvas.Width = Width
SpriteCanvas.Height = Height
'NOTE: because the setter for Position uses both Height and Width, it is important this comes after they are set.
Position = initialPosition
End Sub
Public MustOverride Function RenderSpriteCanvas() As Canvas
Public Function LoadSpriteCanvas(ByVal xamlPath As String) As Canvas
Dim s As System.IO.Stream = Me.[GetType]().Assembly.GetManifestResourceStream(xamlPath)
Return DirectCast(XamlReader.Load(New System.IO.StreamReader(s).ReadToEnd()), Canvas)
End Function
Public Overridable Sub Update(ByVal elapsedTime As TimeSpan)
Position = (Position + Velocity * elapsedTime.TotalSeconds)
End Sub
End Class
This Sprite class will give us the basis for entities in the game such as a ship, aliens, and projectiles. For all these items, we need to know the location of the item, the site, and what it looks like. We will take advantage of Point to track our position, and a property of type Canvas to display the XAML for each of these items. The constructor sets these initial parameters, and calls a RenderSpriteCanvas method to be implemented by each class that inherits from it. This method allows the inheriting class to set the contents of the canvas and therefore as control of the way the sprite looks.
Next we have our vector class that will help us control movement of our sprite. Again, I will not go into detail on vectors as this code is borrowed and adapted from many other readily available sources:
C#
public struct Vector
{
public double X;
public double Y;
public Vector(double x, double y)
{
X = x;
Y = y;
}
public double Length
{
get
{
return Math.Sqrt(LengthSquared);
}
}
public double LengthSquared
{
get
{
return X * X + Y * Y;
}
}
public void Normalize()
{
double length = Length;
X /= length;
Y /= length;
}
public static Vector operator -(Vector vector)
{
return new Vector(-vector.X, -vector.Y);
}
public static Vector operator *(Vector vector, double scalar)
{
return new Vector(scalar * vector.X, scalar * vector.Y);
}
public static Point operator +(Point point, Vector vector)
{
return new Point(point.X + vector.X, point.Y + vector.Y);
}
static public Vector CreateVectorFromAngle(double angleInDegrees, double length)
{
double x = Math.Sin(DegreesToRadians(180 - angleInDegrees)) * length;
double y = Math.Cos(DegreesToRadians(180 - angleInDegrees)) * length;
return new Vector(x, y);
}
static public double DegreesToRadians(double degrees)
{
double radians = ((degrees / 360) * 2 * Math.PI);
return radians;
}
}
VB
Public Structure Vector
Public X As Double
Public Y As Double
Public Sub New(ByVal x__1 As Double, ByVal y__2 As Double)
X = x__1
Y = y__2
End Sub
Public ReadOnly Property Length() As Double
Get
Return Math.Sqrt(LengthSquared)
End Get
End Property
Public ReadOnly Property LengthSquared() As Double
Get
Return X * X + Y * Y
End Get
End Property
Public Sub Normalize()
Dim length__1 As Double = Length
X /= length__1
Y /= length__1
End Sub
Public Shared Operator -(ByVal vector As Vector) As Vector
Return New Vector(-vector.X, -vector.Y)
End Operator
Public Shared Operator *(ByVal vector As Vector, ByVal scalar As Double) As Vector
Return New Vector(scalar * vector.X, scalar * vector.Y)
End Operator
Public Shared Operator +(ByVal point As Point, ByVal vector As Vector) As Point
Return New Point(point.X + vector.X, point.Y + vector.Y)
End Operator
Public Shared Function CreateVectorFromAngle(ByVal angleInDegrees As Double, ByVal length As Double) As Vector
Dim x As Double = Math.Sin(DegreesToRadians(180 - angleInDegrees)) * length
Dim y As Double = Math.Cos(DegreesToRadians(180 - angleInDegrees)) * length
Return New Vector(x, y)
End Function
Public Shared Function DegreesToRadians(ByVal degrees As Double) As Double
Dim radians As Double = ((degrees / 360) * 2 * Math.PI)
Return radians
End Function
End Structure
Now we can implement Sprite with a new Ship class. Add a class called Ship, and a file called Ship.xaml to your project. Be sure to set the properties of Ship.xaml to 'Embedded Resource'. We now need to inherit from our Sprite class:
C#
public class Ship : Sprite
{
public Ship(double width, double height, Point firstPosition)
: base(width, height, firstPosition)
{
}
public override Canvas RenderSpriteCanvas()
{
return LoadSpriteCanvas("SimpleShooter.Sprites.Ship.xaml");
}
}
VB
Public Class Ship
Inherits Sprite
Public Sub New(ByVal width As Double, ByVal height As Double, ByVal firstPosition As Point)
MyBase.New(width, height, firstPosition)
End Sub
Public Overloads Overrides Function RenderSpriteCanvas() As Canvas
Return LoadSpriteCanvas("SimpleShooter.Ship.xaml")
End Function
End Class
When Ship is instantiated, it calls the constructor of it's base, Sprite. It also implements the RenderSpriteCanvas method and specifies the XAML (a simple white square) to load into the sprite's canvas. Now we are ready to add a sprite to our main page. In this simple game, we will have only one ship (the others will be aliens), so lets add a property to our page, and a function that will instantiate our ship:
<Canvas x:Name="LayoutRoot" Width="30" Height="30"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
<Rectangle Height="30" Width="30" Fill="White" />
</Canvas>
C#
void InitializeGame()
{
PlayerShip = new Ship(10, 10, new Point(100, 300));
gameRoot.Children.Add(PlayerShip.SpriteCanvas);
}
VB
Private Sub InitializeGame()
PlayerShip = New Ship(10, 10, New Point(100, 300))
gameRoot.Children.Add(PlayerShip.SpriteCanvas)
End Sub
We can now call this method from our page constructor and when we run the project, we get a white square (our ship) at the bottom left of our Page. Taking a closer look, we set our page size to 500 x 400, and the InitializeGame method places our Ship at a point 100 pixels from the left of the gameRoot canvas, and 300pixels from the top.
Keyboard Input and the Game Loop
Now we are ready to make some things move. To start, we need choose keys on the keyboard to allow movement, and act on those events. We then need to capture those key presses and act on them if they are relevant to our game loop. Once again, may examples of key handlers and game loops are readily available, so I won't go into detail. The keyboard handler captures all key up and down events. We can therefore ask our instance of the handler if a key is pressed at any given time. The game loop is just that, a constant loop. It consists of a storyboard that has the start called on it and it immediately ends. The class raises an event, and starts the storyboard again. Subscribers to Update event are provided a value that reports the number of milliseconds since the last update. That value can be applied to vectors to apply smooth movement to sprites. To take advantage of these classes, we need to add an instance of both a KeyHandler and GameLoop to our Page. To do this, we update the InitializeGame and Page constructor, and add a handler for the GameLoop:
C#
public Page()
{
InitializeComponent();
keyHandler = new KeyHandler(this);
GenerateStarField(350);
InitializeGame();
}
void InitializeGame()
{
gameLoop = new GameLoop(this);
gameLoop.Update += new GameLoop.UpdateHandler(gameLoop_Update);
PlayerShip = new Ship(10, 10, new Point(100, 360));
gameRoot.Children.Add(PlayerShip.SpriteCanvas);
gameLoop.Start();
}
void gameLoop_Update(TimeSpan elapsed)
{
//clear the current vector so the sprite is not moving unless a keys is pressed
PlayerShip.Velocity = new Vector(0, 0);
if (keyHandler.IsKeyPressed(Key.Left))
{
PlayerShip.Velocity = Vector.CreateVectorFromAngle(270, 125);
}
if (keyHandler.IsKeyPressed(Key.Right))
{
PlayerShip.Velocity = Vector.CreateVectorFromAngle(90, 125);
}
PlayerShip.Update(elapsed);
}
VB
Public Sub New()
InitializeComponent()
keyHandler = New KeyHandler(Me)
GenerateStarField(350)
InitializeGame()
End Sub
Private Sub InitializeGame()
gameLoop = New GameLoop(Me)
AddHandler gameLoop.Update, AddressOf gameLoop_Update
PlayerShip = New Ship(10, 10, New Point(100, 360))
gameRoot.Children.Add(PlayerShip.SpriteCanvas)
gameLoop.Start()
End Sub
Private Sub gameLoop_Update(ByVal elapsed As TimeSpan)
'clear the current vector so the sprite is not moving unless a keys is pressed
PlayerShip.Velocity = New Vector(0, 0)
If keyHandler.IsKeyPressed(Key.Left) Then
PlayerShip.Velocity = Vector.CreateVectorFromAngle(270, 125)
End If
If keyHandler.IsKeyPressed(Key.Right) Then
PlayerShip.Velocity = Vector.CreateVectorFromAngle(90, 125)
End If
PlayerShip.Update(elapsed)
End Sub
Now we have a functioning game loop. Launch the app, click on the Silverlight control to give it focus, and you can now use the arrow keys to move our ship right and left. One problem however is that you are able to take the ship completely off the screen. To prevent this, we can add MinX and MaxX properties to our ship class and override the Update method it inherits from Sprite. Be sure to also add those min and max values after the Ship is instantiated in InitializeGame of our Page:
C#
public class Ship : Sprite
{
public double MaxX { get; set; }
public double MinX { get; set; }
public Ship(double width, double height, Point firstPosition)
: base(width, height, firstPosition)
{
}
public override Canvas RenderSpriteCanvas()
{
return LoadSpriteCanvas("SimpleShooter.Sprites.Ship.xaml");
}
public override void Update(System.TimeSpan elapsedTime)
{
//verify that this is a position we can move to
if (Position.X > MaxX)
{
Position = new Point(MaxX, Position.Y);
Velocity = new Vector(0, 0);
}
if (Position.X < MinX)
{
Position = new Point(MinX, Position.Y);
Velocity = new Vector(0, 0);
}
base.Update(elapsedTime);
}
}
VB
Public Class Ship
Inherits Sprite
Private _MaxX As Double
Public Property MaxX() As Double
Get
Return _MaxX
End Get
Set(ByVal value As Double)
_MaxX = value
End Set
End Property
Private _MinX As Double
Public Property MinX() As Double
Get
Return _MinX
End Get
Set(ByVal value As Double)
_MinX = value
End Set
End Property
Public Sub New(ByVal width As Double, ByVal height As Double, ByVal firstPosition As Point)
MyBase.New(width, height, firstPosition)
End Sub
Public Overloads Overrides Function RenderSpriteCanvas() As Canvas
Return LoadSpriteCanvas("SimpleShooter.Ship.xaml")
End Function
Public Overloads Overrides Sub Update(ByVal elapsedTime As System.TimeSpan)
'verify that this is a position we can move to
If Position.X > MaxX Then
Position = New Point(MaxX, Position.Y)
Velocity = New Vector(0, 0)
End If
If Position.X < MinX Then
Position = New Point(MinX, Position.Y)
Velocity = New Vector(0, 0)
End If
MyBase.Update(elapsedTime)
End Sub
End Class
Prepare to Fire!
We now have all the basic plumbing to add additional sprites such as aliens or projectiles. To begin, we will add additional classes inheriting from Sprite: Alien, Missle, and Bomb, along with Alien.xaml, Missle.xaml, and Bomb.xaml (these new .xaml files need to be set as Embedded Resources). These xaml files are just like our Ship.xaml with different sizes and colors. Make the Aliens and their bombs red, and shrink the bomb and missile height and width to 5. The xaml is very similar, but the classes themselves are going to have a few differing capabilities. Bomb and Missile are very similar. They only need to load their corresponding xaml. Here is our Bomb class for example:
C#
public class Bomb : Sprite
{
public double MaxX { get; set; }
public double MinX { get; set; }
public Bomb(double width, double height, Point firstPosition)
: base(width, height, firstPosition)
{
}
public override Canvas RenderSpriteCanvas()
{
return LoadSpriteCanvas("SimpleShooter.Sprites.Bomb.xaml");
}
public override void Update(System.TimeSpan elapsedTime)
{
base.Update(elapsedTime);
}
}
VB
Public Class Bomb
Inherits Sprite
Private _MaxX As Double
Public Property MaxX() As Double
Get
Return _MaxX
End Get
Set(ByVal value As Double)
_MaxX = value
End Set
End Property
Private _MinX As Double
Public Property MinX() As Double
Get
Return _MinX
End Get
Set(ByVal value As Double)
_MinX = value
End Set
End Property
Public Sub New(ByVal width As Double, ByVal height As Double, ByVal firstPosition As Point)
MyBase.New(width, height, firstPosition)
End Sub
Public Overloads Overrides Function RenderSpriteCanvas() As Canvas
Return LoadSpriteCanvas("SimpleShooter.Sprites.Bomb.xaml")
End Function
Public Overloads Overrides Sub Update(ByVal elapsedTime As System.TimeSpan)
MyBase.Update(elapsedTime)
End Sub
End Class
We now need to add an Alien class similar to our Ship class. These will both need a bit more capability. For one, we are going to have them both fire at each other. In the case of the Alien, it will be firing down, and it's monition of choice will be a bomb:
C#
public class Alien : Sprite
{
public double fireRateMilliseconds = 2000;
public double fireVelocity = 250;
public double wayPointMin;
public double wayPointMax;
public double speed = 100;
public bool spawnWait;
public DateTime spawnComplete;
public double MaxX { get; set; }
public double MinX { get; set; }
public Alien(double width, double height, Point firstPosition)
: base(width, height, firstPosition)
{
}
public void CheckDirection()
{
if (Position.X > wayPointMax)
{
Velocity = Vector.CreateVectorFromAngle(270, speed);
}
if (Position.X < wayPointMin)
{
Velocity = Vector.CreateVectorFromAngle(90, speed);
}
}
public override Canvas RenderSpriteCanvas()
{
return LoadSpriteCanvas("SimpleShooter.Sprites.Alien.xaml");
}
public override void Update(TimeSpan elapsedTime)
{
CheckDirection();
base.Update(elapsedTime);
}
public Bomb Fire()
{
Bomb bomb = new Bomb(5, 5, Position);
bomb.Velocity = Vector.CreateVectorFromAngle(180, fireVelocity);
return bomb;
}
}
VB
Public Class Alien
Inherits Sprite
Public fireRateMilliseconds As Double = 2000
Public fireVelocity As Double = 250
Public wayPointMin As Double
Public wayPointMax As Double
Public speed As Double = 100
Public spawnWait As Boolean
Public spawnComplete As DateTime
Private _MaxX As Double
Public Property MaxX() As Double
Get
Return _MaxX
End Get
Set(ByVal value As Double)
_MaxX = value
End Set
End Property
Private _MinX As Double
Public Property MinX() As Double
Get
Return _MinX
End Get
Set(ByVal value As Double)
_MinX = value
End Set
End Property
Public Sub New(ByVal width As Double, ByVal height As Double, ByVal firstPosition As Point)
MyBase.New(width, height, firstPosition)
End Sub
Public Sub CheckDirection()
If Position.X > wayPointMax Then
Velocity = Vector.CreateVectorFromAngle(270, speed)
End If
If Position.X < wayPointMin Then
Velocity = Vector.CreateVectorFromAngle(90, speed)
End If
End Sub
Public Overloads Overrides Function RenderSpriteCanvas() As Canvas
Return LoadSpriteCanvas("SimpleShooter.Alien.xaml")
End Function
Public Overloads Overrides Sub Update(ByVal elapsedTime As TimeSpan)
CheckDirection()
MyBase.Update(elapsedTime)
End Sub
Public Function Fire() As Bomb
Dim bomb As New Bomb(5, 5, Position)
bomb.Velocity = Vector.CreateVectorFromAngle(180, fireVelocity)
Return bomb
End Function
End Class
Now that we are going to add munitions to the game, we need to be able to know when these munitions collide with other sprites. To do this, we are going to add a collision detection method to our sprite class. As a form of simple collision detection, we will give each sprite a CollisionRadius from its center point. To keep it simple we will make the radius one half the width of our sprites. A vector created from these two points can then detect if the sum of those two radius is greater than the length of our vector. If it is, they have collided:
C#
public static bool Collides(Sprite s1, Sprite s2)
{
Vector v = new Vector((s1.Position.X) - (s2.Position.X), (s1.Position.Y) - (s2.Position.Y));
if (s1.CollisionRadius + s2.CollisionRadius > v.Length)
{
return true;
}
else
{
return false;
}
}
VB
Public Shared Function Collides(ByVal s1 As Sprite, ByVal s2 As Sprite) As Boolean
Dim v As New Vector((s1.Position.X) - (s2.Position.X), (s1.Position.Y) - (s2.Position.Y))
If s1.CollisionRadius + s2.CollisionRadius > v.Length Then
Return True
Else
Return False
End If
End Function
We are now ready to extend our game loop of our Page to do more than track the movement of our Ship. We begin by taking action on more than just the arrow keys. We will wire up the space bar for shooting. Lets add an enumeration to track the state of our game. We will also add a supporting class that will handle the firing of munitions and adding those to our game. We will keep this simple for now, but it is an obvious place to being extending this code to take advantage of things like partial classes that could contain functionality like our 'Fire' method:
C#
public enum GameState
{
Ready = 0,
Running = 1,
Paused = 2,
BetweenWaves = 3,
GameOver = 4
}
if (keyHandler.IsKeyPressed(Key.Space))
{
switch (status)
{
case GameState.Ready:
break;
case GameState.Running:
EntityFired(PlayerShip);
break;
case GameState.Paused:
break;
case GameState.BetweenWaves:
status = GameState.Running;
ctlInfo.GameInfo = "";
StartWave();
break;
case GameState.GameOver:
break;
default:
break;
}
}
void EntityFired(Sprite shooter)
{
Debug.WriteLine(shooter);
switch (shooter.ToString())
{
case "SimpleShooter.Ship":
if (missles.Count == 0)
{
Missle missle = ((Ship)shooter).Fire();
missles.Add(missle);
gameRoot.Children.Add(missle.SpriteCanvas);
}
break;
case "SimpleShooter.Alien":
Bomb bomb = ((Alien)shooter).Fire();
bombs.Add(bomb);
gameRoot.Children.Add(bomb.SpriteCanvas);
break;
default:
break;
}
}
VB
Public Enum GameState
Ready = 0
Running = 1
Paused = 2
BetweenWaves = 3
GameOver = 4
End Enum
If keyHandler.IsKeyPressed(Key.Space) Then
Select Case status
Case GameState.Ready
Exit Select
Case GameState.Running
EntityFired(PlayerShip)
Exit Select
Case GameState.Paused
Exit Select
Case GameState.BetweenWaves
status = GameState.Running
ctlInfo.GameInfo = ""
StartWave()
Exit Select
Case GameState.GameOver
Exit Select
Case Else
Exit Select
End Select
End If
Private Sub EntityFired(ByVal shooter As Sprite)
Debug.WriteLine(shooter)
Select Case shooter.ToString()
Case "SimpleShooter.Ship"
If missles.Count = 0 Then
Dim missle As Missle = DirectCast(shooter, Ship).Fire()
missles.Add(missle)
gameRoot.Children.Add(missle.SpriteCanvas)
End If
Exit Select
Case "SimpleShooter.Alien"
Dim bomb As Bomb = DirectCast(shooter, Alien).Fire()
bombs.Add(bomb)
gameRoot.Children.Add(bomb.SpriteCanvas)
Exit Select
Case Else
Exit Select
End Select
End Sub
To make it easier to track the state of our game, let's add public properties to our controls and let them be the keepers of those game states. For instance, give the ReamainingLives control a Lives property, and have the setter of that property also update the TextBlock on the control to show the user how many lives are left. Perform a similar task for the other three controls:
C#
public partial class RemainingLives : UserControl
{
private int _lives;
public int Lives
{
get { return _lives; }
set
{
_lives = value;
string livesString = string.Empty;
for (int i = 0; i < _lives - 1; i++)
{
livesString = string.Format("{0}{1}", livesString, "A");
}
txtRemainingLives.Text = livesString;
}
}
public RemainingLives()
{
InitializeComponent();
}
}
VB
Partial Public Class RemainingLives
Inherits UserControl
Private _lives As Integer
Public Property Lives() As Integer
Get
Return _lives
End Get
Set(ByVal value As Integer)
_lives = value
Dim livesString As String = String.Empty
For i As Integer = 0 To _lives - 2
livesString = String.Format("{0}{1}", livesString, "A")
Next
txtRemainingLives.Text = livesString
End Set
End Property
Public Sub New()
InitializeComponent()
End Sub
End Class
Now we have a number of other things to add to our page class. We are going to have lots of entities to deal with in our game loop. We will have a ship, x number of Aliens, x number of Bombs, and at some points we will have our Missiles, though we restricted that to one at a time in our EntityFired method. We need to, in each game loop check if any missile hits any alien or leaves the game map, if any bomb hits our ship or leaves the game map, and if it is time for a Alien to fire at the ship. Our Page already has a property for our Ship, but the Aliens, Bombs, Missiles need to be collections. When we begin to loop through our Bombs for instance, we will want to remove a bomb if it strikes a Ship or leaves the game map. Since we are going to be iterating these collections, we cannot subtract from those collections as we are looping through them. There are a number of approaches for this, but to keep it simple, we will add a corresponding collection for each of our Bomb, Alien, and Missile collections to track the ones that need to be removed and removed from our game canvas. With this approach, our game loop can add to the "remove these" collection, and then take action on them when we leave the main loop for the parent collection:
C#
List<Alien> aliens;
List<Alien> aliensRemove;
List<Alien> alienShooters;
List<Bomb> bombs;
List<Bomb> bombsRemove;
List<Missle> missles;
List<Missle> misslesRemove;
VB
Private aliens As List(Of Alien)
Private aliensRemove As List(Of Alien)
Private alienShooters As List(Of Alien)
Private bombs As List(Of Bomb)
Private bombsRemove As List(Of Bomb)
Private missles As List(Of Missle)
Private misslesRemove As List(Of Missle)
We also want to add a class to our page that can track the waves of aliens we are going to send one at a time. In addition, we can add a collection to hold these waves. Each wave will track the number of aliens that spawn at once, the total number of aliens faced in the wave, how many aliens get to drop bombs, and how frequently they can fire.
C#
public class WaveData
{
public WaveData(int count, double fireRate, int atOnce, int fireatonce)
{
EnemyCount = count;
fireRateMilliseconds = fireRate;
enemiesAtOnce = atOnce;
fireAtOnce = fireatonce;
waveEmpty = false;
}
public int EnemyCount { get; set; }
public double fireRateMilliseconds { get; set; }
public int enemiesAtOnce { get; set; }
public int fireAtOnce { get; set; }
public bool waveEmpty { get; set; }
}
VB
Public Class WaveData
Public Sub New(ByVal count As Integer, ByVal fireRate As Double, ByVal atOnce As Integer, ByVal fireatonce__1 As Integer)
EnemyCount = count
fireRateMilliseconds = fireRate
enemiesAtOnce = atOnce
fireAtOnce = fireatonce__1
waveEmpty = False
End Sub
Private _EnemyCount As Integer
Public Property EnemyCount() As Integer
Get
Return _EnemyCount
End Get
Set(ByVal value As Integer)
_EnemyCount = value
End Set
End Property
Private _fireRateMilliseconds As Double
Public Property fireRateMilliseconds() As Double
Get
Return _fireRateMilliseconds
End Get
Set(ByVal value As Double)
_fireRateMilliseconds = value
End Set
End Property
Private _enemiesAtOnce As Integer
Public Property enemiesAtOnce() As Integer
Get
Return _enemiesAtOnce
End Get
Set(ByVal value As Integer)
_enemiesAtOnce = value
End Set
End Property
Private _fireAtOnce As Integer
Public Property fireAtOnce() As Integer
Get
Return _fireAtOnce
End Get
Set(ByVal value As Integer)
_fireAtOnce = value
End Set
End Property
Private _waveEmpty As Boolean
Public Property waveEmpty() As Boolean
Get
Return _waveEmpty
End Get
Set(ByVal value As Boolean)
_waveEmpty = value
End Set
End Property
End Class
We will need to setup an initialization method for our game, to setup all our collections for sprites, and add progressively difficult waves to our game. The meat of it all now lies in our game loop. For each collection of sprites we call methods that will iterate over its contents and make decisions based on collisions, locations of the sprites. After a loop on each main collection, we can use the 'remove us' collection to clear items from the main collection, remove their canvas from our game canvas, and delete them from the main collection. Finally, we check to see if enough time has elapsed to let the aliens drop another bomb:
C#
void gameLoop_Update(TimeSpan elapsed)
{
//clear the current Vector so the sprite is not moving unless a keys is pressed
PlayerShip.Velocity = new Vector(0, 0);
if (keyHandler.IsKeyPressed(Key.Left))
{
PlayerShip.Velocity = Vector.CreateVectorFromAngle(270, 125);
}
if (keyHandler.IsKeyPressed(Key.Right))
{
PlayerShip.Velocity = Vector.CreateVectorFromAngle(90, 125);
}
if (keyHandler.IsKeyPressed(Key.Space))
{
switch (status)
{
case GameState.Ready:
break;
case GameState.Running:
EntityFired(PlayerShip);
break;
case GameState.Paused:
break;
case GameState.BetweenWaves:
status = GameState.Running;
ctlInfo.GameInfo = "";
StartWave();
break;
case GameState.GameOver:
break;
default:
break;
}
}
PlayerShip.Update(elapsed);
BombLoop(elapsed);
MissleLoop(elapsed);
AlienLoop(elapsed);
foreach (Alien alien in aliensRemove)
{
aliens.Remove(alien);
gameRoot.Children.Remove(alien.SpriteCanvas);
AlienShot(alien);
}
aliensRemove.Clear();
foreach (Missle missle in misslesRemove)
{
missles.Remove(missle);
gameRoot.Children.Remove(missle.SpriteCanvas);
}
misslesRemove.Clear();
if (nextShot <= DateTime.Now)
{
nextShot = DateTime.Now.AddMilliseconds(enemyShootMilliseonds).AddMilliseconds(elapsed.Milliseconds * -1);
shotsThisPass = shotsAtOnce;
if (shotsThisPass > aliens.Count)
{
shotsThisPass = aliens.Count;
}
if (aliens.Count > 0)
{
foreach (Alien alien in aliens)
{
alienShooters.Add(alien);
}
}
while (alienShooters.Count > shotsThisPass)
{
alienShooters.RemoveAt(GetRandInt(0, alienShooters.Count - 1));
}
foreach (Alien alien in alienShooters)
{
EntityFired(alien);
}
alienShooters.Clear();
}
}
VB
Private Sub gameLoop_Update(ByVal elapsed As TimeSpan)
'clear the current Vector so the sprite is not moving unless a keys is pressed
PlayerShip.Velocity = New Vector(0, 0)
If keyHandler.IsKeyPressed(Key.Left) Then
PlayerShip.Velocity = Vector.CreateVectorFromAngle(270, 125)
End If
If keyHandler.IsKeyPressed(Key.Right) Then
PlayerShip.Velocity = Vector.CreateVectorFromAngle(90, 125)
End If
If keyHandler.IsKeyPressed(Key.Space) Then
Select Case status
Case GameState.Ready
Exit Select
Case GameState.Running
EntityFired(PlayerShip)
Exit Select
Case GameState.Paused
Exit Select
Case GameState.BetweenWaves
status = GameState.Running
ctlInfo.GameInfo = ""
StartWave()
Exit Select
Case GameState.GameOver
Exit Select
Case Else
Exit Select
End Select
End If
PlayerShip.Update(elapsed)
BombLoop(elapsed)
MissleLoop(elapsed)
AlienLoop(elapsed)
For Each alien As Alien In aliensRemove
aliens.Remove(alien)
gameRoot.Children.Remove(alien.SpriteCanvas)
AlienShot(alien)
Next
aliensRemove.Clear()
For Each missle As Missle In misslesRemove
missles.Remove(missle)
gameRoot.Children.Remove(missle.SpriteCanvas)
Next
misslesRemove.Clear()
If nextShot <= DateTime.Now Then
nextShot = DateTime.Now.AddMilliseconds(enemyShootMilliseonds).AddMilliseconds(elapsed.Milliseconds * -1)
shotsThisPass = shotsAtOnce
If shotsThisPass > aliens.Count Then
shotsThisPass = aliens.Count
End If
If aliens.Count > 0 Then
For Each alien As Alien In aliens
alienShooters.Add(alien)
Next
End If
While alienShooters.Count > shotsThisPass
alienShooters.RemoveAt(GetRandInt(0, alienShooters.Count - 1))
End While
For Each alien As Alien In alienShooters
EntityFired(alien)
Next
alienShooters.Clear()
End If
End Sub
Now we can almost call it a game:
Taking it one step further, I jumped into Expression Blend to create a bit more interesting XAML that our sprites are loading into their SpriteCanvas. This XAML can be found in the full download of the source code. The results make our game just a tad bit more interesting:
Conclusion
Silverlight provides a number of capabilities to enable robust game development. It should be noted that in this game, we are only scratching the surface. The core of this example used a game loop to position sprites on a canvas. Additional capabilities within Silverlight such as animations, styling, templating, and visual states provide even more ways to push the limits of browser based games. If you are at all like me, dabbling with game development is a great way to get your feet wet with Silverlight and learn some basics. Happy coding!
If you want to try this out, the download link for the source code is at the top of the article!
About The Author
Roger Guess is the Director of IT for The Wedge Group where he works with technologies such as Silverlight and WPF. He writes games in his spare time, and blogs at SilverlightAddict.com. He can be reached via email at email@rogerguess.net.