Before Introduction
If you only want to download the code and see what's happening, then the important things are:
- There's a solution for Silverlight 5 and one for WPF, but most units are simply used by both;
- The game uses the directional keys to move, Space to shot and either Esc, P or Enter to pause;
- The game has three levels. Between the levels you may acquire upgrades to your ships, so if you think the initial ships are too weak, well, that's on purpose.
You can play the
Silverlight game by opening this link:
http://paulozemek.azurewebsites.net/ShootEmUpTestPage.2013_10_10.html
Or you can download the
WPF version by clicking on the next image:
Introduction
I know that many programmers would love to write games. I myself started to program computers because I wanted to create games and, even if I already wrote some games, I've spent most of my time dealing with systems and solving problems related to database connectivity and better caching mechanisms.
One of the problems I see to write games is the lack of material. In my particular case, I usually look for graphics and sounds, as those are the things that I am not able to do. Yet, I think there's missing material for games in general. It is hard to find game tutorials and, when I find something, I only see material to write games using
XNA or
Unity. There is almost no material on how to write games using
WPF or
Silverlight or other similar technologies.
And, considering that I usually write games using those technologies, I though it will be a good idea to present a solution on how to write games using non-game specific technologies.
Why not use XNA or Unity?
My idea on writing games without using
XNA or
Unity is to show that you can use normal UI technologies to write games, which in many cases simplify the job as you don't need to care about how to render things to the screen or to recreate layout controls. Also, by doing this you will be able to create games without having to install such libraries/engines and you will be able to write games even for
Windows 8, as using
C# + XAML(which is what I am going to use in this article) is a trait of
Silverlight,
WPF and of
Windows 8 applications.
Declarative Animations
Most articles about games say that we must deal with a "game loop", moving objects (like the player ship and enemies) at each frame or time lapse. That's said even in articles that aren't specific for
XNA or
Unity and that's really how most games are written, yet it is not how it must be.
Seeing the
Storyboard
used by both
WPF and
Silverlight it is possible to see that animations can be declarative and a game is mostly made of animations. So, why should we care about a game loop?
The most compelling reason may be interaction. Declarative animations aren't interactive. They update values and the only input they have is time. Well, at least that's what happens with
WPF and
Silverlight's
Storyboard
s, but that doesn't mean you can't have declarative animations that can react to some external factors.
That, and some other reasons, made me write a library dedicated to animations recently, which I presented
here. In such library, the declarative animations are still written in
C#, not in the
XAML, using a
Fluent APIthat's very similar to common declarative code, yet it allows the use of delegates to build part of the animations or to validate some conditions, which is all we need to move a character when the user press some keys.
With such library, it is possible to write the following code to control the player character movements:
Collapse | Copy Code
AnimationBuilder.
BeginParallel().
BeginLoop().
BeginPrematureEndCondition(() => HorizontalMove >= 0).
RangeBySpeed(() => Canvas.GetLeft(this), 0, 120, (value) => Canvas.SetLeft(this, value)).
EndPrematureEndCondition().
EndLoop().
BeginLoop().
BeginPrematureEndCondition(() => HorizontalMove <= 0).
RangeBySpeed(() => Canvas.GetLeft(this), canvas.Width-Width, 120, (value) => Canvas.SetLeft(this, value)).
EndPrematureEndCondition().
EndLoop().
BeginLoop().
BeginPrematureEndCondition(() => VerticalMove >= 0).
RangeBySpeed(() => Canvas.GetTop(this), 0, 120, setTop).
EndPrematureEndCondition().
EndLoop().
BeginLoop().
BeginPrematureEndCondition(() => VerticalMove <= 0).
RangeBySpeed(() => Canvas.GetTop(this), canvas.Height-Height, 120, setTop).
EndPrematureEndCondition().
EndLoop().
EndParallel();
And, the best of all, such library does not change the kind of application you write. You can continue to create a
WPF application, a
Silverlight application, a
Windows Forms application, a Console application or whatever you want, even a
XNA application. Everything that's really needed by such library is an "event" that's triggered fast enough to be able to play the animations (in fact, you can even fake such event when calling the animations if, for example, you want to write a video file with a "consistent timing").
So, if you are worried that you should use an external library to write the presented game, don't be. It is a library, true, but it can be added to any application and, as you will receive the source code, you can add only the parts that you use to your applications and games.
Before writing some code
This article will explain important steps to write a game. Yet, it is not enough to simply copy those code blocks to have a game. In fact, to start you will need to create a new project (
Silverlight or
WPF) and add a reference to the animation library (be it to the dll or to its source code). Also, some images, sounds and other files aren't presented as part of the article itself but you will be able to verify that all steps presented here are really in use by downloading the final application.
1. The stars
I already said that I usually look for graphics as I am not a designer and a game needs a good design. Sometimes, by simply changing a badly written character by a better looking one the game seems to be more real and interesting.
Originally I was unsure if I would write a space
Shoot'em Up game or a platform one, but as I didn't find good looking backgrounds, I decided to go to the
Shoot'em Up game as I am capable of creating its background. In fact, I started writing the background because I wanted to check if it will be acceptable or not.
To have a background I only needed stars, so I immediately created a
Star
user control. It's only content is an
Ellipse
, which has its real size defined by code. In its code, the
Intensity
determines its color (maybe all white was OK, but I wanted some small differences) and how fast it will go down. To create the illusion that the ship is always moving forward, what really happens is that only the stars move towards the bottom.
The initial
Star
animation was a simple
Range
from its actual position to the off screen bottom and, by creating some stars at random positions I had something that looked like snow, as the stars were going off-screen and no new stars were appearing.
I thought about many possible solutions to make new stars appear but I opted to fake it. Each time a
Star
goes off-screen it is repositioned on the top (also off-screen) and a new horizontal position and
Intensity
is calculated. So, I can simply restart the animation (that is, put it inside a
Loop) and it looks like new stars are appearing all the time. And, as a new position, size and even a small off-screen value to the top is also calculated you don't keep seeing a star that's disappearing from the bottom immediately reapearing on the top.
The final code is this:
Collapse | Copy Code
public IEnumerable CreateAnimation()
{
var canvas = MainPage._instance.canvas;
int width = (int)canvas.Width;
canvas.Children.Add(this);
while(true)
{
yield return
AnimationBuilder.
RangeBySpeed
(
(int)Canvas.GetTop(this),
canvas.Height,
(_intensity + 1),
(value) => Canvas.SetTop(this, value)
);
int x = MainPage._random.Next(width);
int y = -MainPage._random.Next(30);
int intensity = MainPage._random.Next(MaxIntensity + 1);
Canvas.SetLeft(this, x);
Canvas.SetTop(this, y);
Intensity = intensity;
}
If you want, you can see a working version of it at:
http://paulozemek.azurewebsites.net/ShootEmUp/StarsSilverlight.html
2. The player character
To move the character I used
RangeBySpeed
animations that are enclosed by
PrematureEndCondition
s. That is, I have a
RangeBySpeed
that moves the character to the right, but that ends prematurely if the right arrow is not pressed.
I used a
RangeBySpeed
instead of a normal
Range
because a character at the center of the screen will arrive the right limit faster than a character that is located at the extreme left of the screen. If I was using a normal
Range
then the character will always take the same time to arrive to the right corner, which will represent as if it was moving at different speeds.
The big trick here was to put each one of the possible moves (left, right, up, down) ranges inside their own conditions, which are inside their own loops, so they can reexecute if you hold a key down, then stop, then press the key again. And finally, all those animations are inside a
ParallelGroup
animation (well, at least horizontal and vertical movements are allowed to run in parallel).
The only missing thing was how to tell a
PrematureEndCondition
if it should end or not. At least in
Silverlight we can't simply check if a key is pressed or not. To solve that, I implemented the
KeyDown
and
KeyUp
events to set the
HorizontalMove
and
verticalMove
, and the conditions only check those variables. Note that if we were writing the game for other frameworks (even for
WPF) we may be able to check if a key is down without having to implement the
KeyDown
and
KeyUp
events.
I already shown the code to animate the character, so here I will present the
KeyUp
and
KeyDown
event handlers:
Collapse | Copy Code
void _KeyDown(object sender, KeyEventArgs e)
{
switch(e.Key)
{
case Key.Left:
HorizontalMove = -1;
break;
case Key.Right:
HorizontalMove = 1;
break;
case Key.Up:
VerticalMove = -1;
break;
case Key.Down:
VerticalMove = 1;
break;
}
}
void _KeyUp(object sender, KeyEventArgs e)
{
switch(e.Key)
{
case Key.Left:
if (HorizontalMove == -1)
HorizontalMove = 0;
break;
case Key.Right:
if (HorizontalMove == 1)
HorizontalMove = 0;
break;
case Key.Up:
if (VerticalMove == -1)
VerticalMove = 0;
break;
case Key.Down:
if (VerticalMove == 1)
VerticalMove = 0;
break;
}
}
And you can see a working version of it at
http://paulozemek.azurewebsites.net/ShootEmUp/StarsAndPlayerSilverlight.html.
Note: You may need to click on the
Silverlight window to be able to control the ship using the directional arrows.
3. The enemies
How can I say that a game is a
Shoot'em Up if there's nothing to shoot?
Again I started by creating a class, named
Enemy
. Maybe for a really versatible game I should create it as an abstract class and then implement each different enemy in its own class, but for this game, a single class was enough.
At the first moment, the
Enemy
class had a fixed ship image and its animation was a simple
Range
to go from the off-screen top to the off-screen bottom. Then, there's another animation that keeps creating new enemies from time to time.
The code is like this:
Collapse | Copy Code
return
AnimationBuilder.
BeginSequence().
Add(() => canvasChildren.Add(this)).
BeginPrematureEndCondition(() => Health <= 0).
RangeBySpeed(-Height, MainPage._instance.canvas.Height, (30 + MainPage._random.Next(90)) * speed, (value) => Canvas.SetTop(this, value)).
EndPrematureEndCondition().
ExplodeIfDead(this, 1).
Add(() => canvasChildren.Remove(this)).
EndSequence();
4. The rectangle collisions + segmented time
At this moment we have a player character that moves, new enemies that appear on the screen... but the ships never collide.
To do a basic collision detection, we deal with rectangles. We consider the
Canvas.Left
and
Canvas.Top
of the characters and their
Width
and
Height
to build the rectangles, and then we check if there's an intersection using the method
Intersect
of the
Rect
type.
Seeing this image, we can easily see the intersection between the red and the green rectangles, with I am representing with the yellow rectangle.
But, when do we check for the collision?
The game is using declarative animations, which don't have a right moment to do the collision detection. You can easily put an imperative animation and check for collisions everytime it is invoked, but this will create an imprecise collision detection, as the
Update
s may be invoked with different time lapses.
The solution is the use of a segmented time (be it with the
BeginSegmentedTime
/
EndSegmentedTime
or by instantiating a
SegmentedType
directly). Such segmented time guarantees that it will update its inner animations at maximum intervals determined by its
SegmentDuration
. Note that animations will stay smooth, as they will be updated in smaller intervals if the time lapses are short, yet the
SegmentCompleted
will only be invoked when a segment is completed (that is, if the segment is of 15 milliseconds, an
Update
of 10 milliseconds will not generate a segment end, but a new
Update
of 5 will complete those 15 milliseconds and generate the completed event).
If, for example, the time lapse defined is of 15 miliseconds and there are two updates, both of ten miliseconds, what will really happen is:
- The first update will update the inner animation with those 10 miliseconds;
- The second update will be split in 2 steps. The first step will update only 5 miliseconds, to complete the 15 miliseconds to invoke the event, then it will invoke the
SegmentCompleted
event. And then finally it will do an extra update of 5 miliseconds to put all the objects at the right places.
The most important thing here is to put all the animations that participate on the collision detection as inner animations of this segmented time, as external animations will not be segmented and so a slowdown may make the external animation advance 500 milliseconds directly instead of segmenting it by 15 milliseconds before each collision detection.
So, the code to do the basic collision detection can look like this:
Collapse | Copy Code
internal static void _CheckAllCollisions()
{
var objects = _instance.canvas.Children;
PlayerCharacter._instance.IsReceivingDamage = false;
foreach(var enemy in objects.OfType())
{
if (PlayerCharacter._instance.Health > 0)
{
if (enemy.CollidesWith((WriteableBitmap)enemy.image.Source, PlayerCharacter._instance, (WriteableBitmap)PlayerCharacter._instance.image.Source))
{
PlayerCharacter._instance.Health --;
enemy.Health --;
PlayerCharacter._instance.IsReceivingDamage = true;
}
}
}
}
5. Explosions
The explosion may look very simple as we are already detecting collisions and we already have lots of animations. Yet, there's something very different here.
Up to this moment, all animations are simply composed of movement as we don't have different frames. Considering I am not simulating a explosion by code, but using an already drawn animation, I must be able to load and play it.
Searching the internet I found some explosion sprite sheets. So, at this moment, I must be able to use a sprite sheet.
A sprite sheet is usually composed of many equally sized images (well, at least that's the most common case if we have a single animation in the sprite sheet). One of the sprite sheets I found is this:
And what we need to do is to show only one of the "sprites" at once. To do this, I created the
SpriteSheet
user control. Its main purpose is simply to load a sprite sheet image but present only one frame at a time.
To do that we must apply a
RectangleGeometry
to the
Clip
property of the
UserControl
. The content of the control is a canvas with an image and so, each time I that must show a different frame, I change the
Canvas.Left
and/or the
Canvas.Top
of the image (to show the second frame, we must set the
Canvas.Left
to minus the frame width, so the first frame is invisible at the left and the second frame becomes visible).
Well, I did something more. The
SpriteSheet
control is capable of presenting the frames with a different size. To do this, I multiply the entire image size by the size I want to show the frame, then I divide the the size by the real frame size. With the entire image stretched I must also use the modified size when doing the Left/Top calculations, so the method that really "repositions the frame" ended up like this:
Collapse | Copy Code
private void _Invalidate()
{
var source = _bitmapSource;
if (source == null)
return;
if (_bitmapSource.PixelWidth <= 0)
return;
if (double.IsNaN(Width) || double.IsNaN(Height))
return;
int itemsPerRow = _bitmapSource.PixelWidth / _frameWidth;
int column = _frameIndex % itemsPerRow;
int row = _frameIndex / itemsPerRow;
Canvas.SetLeft(_image, Width * -column);
Canvas.SetTop(_image, Height * -row);
_image.Width = _bitmapSource.PixelWidth * Width / _frameWidth;
_image.Height = _bitmapSource.PixelHeight * Height / _frameHeight;
_clip.Rect = new Rect(0, 0, Width, Height);
}
Finally, considering that I created the
FrameIndex
property, to animate the explosion it is enough to do a
Range
from the first frame index to the last frame index in a time that we consider acceptable (I used 1 second in most cases).
Of course, I have to create such spritesheet component for the explosion, position it at the same place of a exploding ship, add it to the canvas were all characters are already added, play it and then remove it from the canvas. So, the explosion animation is like this:
Collapse | Copy Code
var explosion = _SpriteSheetImages.CreateExplosion((int)enemy.Width);
Add
(
() =>
{
Canvas.SetLeft(explosion, Canvas.GetLeft(enemy));
Canvas.SetTop(explosion, Canvas.GetTop(enemy));
Canvas.SetZIndex(explosion, Canvas.GetZIndex(enemy) + 1);
canvasChildren.Add(explosion);
}
).
BeginParallel().
Range(0, explosion.FrameCount, time, (value) => explosion.FrameIndex = value).
Range(1.0, 0.0, time/2, (value) => enemy.Opacity = value).
EndParallel().
Add(() => canvasChildren.Remove(explosion)).
You may notice that I also apply a range to the enemy's
Opacity
. To me it looks more natural, as the ships start to disappear while it is exploding, as my explosion doesn't include the ship image itself being destroyed. If there was an explosion per ship, already including the ship parts, I may immediately remove the ship from the screen while playing the explosion.
5.1. Sound of an explosion.
The explosion isn't complete without the sound of an explosion. Initially I was creating a new media element each time I wanted to play a sound, which wasn't working really well.
I don't really know the reason for this, but sometimes the sounds weren't playing. I don't believe it was because too many sound channels were used, as sometimes that happened to the first sounds.
So, the solution I found was to have the
MediaElement
s created all the time by adding them to the
MainWindow
. So, each time I wanted to play a sound, I simply played the right media element.
In some cases, though, I wanted to play the same sound 2 or 3 times before letting it stop. For sounds that start with a high volume and then decrease I solved this by simply stopping the sound that was still playing and then starting to play it again. But in case of some explosions that happened in sequence it appeared to be simply "delaying" the sound of a single explosion, which happened when the last ship was destroyed.
I can't say that I used the best solution of all times, but I solved the issue by creating 3 media elements for the explosions and by alternating them when playing. So, the first, second and third explosion may play at the same time and, if there's a fourth explosion, it will stop the first one, which is probably ending already and so, at least to me, the explosions sound great.
So, when playing a explosion sound I get the right media element using the
_GetExplosion
method:
Collapse | Copy Code
private static int _explosionIndex;
private static MediaElement[] _explosions;
internal static MediaElement _GetExplosion()
{
var result = _explosions[_explosionIndex];
_explosionIndex++;
if (_explosionIndex >= _explosions.Length)
_explosionIndex = 0;
return result;
}
6. The shoots
When I first thought about the shoots, I thought that there will be all kinds of different shoots, like explosive one, laser ones and the like. But I started with a single one, a
Laser
, which as you may imagine became a class.
But when I finally decided to add new resources to the game I decided to only change the shoots between normal and strong, without really creating new shoots. I did it only to avoid creating a huge game, as my purpose is not the create the best game, but to create a tutorial on how to create a basic game.
So, we have a
Laser
class and such class is responsible for doing the animation of a shoot. I needed to change the player animation to include the shot itself, with is done by adding the following code to the Parallel animation of the
PlayerCharacter
:
Collapse | Copy Code
BeginLoop().
BeginRunCondition(() => IsShootPressed && _hotOffset > 0).
BeginSequence().
Add(_Shoot).
Wait(() => _traits._waitBetweenShoots / _shootSpeed).
EndSequence().
EndRunCondition().
EndLoop().
And the shot itself is again a simple RangeBySpeed, but as I play a sound and make the laser become weaker after touching enemies, the final code is like this:
Collapse | Copy Code
AnimationBuilder.
BeginSequence().
PlaySound(() => MainPage._instance.mediaElementLaser, 0.4).
BeginPrematureEndCondition(() => _health <= 0).
RangeBySpeed(Canvas.GetTop(this), -Height, 300, (value) => Canvas.SetTop(this, value)).
EndPrematureEndCondition().
Add(() => MainPage._instance.canvas.Children.Remove(this)).
EndSequence();
And, as the shot also collides, I've added a code to check for shot collisions inside the
SegmentCompleted
event (in fact, inside the
foreach
over all the enemies):
Collapse | Copy Code
foreach(var shoot in objects.OfType())
{
Rect shotRect =
new Rect
(
Canvas.GetLeft(shoot) - Canvas.GetLeft(enemy),
Canvas.GetTop(shoot) - Canvas.GetTop(enemy),
shoot.Width,
shoot.Height
);
int oldHealth = enemy.Health;
if (oldHealth > 0 && shotRect.CollidesWith((WriteableBitmap)enemy.image.Source))
{
if (enemy._hitBy.Add(shoot))
{
byte value = (byte)shoot.Health;
var hit = new Hit();
hit.Intensity = value;
hit.SetCenterX(shoot.GetCenterX());
hit.SetCenterY(shoot.GetCenterY());
AnimationManager.Add(hit.CreateAnimation());
}
int shootDamage = PlayerCharacter._instance._traits._shootDamage;
if (shoot._isGreen)
shootDamage *= 2;
enemy.Health -= shootDamage;
shoot.Health -= 10;
PlayerCharacter._instance.Score += oldHealth - enemy.Health;
if (enemy.Health <= 0)
{
enemy._killedByShoot = true;
shoot._killCount++;
double enemyValue = enemy.MaxHealth / 40.0;
enemyValue *= enemyValue;
enemyValue *= 100;
int killScore = (int)enemyValue / enemy._hitBy.Count;
if (killScore < 1)
killScore = 1;
killScore *= shoot._killCount;
PlayerCharacter._instance.Score += killScore;
var positionedScore = new PositionedScore();
var animation = positionedScore.CreateAnimation(enemy.GetCenterX(), enemy.GetCenterY(), killScore);
AnimationManager.Add(animation);
}
}
}
As you can see, I am still ignoring the single responsibility principle. I am having to change the
SegmentCompleted
handler everytime there's a new kind of valid collision. If I really wanted to have a game that grows I would have to review such implementation, but I still think it is OK to keep it that way for a game with only 3 levels.
7. The pixel by pixel collision detection
You probably noticed that somethings don't require a specific order. I could've added the pixel by pixel collision detection before supporting the shoots, yet I opted to first have the working shoots to then write the better collision detection algorithm. If I was working with somebody else we could've worked at the same time on different things, so don't take that
seven number as a required order.
Considering I started the project as a
Silverlight one, and considering I already knew that the
WriteableBitmap
has the
Pixels
property, I immediately though that I should convert my bitmaps to
WriteableBitmap
s to check their pixels. Maybe I should've used a better approach, as it is not the best one for
WPF and I did create a
WPF project reusing most of the
Silverlight code untouched. Yet, that's what I did and I am presenting it. For the images that will participate on collision detection (that is, the ship images) I load them as
WriteableBitmap
s.
The
Silverlight's
WriteableBitmap.Pixels
property returns them as an array of
int
. The colors are in the
ARGB format and, to detect a collision, I only consider the pixels that have a opacity greater than 127 (that is, the A(lpha) element should be bigger than 127).
Maybe this is the hardest code to understand in this application, so I will try to split it in some parts:
- I still keep the code that does a rectangle intersection. In fact, if there is no intersection there is no chance of collision and, if there is an intersection we must check the pixel values;
- To get a pixel at a given X, Y coordinate, considering the Pixels property is a single dimension array, we should do a calculation like this:
Collapse | Copy Code
int pixelIndex = (y * bitmap.Width) + x;
- Look at the intersection image. You can see that betwen the Red and the Green "squares" (don't judge the precision of my drawing) there is a yellow intersection. When analysing for the collision, we will be analysing all of the intersection pixels from both sides, so we need to calculate where exactly we should check each image. The position "0, 0" of the intersection coincides as the 0, 0 of the Green square, but it is not the 0, 0 of the red square.
Well, considering those factors and also trying to optimize the reads, I finished up with this code:
Collapse | Copy Code
public static bool CollidesWith(this FrameworkElement element1, WriteableBitmap bitmap1, FrameworkElement element2, WriteableBitmap bitmap2)
{
int left1 = (int)Canvas.GetLeft(element1);
int top1 = (int)Canvas.GetTop(element1);
int width1 = (int)element1.Width;
int height1 = (int)element1.Height;
int left2 = (int)Canvas.GetLeft(element2);
int top2 = (int)Canvas.GetTop(element2);
int width2 = (int)element2.Width;
int height2 = (int)element2.Height;
Rect rect1 = new Rect(left1, top1, width1, height1);
Rect rect2 = new Rect(left2, top2, width2, height2);
Rect intersect = rect1;
intersect.Intersect(rect2);
if (intersect.IsEmpty)
return false;
int intersectLeft = (int)intersect.Left;
int intersectTop = (int)intersect.Top;
int intersectRight = (int)intersect.Right;
int intersectBottom = (int)intersect.Bottom;
var pixels1 = bitmap1.Pixels;
var pixels2 = bitmap2.Pixels;
for(int y=intersectTop; yint