10 Minute Silverlight Game Programming Tutorial - Shootorial Conversion #5 (C#)
Welcome bac..
Ahhh, screw it. You know the drill.
This time, we blow $#!* up.
Prerequisites
- All the prerequisites defined in Silverlight game programming Shootorial #4 conversion.
QuickStart
If you want to see the end result of this tutorial and you have installed all the prerequisites, then please download the ZIP file below, unzip it and open the shootorial.html file in your browser. Now, you can blow up enemy ships and they will also explode when they hit you (however, enemy missles have no effect…yet ;)). Like before, you will need to click on the Silverlight control to give it focus first.
- Shootorial #5 Sample (you may read this software’s license here)
You can also see it in action, here.
Lesson
Step 1: Create the IGameEntityManager interface
In Shootorial 3, we added the IGameEntity interface which represented every game element on the screen. IGameEntity’s Update function took a Canvas object as a parameter. This causes two problems:
- It creates a tight coupling between every game entity and the Canvas which would severely hamper unit testing. (I’ll be the first to admit that there are plenty of other tight coupling issues in these shootorials, but this is one of the most glaring :)).
- Game entities adding and removing themselves from the Canvas individually leaves no way to intercept this behavior at a common point.
So, to fix this issue, we will declare a IGameEntityManager object that will abstract away the need for direct access to the instance of the Canvas. Open a file named IGameEntityManager.cs and put this code in there:
namespace ShootorialApplication
{
using System;
using System.Windows;
using System.Windows.Controls;
using System.Collections.Generic;
public interface IGameEntityManager
{
void AddGameEntity(ContentControl control);
void RemoveGameEntity(ContentControl control);
void AddEnemy(EnemyShip enemyShip);
void RemoveEnemy(EnemyShip enemyShip);
Ship HeroShip { get; }
IEnumerable<EnemyShip> Enemies { get; }
bool DetectCollision(ContentControl controlOne, ContentControl controlTwo);
}
}
To fully abstract away access to the canvas, the IGameEntity also needs updating:
public interface IGameEntity
{
//Called once per frame
void Update( IGameEntityManager theManager );
void Move( Direction direction );
}
So, instead of requiring an instance of Canvas, the Update function now uses an instance of IGameEntityManager.
Rather than tell you what these new functions do, I will show you.
Step 2: Implement the IGameEntityManager interface
Instead of creating a new GameEntityManager class to implement the IGameEntityManager interface, we will reuse the the ShootorialControl class:
namespace ShootorialApplication
{
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Controls;
using System.Windows.Shapes;
using System.Collections.Generic;
public partial class ShootorialControl : UserControl, IGameEntityManager
...
}
Now, to implement the functions. First, the AddGameEntity and RemoveGameEntity functions:
public partial class ShootorialControl : UserControl, IGameEntityManager
{
...
public void AddGameEntity(ContentControl control)
{
theCanvas.Children.Add(control);
}
public void RemoveGameEntity(ContentControl control)
{
theCanvas.Children.Remove(control);
}
...
}
These functions delegate to Canvas.Children.Add and Canvas.Children.Remove functions, respectively. This prevents unecessary coupling between IGameEntity and the Canvas object.
Next, the AddEnemy and RemoveEnemy functions:
public partial class ShootorialControl : UserControl, IGameEntityManager
{
private IList<EnemyShip> enemies = new List<EnemyShip>();
...
public void AddEnemy(EnemyShip enemyShip)
{
enemies.Add(enemyShip);
this.AddGameEntity(enemyShip);
}
public void RemoveEnemy(EnemyShip enemyShip)
{
enemies.Remove(enemyShip);
this.RemoveGameEntity(enemyShip);
}
...
}
In addition to adding new game elements to the canvas via the AddGameEntity and RemoveGameEntity functions, these functions also add to a list of enemy objects tracked in the highlighted “enemies” variable. This will come in handy later to test whether or not the hero ship’s bullets have hit their target.
Speaking of which, this class exposes both the hero ship and the of currently displaying enemies through these properties:
public partial class ShootorialControl : UserControl, IGameEntityManager
{
...
public Ship HeroShip
{
get
{
return spaceShip;
}
}
public IEnumerable<EnemyShip> Enemies
{
get
{
return enemies;
}
}
...
}
Finally, to determine whether or not bullets (or ships) have connected, we will need to add some sort of “collision detection”. In the world of video game programming, collision detection encompasses a body of techniques used to see whether two game elements have “hit” each other by trying to occupy the same space. Since we use a ContentControl to describe our game elements, we simply need to figure out when two content controls overlap. The IGameEntityManager class works across all game elements, so it will contain this functionality in the DetectCollision function:
public partial class ShootorialControl : UserControl, IGameEntityManager
{
...
public bool DetectCollision( ContentControl controlOne, ContentControl controlTwo )
{
Rect controlOneRect = new Rect( new Point( Convert.ToDouble(controlOne.GetValue(Canvas.LeftProperty)),
Convert.ToDouble(controlOne.GetValue(Canvas.TopProperty))
),
new Point( (Convert.ToDouble(controlOne.GetValue(Canvas.LeftProperty)) + controlOne.ActualWidth),
(Convert.ToDouble(controlOne.GetValue(Canvas.TopProperty)) + controlOne.ActualHeight)
)
);
Rect controlTwoRect = new Rect( new Point( Convert.ToDouble(controlTwo.GetValue(Canvas.LeftProperty)),
Convert.ToDouble(controlTwo.GetValue(Canvas.TopProperty))
),
new Point( (Convert.ToDouble(controlTwo.GetValue(Canvas.LeftProperty)) + controlTwo.ActualWidth),
(Convert.ToDouble(controlTwo.GetValue(Canvas.TopProperty)) + controlTwo.ActualHeight)
)
);
controlOneRect.Intersect(controlTwoRect);
return !(controlOneRect == Rect.Empty);
}
...
}
Lots of code, but still very simple. Each Rect structure declaration represents a rectangle, called the “bounding rectangle” in video game programmer speak, that fully encompasses the corresponding ContentControl. For example, even though the outline of the hero ship contains many bumps and grooves, if you drew the smallest rectangle around it such that the rectangle didn’t cross into any part of the ship image, you would have its “bounding rectangle”. So, testing to see if two these ContentControls overlap, makes for a very simple (but somewhat inaccurate) method of collision detection. The call to Rect.Intersect determines if the two rectangles representing “controlOne” and “controlTwo” overlap and if so, it puts the resulting rectangle (more than likely very small) into the “controlOneRect” variable. If they do not overlap, then “controlOneRect” will equal the Rect.Empty value. See the Rect structure documentation for more information.
- Learn more about the Rect structure
- Learn more about the ContentControl class
- Learn more about the Point structure
As mentioned in Step 1, we want to limit access to the visual Canvas as much as possible, so let’s update the two functions which work on the canvas reference directly with ones that use the IGameEntityManager instead. First, ShootorialControl_KeyDown function:
private void ShootorialControl_KeyDown(object sender, KeyEventArgs e)
{
if( e.Key == Key.Right )
spaceShip.Move( Direction.Right );
else if( e.Key == Key.Left )
spaceShip.Move( Direction.Left );
else if( e.Key == Key.Up )
spaceShip.Move( Direction.Up );
else if( e.Key == Key.Down )
spaceShip.Move( Direction.Down );
else if( e.Key == Key.Space && shootLimiter == 8 )
{
shootLimiter = 0;
spaceShip.Fire(this);
}
}
And now, the ShootorialControl_Rendering function:
private void ShootorialControl_Rendering(object sender, EventArgs e)
{
if( shootLimiter < 8 )
shootLimiter += 1;
enemyTimer += 1;
if( enemyTimer > 60 )
{
enemyTimer = 0;
this.AddEnemy(new EnemyShip());
}
for( int elementIndex = 0; elementIndex < theCanvas.Children.Count; elementIndex++ )
{
IGameEntity gameObject = theCanvas.Children[elementIndex] as IGameEntity;
if( gameObject != null )
{
gameObject.Update(this);
}
}
}
That should do it. Now, let’s put some of these new functions to work.
Step 3: Use the IGameEntityManager interface
First, we’ll update the ScrollingBackround class in ScrollingBackground.cs:
public class ScrollingBackground : ContentControl, IGameEntity
{
...
public void Update( IGameEntityManager theManager )
{
Move(Direction.Left);
}
...
}
Pretty simple. Remember, in Step 1 we updated the IGameEntity which the ScrollingBackground class implements, so it needed updating here. Next, the Ship class:
public class Ship : ContentControl, IGameEntity
{
...
public void Update( IGameEntityManager theManager )
{
}
public void Fire(IGameEntityManager theManager)
{
Missile missile = new Missile(Canvas.GetLeft(this) + 57, Canvas.GetTop(this) + 17);
theManager.AddGameEntity(missle);
}
...
}
Again, very simple. Note, the call to AddGameEntity replaces the previous call to Canvas.Children.Add.
We will update the rest of the classes while adding collision detection logic at the same time. But before that happens, we need to add two new classes.
Step 4: Create the Explosion class
Kongregate’s fifth tutorial adds functionality to make enemy ships explode and enemies shoot, so we will do the same. First, the explosions: create a file named Explosion.cs and put this code in it:
namespace ShootorialApplication
{
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Controls;
using System.Windows.Shapes;
public class Explosion : ContentControl, IGameEntity
{
private static BitmapImage[] explosionImages = new BitmapImage[] {
new BitmapImage(new Uri("/explosion1.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion2.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion3.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion4.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion5.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion6.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion7.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion8.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion9.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion10.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion11.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion12.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion13.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion14.png", UriKind.RelativeOrAbsolute)),
new BitmapImage(new Uri("/explosion15.png", UriKind.RelativeOrAbsolute))
};
private int curExplosionImage = 0;
private Image explosionImage = new Image();
public Explosion(double left, double top)
{
explosionImage.Source = explosionImages[curExplosionImage];
this.Content = explosionImage;
Canvas.SetLeft(this, left);
Canvas.SetTop(this, top);
}
public void Update(IGameEntityManager theManager)
{
if( curExplosionImage == explosionImages.Length )
{
theManager.RemoveGameEntity(this);
}
else
{
explosionImage.Source = explosionImages[curExplosionImage];
curExplosionImage++;
}
}
public void Move(Direction direction)
{
}
}
}
The static array of BitmapImages contains each “frame” of an animated explosion. Think of a “frame” as a specific image displayed at a specific point in time. Displaying many frames rapidly simulates animation, in this case the animation of an exploding ship.
So, the the constructor displays the first frame of the explosion and positions it at the point represented by the “left” and “top” parameters. The Update function first checks to see if we’ve reached the end of the animation. If so, it removes the explosion, making the ship officially “dead”. If not, it displays the next frame of the explosion.
Note, you can find the explosion images in the download archive. Also, as a consequence of the asynchronous loading behavior of an Image, the first explosion may not show because not all the explosion images my reside in memory yet. See the Image class documentation for more information on this issue.
This class will come in handy, later. Now, let’s add enemy missiles.
Step 4: Create the EnemyMissile class
Similar to Kongregate’s tutorial we will create a class to represent enemy missiles. Create a file named EnemyMissle.cs and put this code in it:
namespace ShootorialApplication
{
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Controls;
using System.Windows.Shapes;
public class EnemyMissile : ContentControl, IGameEntity
{
private int speed = 20;
public EnemyMissile(double left, double top)
{
Rectangle missleRectangle = new Rectangle();
missleRectangle.Height = 7;
missleRectangle.Width = 15;
missleRectangle.Fill = new SolidColorBrush(Colors.Red);
missleRectangle.Stroke = new SolidColorBrush(Colors.Black);
missleRectangle.RadiusX = 3;
missleRectangle.RadiusY = 3;
missleRectangle.StrokeThickness = 2;
this.Content = missleRectangle;
Canvas.SetLeft(this, left);
Canvas.SetTop(this, top);
}
public void Update( IGameEntityManager theManager )
{
Move(Direction.Left);
if( theManager.DetectCollision( this, theManager.HeroShip ))
{
theManager.RemoveGameEntity(this);
}
if( Canvas.GetLeft(this) < 0)
{
theManager.RemoveGameEntity(this);
}
}
public void Move( Direction direction )
{
Canvas.SetLeft(this, Canvas.GetLeft(this) - speed);
}
}
}
If this class looks similar to the Missile class, it should because I copied the code ;). However, I also highlighted some of the major differences. First, as defined in the constructor, enemy missiles will appear red instead of white. Second, in the Update function we get our first look at collision detection logic. The call to DetectCollision simply asks whether or not this missile has hit the hero ship. If so, it asks IGameEntityManager to remove it from display via the RemoveGameEntity function. Finally, the Move function subtracts the “speed” variable from the current position, meaning that missiles fired from enemies will fly to the left.
Good, now let’s go through the rest of the classes.
Step 5: Update the EnemyShip class
The EnemyShip class requires lots of updating, so let’s take it one at a time. First, since ships can now explode, let’s add an Explode function:
public class EnemyShip : ContentControl, IGameEntity
{
...
private bool exploded = false;
...
public void Explode()
{
exploded = true;
}
}
This function merely sets the “exploded” variable which tracks the state of whether or not the enemy ship has exploded. Now, let’s make the enemy ship explode when it collides with the hero ship, by changing the Update function:
public class EnemyShip : ContentControl, IGameEntity
{
...
public void Update( IGameEntityManager theManager )
{
if( theManager.DetectCollision( this, theManager.HeroShip ))
{
this.Explode();
}
if( exploded )
{
theManager.RemoveEnemy(this);
theManager.AddGameEntity(new Explosion( Canvas.GetLeft(this), Canvas.GetTop(this) ));
}
else
{
Move(Direction.Left);
if( Canvas.GetLeft(this) < -100 )
{
theManager.RemoveEnemy(this);
}
}
}
...
}
So, the Update function first checks for a collision between this enemy ship and the hero ship. If so, it calls explode. Next, if the enemy ship has exploded, it replaces itself with an Explosion. Note that it passes its position into the Explosion class’ constructor, so they both appear in the same spot.
Finally, let’s make the enemy ship shoot missiles:
public class EnemyShip : ContentControl, IGameEntity
{
...
private int shootTimer = 0;
public void Update( IGameEntityManager theManager )
{
if( theManager.DetectCollision( this, theManager.HeroShip ))
{
this.Explode();
}
if( exploded )
{
theManager.RemoveEnemy(this);
theManager.AddGameEntity(new Explosion( Canvas.GetLeft(this), Canvas.GetTop(this) ));
}
else
{
Move(Direction.Left);
if( Canvas.GetLeft(this) < -100 )
{
theManager.RemoveEnemy(this);
}
else
{
shootTimer +=1;
if(shootTimer > 30)
{
shootTimer = 0;
theManager.AddGameEntity( new EnemyMissile( Canvas.GetLeft(this) - 25, Canvas.GetTop(this) + 2 ));
}
}
}
}
...
}
Similar to the hero ship, this class has a “shootTimer” variable that determines when enemy ships fire. If this variable goes above 30, the enemy ship “shoots” a missile by creating a new EnemyMissile class and telling IGameEntityManager to add it.
Last but not least, let’s update the Missile class.
Step 6: Update the Missile class
public class Missle : ContentControl, IGameEntity
{
...
public void Update( IGameEntityManager theManager )
{
if( Canvas.GetLeft(this) > 640 )
{
theManager.RemoveGameEntity(this);
}
else
{
foreach( EnemyShip enemy in theManager.Enemies )
{
if( theManager.DetectCollision( enemy, this ))
{
enemy.Explode();
theManager.RemoveGameEntity(this);
}
}
}
Move(Direction.Right);
}
...
}
}
Recall that in Step 2 we added the Enemies property to the IGameEntityManager class which would hold the list of currently displaying enemies. In its Update function, the missile loops through each enemy ship and checks for a collision. If it has collided with an enemy ship, it tells it to explode, and then removes itself from display.
Whew, glad that’s over. Now, let’s build it.
Step 7: Build it
Even after adding a few new classes and several new images, the build file should remain pretty self explanatory:
<ItemGroup>
<Compile Include="IGameEntity.cs">
</Compile>
<Compile Include="Missile.cs">
</Compile>
<Compile Include="Ship.cs">
</Compile>
<Compile Include="ScrollingBackground.cs">
</Compile>
<Compile Include="EnemyShip.cs">
</Compile>
<Compile Include="IGameEntityManager.cs">
</Compile>
<Compile Include="EnemyMissile.cs">
</Compile>
<Compile Include="Explosion.cs">
</Compile>
</ItemGroup>
...
<ItemGroup>
<None Include="explosion1.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion2.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion3.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion4.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion5.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion6.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion7.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion8.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion9.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion10.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion11.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion12.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion13.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion14.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="explosion15.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
Of course, the build command still hasn’t changed:
"C:\WINDOWS\Microsoft.NET\Framework\v3.5\msbuild.exe" ShootorialApplication.csproj
Step 7: Run it!
To run it, simply open the shootorial.html file in your browser. Now, you can blow up enemy ships and they will also explode when they hit you. Remember, to control the ship, you need to give Silverlight focus by clicking on the Silverlight control first (i.e just click on the ship).
Conclusion
Yet another big article, and, by the looks of it, they won’t get any shorter for a while. But, one should expect that when talking about concepts like collision detection. Keep in mind that simple collision detection between rectangles barely scratches the surface of what most games today need, so I encourage you to do more reading on the subject. Next time, we will make the hero ship take damage, add scoring and add a health meter, so stay tuned!



