OK, first things first. We need to install Microsoft Expression Blend (I’m using the 2.5 June 2008 Preview which has been updated for Silverlight Beta 2 and version 3.5 SP1 of the .NET Framework)
You will also need to install the Silverlight Tools Beta 2 for VS 2008 in order to edit Silverlight projects & codefiles in VS:
This includes Silverlight version 2 client which will allow you to run all the samples in your browsers. Note that this one requires all dependant apps be closed & takes 10 to 15 minutes to install, so its a good time to grab a coffee if you’re so inclined.
Now onto the basics. Aside from the demo’s & tutorials available within Expression Blend, I’ve primarily utilised the
following two resources as a basis to develop my understanding of Silverlight:
As a starting point, I’d like to develop two small projects to demonstrate the application of Silverlight. The first a simple game, and the second a simple client-side grid capable of aggregating data directly from a backend SqlServer database via a WCF service.
I asked Rachel (my partner) to suggest a simple game to re-author in Silverlight, and she came back with Pacman. There are similar projects online, however most of them seem to utilise Microsofts XNA/Game Studio which is not my interest in this exercise. I came across http://ctrl-alt-die.blogspot.com/2008/03/pacman-silverlight.html which seems to be exactly what I want to accomplish here, so for my very first Silverlight project I’ll use this post from Dominic Green’s blog as a springboard to get me started into re-creating the game myself in order to gain familiarity with both xaml (I didn’t realise Silverlight utilised so much xaml until this week!) and Expression Blend.
So far, I’ve found that using VS & Expression Blend side-by-side has been the best strategy. The majority can be done within VS, however I am finding that Expression Blend is handy for the positioning & layering of objects visually in the designer.
After following the approach indicated in the blog post, we have a simple pacman screen with a moving pacman & collision detection for the boundaries. Now we’re on our own, so lets take it one step at a time
The next thing we need is for our pacman’s mouth to be facing correctly for his direction of movement.
Lets hit IrfanView (or any simple image editor) and produce rotated images for the pacman facing up, down, and left.
Now the key things we are missing is some tic tacs? for our pacman to munch on, and a couple of monsters.
Rather than deliberate on the specifics of how I tackled each area of the game development, I’ll dump the complete source in which these should be clearly apparent.
namespace SilverlightPacman
{
public partial class Page : UserControl
{
int count = 0;
int spriteX = 0;
int spriteY = 0;
int moveX = 0;
int moveY = 0;
Rect[] rects;
int noRects;
/// <summary>
/// The current appropriate image given his direction of movement (or attempted movement)
/// </summary>
string pacImage = "pacman.png";
/// <summary>
/// Stores the tictacs so we can make our pacman eat them
/// </summary>
Ellipse[] tictacs;
/// <summary>
/// The bad guys!
/// </summary>
Image[] monsters;
// and a timer for monsters
System.Threading.Timer aiTimer;
public Page()
{
// Required to initialize variables
InitializeComponent();
ResetGame();
}
/// <summary>
/// Clears the game board and restarts game play
/// </summary>
private void ResetGame()
{
// clear any existing game objects
if (monsters != null)
foreach (Image m in monsters) GameMap.Children.Remove(m);
if (aiTimer != null)
aiTimer.Dispose();
if (tictacs != null)
foreach (Ellipse e in tictacs) GameMap.Children.Remove(e);
// create the boundaries
createOutOfBounds();
// start pacman at the entrance
spriteX = 92;
spriteY = 190;
pacman.SetValue(Canvas.LeftProperty, (double)spriteX);
pacman.SetValue(Canvas.TopProperty, (double)spriteY);
// and draw some tic tacs for foodage
tictacs = new Ellipse[2000]; // should be enough
DrawTicTacs();
// lets add some monsters
PlaceMonsters();
// start our timer for the monster movement (gives a 2 second headstart for pacman)
aiTimer = new System.Threading.Timer(new System.Threading.TimerCallback(RunMonsterAI), null, 2000, 300);
}
private enum CoinToss
{
heads,
tails
}
private CoinToss FlipCoin()
{
Random rand = new Random();
if (rand.Next(42) % 2 == 0)
return CoinToss.heads;
else
return CoinToss.tails;
}
/// <summary>
/// Makes it easy to re-call the movement code. If reverseOverride is tainted, they will make the first possible move
/// </summary>
/// <param name="reverseOveride"></param>
private delegate void moveDelegate(bool reverseOverride);
/// <summary>
/// Moves the monsters based on the simple strategy of generally moving towards pacman
/// </summary>
/// <param name="unused"></param>
private void RunMonsterAI(object unused)
{
// rule 1: Monsters should move towards pacman
// need UI thread to access DependencyProperties
Dispatcher.BeginInvoke(() =>
{
foreach (Image monster in monsters)
{
// pull position props
double pacLeft = (double)pacman.GetValue(Canvas.LeftProperty);
double pacTop = (double)pacman.GetValue(Canvas.TopProperty);
double monLeft = (double)monster.GetValue(Canvas.LeftProperty);
double monTop = (double)monster.GetValue(Canvas.TopProperty);
bool moved = false;
// need to mixup the order of these so they won't ALWAYS move horizontally when they have both options available
moveDelegate horizontal = delegate(bool logicOverride)
{
// horizontal first
if (logicOverride || pacLeft > monLeft) // try right
if (!CheckForCollisionAny(monLeft + 5, monTop, 18, 18)) // can move
{
monLeft += 5;
monster.SetValue(Canvas.LeftProperty, monLeft);
moved = true;
}
if (!moved && logicOverride || pacLeft < monLeft) // try left
if (!CheckForCollisionAny(monLeft - 5, monTop, 18, 18)) // can move
{
monLeft -= 5;
monster.SetValue(Canvas.LeftProperty, monLeft);
moved = true;
}
};
moveDelegate vertical = delegate(bool logicOverride)
{
if (!moved && logicOverride || pacTop > monTop) // try down
if (!CheckForCollisionAny(monLeft, monTop + 5, 18, 18)) // can move
{
monTop += 5;
monster.SetValue(Canvas.TopProperty, monTop);
moved = true;
}
if (!moved && pacTop < monTop) // try up
if (!CheckForCollisionAny(monLeft, monTop - 5, 18, 18)) // can move
{
monTop -= 5;
monster.SetValue(Canvas.TopProperty, monTop - 5);
moved = true;
}
};
moveDelegate flipMove = delegate(bool logicOverride)
{
if (FlipCoin() == CoinToss.heads)
{
horizontal(logicOverride);
vertical(logicOverride);
}
else
{
vertical(logicOverride);
horizontal(logicOverride);
}
};
// try logical moves first
flipMove(false);
// if no sensible moves can be made (ie. towards pacman) then override the logic and force any possible move
if (!moved)
{
flipMove(true);
}
}
// and finally lets check to see if they've caught pacman
// need to do this based on pacmans moves as well
CheckForMonsterSmackdown();
});
}
/// <summary>
/// Checks if a monster has come in contact with pacman, and Resets the game as needed
/// </summary>
private void CheckForMonsterSmackdown()
{
foreach (Image monster in monsters)
{
// pull position props
double pacLeft = (double)pacman.GetValue(Canvas.LeftProperty);
double pacTop = (double)pacman.GetValue(Canvas.TopProperty);
double monLeft = (double)monster.GetValue(Canvas.LeftProperty);
double monTop = (double)monster.GetValue(Canvas.TopProperty);
// look for any intersect
Rect pacRect = new Rect(pacLeft, pacTop, pacman.Width, pacman.Height);
Rect monRect = new Rect(monLeft, monTop, 18, 18);
// check for intersect with boundary
pacRect.Intersect(monRect); // returns the intersect rect (if there is one)
if (pacRect != Rect.Empty)
{
ResetGame();
break;
}
}
}
/// <summary>
/// Handler for all keystrokes in the game
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void UserControl_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
moveX = 0;
moveY = -5;
pacImage = "pacman_up.png";
if (!CheckForCollision())
updatePosition(moveX, moveY);
break;
case Key.Down:
moveX = 0;
moveY = 5;
pacImage = "pacman_down.png";
if (!CheckForCollision())
updatePosition(moveX, moveY);
break;
case Key.Left:
moveX = -5;
moveY = 0;
pacImage = "pacman_left.png";
if (!CheckForCollision())
updatePosition(moveX, moveY);
break;
case Key.Right:
moveX = 5;
moveY = 0;
pacImage = "pacman.png";
if (!CheckForCollision())
updatePosition(moveX, moveY);
break;
}
}
/// <summary>
/// Checks if the currently propositioned move of Pacman will intersect any of the defined boundaries
/// </summary>
/// <returns></returns>
private bool CheckForCollision()
{
return CheckForCollisionAny(spriteX + moveX, spriteY + moveY, pacman.Width, pacman.Height);
}
/// <summary>
/// Checks if the object specified by the given co-ordinates is colliding with defined boundaries
/// </summary>
/// <param name="X"></param>
/// <param name="Y"></param>
/// <param name="Width"></param>
/// <param name="Height"></param>
/// <returns></returns>
private bool CheckForCollisionAny(double X, double Y, double Width, double Height)
{
bool tmp = false;
// and loop through the boundary rects to look for any intersects
for (int i = 0; i < noRects; i++)
{
// construct a rect representative of the move taking place
Rect tmpRect = new Rect(X, Y, Width, Height);
// check for intersect with boundary
tmpRect.Intersect(rects[i]); // returns the intersect rect (if there is one)
if (tmpRect != Rect.Empty)
{
tmp = true;
pacCollision.Text = i.ToString();
}
}
return tmp;
}
/// <summary>
/// Moves pacman by the given offsets
/// Implicitly calls AnimatePacman, EatTicTacs, and checks for Monster Collision
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
private void updatePosition(int x, int y)
{
spriteX += x;
spriteY += y;
// i needed to cast to double, looks like the dependency prop def has changed recently
pacman.SetValue(Canvas.LeftProperty, (double)spriteX);
pacman.SetValue(Canvas.TopProperty, (double)spriteY);
animatePacman();
// and mainly to help me define the boundaries, lets throw the pacman position co-ordinates on the page
pacPosition.Text = pacman.GetValue(Canvas.LeftProperty).ToString() + "," + pacman.GetValue(Canvas.TopProperty).ToString();
// have pacman eat any tictacs that he crosses paths with
EatTicTacs();
// check for death
CheckForMonsterSmackdown();
}
/// <summary>
/// Alternates the image used for pacman between mouth open and mouth closed to visualise a hungry pacman
/// </summary>
private void animatePacman()
{
if (count % 4 == 0)
pacman.Source = new BitmapImage(new Uri("pacman2.png", UriKind.Relative));
else
pacman.Source = new BitmapImage(new Uri(pacImage, UriKind.Relative));
count++;
}
/// <summary>
/// Defines the boundaries of play, based upon the corridors visible on the game map image
/// </summary>
private void createOutOfBounds()
{
noRects = 50;
int i = 0; // make it easier
rects = new Rect[noRects];
// left outer wall = 95-100 2 blocks, 10-135 top & 260-410 bottom
rects[i++] = new Rect(95, 10, 5, 125);
rects[i++] = new Rect(95, 260, 5, 150);
// top outer wall = 95-505, 10-15
rects[i++] = new Rect(95, 10, 410, 5);
// right outer wall, 2 blocks
rects[i++] = new Rect(505, 10, 5, 125);
rects[i++] = new Rect(505, 260, 5, 150);
// bottom outer wall
rects[i++] = new Rect(95, 408, 410, 5);
// and now all the internal stuff (working from top left to bottom right, across and then down)
// the top half
rects[i++] = new Rect(125, 40, 50, 27);
rects[i++] = new Rect(201, 41, 64, 27);
rects[i++] = new Rect(290, 15, 19, 50); // top centre 9
rects[i++] = new Rect(335, 40, 65, 27);
rects[i++] = new Rect(426, 41, 50, 27);
rects[i++] = new Rect(125, 93, 49, 15);
rects[i++] = new Rect(201, 93, 20, 92);
rects[i++] = new Rect(217, 133, 46, 13);
rects[i++] = new Rect(245, 93, 109, 14);
rects[i++] = new Rect(290, 106, 21, 40);
rects[i++] = new Rect(335, 133, 46, 13);
rects[i++] = new Rect(381, 94, 19, 92);
rects[i++] = new Rect(426, 93, 49, 14);
// the middle block
rects[i++] = new Rect(246, 173, 108, 52);
// entrance
rects[i++] = new Rect(99, 133, 77, 5); // top top of entrance
rects[i++] = new Rect(167, 135, 5, 47); // top inside of entrance
rects[i++] = new Rect(93, 180, 82, 5); // top corridor of entrance
rects[i++] = new Rect(93, 212, 82, 6); // bottom corridor of entrance
rects[i++] = new Rect(169, 215, 6, 50); // bottom inside of entrance
rects[i++] = new Rect(93, 260, 79, 5); // bottom bottom of entrance
// exit
rects[i++] = new Rect(427, 133, 80, 5); // top top of exit
rects[i++] = new Rect(426, 135, 5, 50); // top inside of exit
rects[i++] = new Rect(427, 181, 79, 5); // top corridor of exit
rects[i++] = new Rect(427, 212, 79, 5); // bottom corridor of exit
rects[i++] = new Rect(427,212, 5, 50); // bottom inside
rects[i++] = new Rect(427, 259, 79, 5); // bottom bottom of exit
// the bottom half
rects[i++] = new Rect(201, 212, 18, 52);
rects[i++] = new Rect(246, 252, 108, 12);
rects[i++] = new Rect(291, 264, 18, 40);
rects[i++] = new Rect(381, 212, 18, 51);
rects[i++] = new Rect(126, 291, 50, 12);
rects[i++] = new Rect(156, 291, 17, 52);
rects[i++] = new Rect(201, 291, 63, 12);
rects[i++] = new Rect(336, 292, 62, 12);
rects[i++] = new Rect(426, 291, 48, 12);
rects[i++] = new Rect(427, 292, 18, 51);
rects[i++] = new Rect(96, 331, 34, 12);
rects[i++] = new Rect(201, 331, 18, 40);
rects[i++] = new Rect(126, 371, 138, 10);
rects[i++] = new Rect(247, 331, 108, 12);
rects[i++] = new Rect(291, 343, 19, 39);
rects[i++] = new Rect(383, 331, 18, 40);
rects[i++] = new Rect(337, 371, 139, 13);
rects[i++] = new Rect(473, 331, 30, 12);
}
/// <summary>
/// Adds the munchables for our pacman to consume
/// </summary>
private void DrawTicTacs()
{
// define our brushes used to draw the biscuits
SolidColorBrush fillBrush = new SolidColorBrush() { Color = Colors.Yellow };
SolidColorBrush strokeBrush = new SolidColorBrush() { Color = Colors.White };
// in lines except where crosses bounds
foreach (int line in new int[] { 24, 79, 119, 275, 316, 357, 394 })
{
for (int i = 107; i < 488; i = i + 20)
{
Ellipse tt = new Ellipse()
{
Height = 5,
Width = 5,
StrokeThickness = 1,
Fill = fillBrush,
Stroke = strokeBrush
};
tt.SetValue(Canvas.TopProperty, (double)line);
tt.SetValue(Canvas.LeftProperty, (double)i);
// don't draw if it intersects a boundary
if (!CheckForCollisionAny(i, line, 10, 10)) // dont want to draw them too close to boundaries
{
GameMap.Children.Add(tt);
// and keep a reference array (no Lists in silverlight!) to check once its eaten
tictacs[GameMap.Children.Count - 1] = tt;
}
}
}
// and the downwards lines
foreach (int line in new int[] { 113, 186, 232, 279, 325, 369, 415, 489 })
{
for (int i = 24; i < 388; i = i + 20)
{
Ellipse tt = new Ellipse()
{
Height = 5,
Width = 5,
StrokeThickness = 1,
Fill = fillBrush,
Stroke = strokeBrush
};
tt.SetValue(Canvas.TopProperty, (double)i);
tt.SetValue(Canvas.LeftProperty, (double)line);
if (!CheckForCollisionAny(line, i, 10, 10)) // leave a buffer as we dont want them hard-up against walls
{
GameMap.Children.Add(tt);
// and keep a reference array (no Lists in silverlight!) to check once its eaten
tictacs[GameMap.Children.Count - 1] = tt;
}
}
}
}
/// <summary>
/// If pacman is currently on top of any tictacs, this will have him 'eat' them and update the score
/// </summary>
private void EatTicTacs()
{
foreach (Ellipse lolly in tictacs)
{
if (lolly == null)
continue;
// and ignore eaten ones
if (!GameMap.Children.Contains(lolly))
continue;
// check if pacman is on top of the biscuit
if ((((double)lolly.GetValue(Canvas.TopProperty)) > ((double)spriteY)) &&
((((double)lolly.GetValue(Canvas.TopProperty)) + 5) < ((double)spriteY + pacman.Height)) &&
(((double)lolly.GetValue(Canvas.LeftProperty)) > ((double)spriteX)) &&
((((double)lolly.GetValue(Canvas.LeftProperty)) + 5) < ((double)spriteX + pacman.Width)))
{
// eat it!
GameMap.Children.Remove(lolly);
// and update the score
int tmp = int.Parse(score.Text.TrimStart('0'));
tmp = tmp + 5;
score.Text = tmp.ToString().PadLeft(8, '0');
}
}
}
/// <summary>
/// Adds the bad guys
/// </summary>
private void PlaceMonsters()
{
const int numMonsters = 4;
monsters = new Image[numMonsters];
// make it easier to reposition our bad guys
double[] monsterX = new double[numMonsters] { 18, 387, 387, 18 };
double[] monsterY = new double[numMonsters] { 478, 480, 103, 103 };
for (int i = 0; i < numMonsters; i++)
{
monsters[i] = new Image() { Source = new BitmapImage(new Uri("monster_hunter.png", UriKind.Relative)), Width = 18 };
monsters[i].SetValue(Canvas.TopProperty, monsterX[i]);
monsters[i].SetValue(Canvas.LeftProperty, monsterY[i]);
GameMap.Children.Add(monsters[i]);
}
}
}
}