David Kennedy’s Tech Ramblings

Just another WordPress.com weblog

Silverlight Adventures Part 1 July 28, 2008

Filed under: Silverlight Tutorials — dotnetdave @ 12:02 am
Tags: , , ,

The Setup

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)

http://www.microsoft.com/expression/try-it/default.aspx

You will also need to install the Silverlight Tools Beta 2 for VS 2008 in order to edit Silverlight projects & codefiles in VS:

http://www.microsoft.com/downloads/details.aspx?FamilyId=50A9EC01-267B-4521-B7D7-C0DBA8866434&displaylang=en

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:

http://www.ddjsilverlight.com/tutorial/section1_3.asp
http://www.c-sharpcorner.com/UploadFile/nipuntomar/DataBindingSilverlightWCFService07152008011652AM/DataBindingSilverlightWCFService.aspx

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.

The Game

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.

The xaml for this is very simple, as follows:

<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="SilverlightPacman.Page" Width="590" Height="433" KeyDown="UserControl_KeyDown"> <Canvas x:Name="GameMap" Background="black"> <Image Source="pacman_gamemap.png"/> <TextBlock Height="22.31" Width="62" Foreground="White" x:Name="pacPosition" Canvas.Left="510" Canvas.Top="396.11" Text="00" TextWrapping="Wrap" RenderTransformOrigin="1.95200002193451,-19.9109992980957"/> <Image Source="pacman.png" x:Name="pacman" Height="15" Width="15"/> <TextBlock Height="22.31" Width="62" Foreground="White" x:Name="pacCollision" Text="none" TextWrapping="Wrap" Canvas.Left="510" Canvas.Top="300"/> <TextBlock x:Name="score" Height="21" Width="82" Canvas.Top="60" Canvas.Left="10" Text="00000001" Foreground="White" TextWrapping="Wrap"/> </Canvas> </UserControl>

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.

The Code

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]); } } } }
Advertisements
 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s