RetroCommand

Sign in to queue

Description

In this article, I will discuss how I have built RetroCommand, a Silverlight 2.0 Beta 1 version of a classic arcade game (play here).  I will explain how I used Expression Blend along with some of the new and interesting Silverlight 2.0 and WCF features to create a fun and entertaining game that will have us all thinking it was 1980 again!

I have recently updated the game for Silverlight 2.0 Beta 2.  If you would like to access that version of the game you can (here). 

Gary Farr
Coding4Fun at Clarity     

Difficulty: Intermediate
Time Required: Greater than 10 hours
Cost: Free
Software: Visual Studio 2008, Silverlight 2.0 Beta 1, Expressions Blend 2.5 March Preview 
Hardware:
Download: Download

Introduction

Ah video games!  Growing up I've played an array of gaming consoles.  I started with the Atari 2600 and followed with Sega, Sega Genesis, Playstation, Dreamcast, Playstation 2, Gamecube, XBOX, and now the Wii.  This of course includes countless hours of computer and online gaming.  Well, I have recently been developing WPF applications and wanted to do something fun with Silverlight since this technology opens so many new doors for web development on the Microsoft platform.  Being an avid game player, web based games struck a chord with me and I quickly decided my first Silverlight app would be a game.  I started researching. 

There is a lot of information out there about game programming in Silverlight.  Mike Snow has some great introductory blog posts.  Silverlight Games 101 is also a good source for game development.  Of course Silverlight.Net has been posting some great gaming examples since the release of Silverlight 1.0.  I also have to give some props to my co-worker Justin Petersen, whose Silverlight 8-Ball game helped me actually get started. 

Once the research began, the next step was deciding what video game I should write.  I decided to go old school, for several reasons.  First, I was somewhat afraid of the design required with newer games.  I look back now and would love to give the game a Silverlight face lift (RetroCommand 2.0!) but at the time I was still fairly new to this development and didn't want to worry about design features.  Second, this is my mother's favorite game, and as I kid I remembered how she would always play it.  It just seemed like a fun choice.  The next step is development!  I will now go through the steps I took to create the gaming code as well as some of the technologies I used.

Application Startup

I have two projects my game code, the first being the Web Application hosting my WCF service called RetroCommand_Web, the second my Silverlight client side code called RetroCommand. 

RetroCommand_Web

I created a WCF service to retain overall high scores in a central location.  This service saves the top 10 high scores to a local file on the host server, as well as pushes these high scores to each user playing the game.  One of the cool things this enables me to do is to keep a total count of how many people have played my game.  The setup of my WCF service is quite simple.  I followed a great video tutorial by the Swiss MSDN Team that gives step by step directions on how to set up a WCF service with Silverlight. 

In my WCF service I have two methods: ReturnHighScores and SaveHighScores.  ReturnHighScores retunrs the current high scores and SaveHighScores persists the high scores. 

C#

   1: public HighScore ReturnHighScores()
   2: {
   3:  
   4:     // Deserialization
   5:     HighScore highScore = null;
   6:     try
   7:     {
   8:         XmlSerializer s = new XmlSerializer(typeof(HighScore));
   9:  
  10:         TextReader r = new StreamReader(HttpRuntime.AppDomainAppPath + "\\HighScores.XML");
  11:         highScore = (HighScore)s.Deserialize(r);
  12:         r.Close();
  13:     }
  14:     catch (Exception ex)
  15:     {
  16:         Console.WriteLine("Error opening file!");
  17:     }
  18:  
  19:     return highScore; 
  20: }
  21:  
  22: public void SaveHighScores(HighScore highScore)
  23: {
  24:     try
  25:     {
  26:         XmlSerializer s = new XmlSerializer(typeof(HighScore));
  27:         TextWriter w = new StreamWriter(HttpRuntime.AppDomainAppPath + "\\HighScores.XML");
  28:         s.Serialize(w, highScore);
  29:         w.Close();
  30:     }
  31:     catch (Exception ex)
  32:     {
  33:         throw;
  34:     }
  35: }

VB

   1: Function ReturnHighScores() As HighScore Implements IService1.ReturnHighScores
   2:     Dim _highScore As HighScore = Nothing
   3:  
   4:     Try
   5:         Dim s As XmlSerializer = New XmlSerializer(GetType(HighScore))
   6:  
   7:         Dim r As TextReader = New StreamReader(HttpRuntime.AppDomainAppPath & "\\HighScores.XML")
   8:         _highScore = s.Deserialize(r)
   9:  
  10:         r.Close()
  11:  
  12:     Catch ex As Exception
  13:  
  14:     End Try
  15:  
  16:     Return _highScore
  17:  
  18: End Function
  19:  
  20: Sub SaveHighScores(ByRef _highScore As HighScore) Implements IService1.SaveHighScores
  21:     Try
  22:         Dim s As XmlSerializer = New XmlSerializer(GetType(HighScore))
  23:  
  24:         Dim w As System.IO.TextWriter = New StreamWriter(HttpRuntime.AppDomainAppPath & "\\HighScores.XML")
  25:         s.Serialize(w, _highScore)
  26:         w.Close()
  27:     Catch ex As Exception
  28:  
  29:     End Try
  30: End Sub

RetroCommand

The following section describes the code used to create the Silverlight portion of my application.  It will detail initial Expression Blend setup, WCF communication, the gaming loop, missile line creation, and collision testing. 

Expression Blend

I used Expression Blend to create initial objects on my game screen.  Such objects include missiles, images, cities, clouds, missile lines, and hit test regions.  Below are brief descriptions of the user controls I created using Expression Blend.

The Page.xaml is my main user control which contains the definitions of all UI Elements that are part of the game play.  For example, all missiles are an instance of a missile user control.  Also, all cities are an instance of a city user control.

The missileLine.xaml is the Silverlight user control I use for all attack line and defense line instances.  During game play, missiles attack your cities and firing bases.  You also have the ability to shoot the missiles by firing defense missiles from your bases. 

The cloud.xaml is the Silverlight user control that pops up a cloud image and animation when an object is hit. 

The remaining user controls are GameOver.xaml, LevelBreak.xaml, Startup.xaml, and CrossCursor.xaml.  The first three controls are for non game play messaging and the Cross Cursor is the mouse display.

All user controls have fade in and fade out animations.  These animations help make the game play smooth.  Examples of this fade in and fade out code are below

   1: <Storyboard x:Name="FadeIn" Completed="FadeIn_Completed">
   2:     <DoubleAnimationUsingKeyFrames Storyboard.TargetName="GameOverScreen" Storyboard.TargetProperty="(UIElement.Opacity)" BeginTime="00:00:00">
   3:         <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
   4:         <SplineDoubleKeyFrame KeyTime="00:00:01" Value="1"/>
   5:     </DoubleAnimationUsingKeyFrames>
   6: </Storyboard>
   7: <Storyboard x:Name="FadeOut" Completed="FadeOut_Completed">
   8:     <DoubleAnimationUsingKeyFrames Storyboard.TargetName="GameOverScreen" Storyboard.TargetProperty="(UIElement.Opacity)" BeginTime="00:00:00">
   9:         <SplineDoubleKeyFrame KeyTime="00:00:00" Value="1"/>
  10:         <SplineDoubleKeyFrame KeyTime="00:00:01" Value="0"/>
  11:     </DoubleAnimationUsingKeyFrames>
  12: </Storyboard>

WCF Communication

In the previous section I described a WCF service I created to communicate with and transfer high scores.  Below I will show the code within my Silverlight project that calls methods from this WCF service.  If you followed the WCF in Silverlight video from the Swiss MSDN Team described above you will have created a Service Reference within your project.  This reference can access methods and objects within your Application Web Project as shown in the example below. 

The Return High Scores Completed event retrieves High Scores from the WCF service and stores them within the Silverlight project.

C#

   1: void _client_ReturnHighScoresCompleted(object sender, MissileCommand.ServiceReference1.ReturnHighScoresCompletedEventArgs e)
   2: {
   3:     HighScores = e.Result as MissileCommand.ServiceReference1.HighScore;
   4:     _singleHighScore.Score = "0";
   5:     foreach (MissileCommand.ServiceReference1.Scores score in HighScores.Scores)
   6:     {
   7:         if (Convert.ToDouble(score.Score) > Convert.ToDouble(_singleHighScore.Score))
   8:         {
   9:             _singleHighScore = score;
  10:         }
  11:     }
  12:   
  13:     SetSingleHighScoreEvent(_singleHighScore, HighScores.GamesPlayed);
  14:  
  15: }

VB

   1: Private Sub _client_ReturnHighScoresCompleted(ByVal sender As Object, ByVal e As RetroCommandVB.ServiceReference1.ReturnHighScoresCompletedEventArgs)
   2:     HighScores = e.Result
   3:     _singleHighScore.Score = "0"
   4:     For Each score As ServiceReference1.Scores In HighScores.Scores
   5:         If Convert.ToDouble(score.Score) > Convert.ToDouble(_singleHighScore.Score) Then
   6:             _singleHighScore = score
   7:         End If
   8:     Next
   9:  
  10:     RaiseEvent SetSingleHighScoreEvent(_singleHighScore, HighScores.GamesPlayed)
  11: End Sub

The method UpdateHighScoresList sends the updated High Scores and count of games played to the WCF Service.

C#

   1: public void UpdateHighScoresList(MissileCommand.ServiceReference1.Scores newScore)
   2: {
   3:     MissileCommand.ServiceReference1.HighScore newHighScoresList = HighScores;
   4:     List<MissileCommand.ServiceReference1.Scores> scoresList = new List<MissileCommand.ServiceReference1.Scores>();
   5:      
   6:     bool newHighScore = false;
   7:     foreach (MissileCommand.ServiceReference1.Scores score in HighScores.Scores)
   8:     {
   9:         if (Convert.ToDouble(newScore.Score) > Convert.ToDouble(score.Score) && !newHighScore)
  10:         {
  11:             scoresList.Add(newScore);
  12:             scoresList.Add(score);
  13:             newHighScore = true;
  14:         }
  15:         else
  16:         {
  17:           scoresList.Add(score);
  18:         }
  19:     }
  20:  
  21:     newHighScoresList.GamesPlayed = (Convert.ToDouble(newHighScoresList.GamesPlayed) + 1).ToString();
  22:     newHighScoresList.Scores[0] = scoresList[0];
  23:     newHighScoresList.Scores[1] = scoresList[1];
  24:     newHighScoresList.Scores[2] = scoresList[2];
  25:     newHighScoresList.Scores[3] = scoresList[3];
  26:     newHighScoresList.Scores[4] = scoresList[4];
  27:     newHighScoresList.Scores[5] = scoresList[5];
  28:     newHighScoresList.Scores[6] = scoresList[6];
  29:     newHighScoresList.Scores[7] = scoresList[7];
  30:     newHighScoresList.Scores[8] = scoresList[8];
  31:     newHighScoresList.Scores[9] = scoresList[9];
  32:  
  33:     HighScores = newHighScoresList;
  34:     _client.SaveHighScoresAsync(newHighScoresList);
  35:    
  36: }

VB

   1: Public Sub UpdateHighScoresList(ByVal newScore As ServiceReference1.Scores)
   2:     Dim newHighScoreList As ServiceReference1.HighScores = HighScores()
   3:     Dim scoresList As List(Of ServiceReference1.Scores) = New List(Of ServiceReference1.Scores)()
   4:  
   5:     Dim newHighScore As Boolean = False
   6:  
   7:     For Each score As ServiceReference1.Scores In HighScores.Scores
   8:         If Convert.ToDouble(newScore.Score) > Convert.ToDouble(score.Score) And Not newHighScore Then
   9:             scoresList.Add(newScore)
  10:             scoresList.Add(score)
  11:             newHighScore = True
  12:         Else
  13:             scoresList.Add(score)
  14:         End If
  15:     Next
  16:  
  17:     newHighScoreList.GamesPlayed = (Convert.ToDouble(newHighScoreList.GamesPlayed) + 1).ToString()
  18:     newHighScoreList.Scores(0) = scoresList(0)
  19:     newHighScoreList.Scores(1) = scoresList(1)
  20:     newHighScoreList.Scores(2) = scoresList(2)
  21:     newHighScoreList.Scores(3) = scoresList(3)
  22:     newHighScoreList.Scores(4) = scoresList(4)
  23:     newHighScoreList.Scores(5) = scoresList(5)
  24:     newHighScoreList.Scores(6) = scoresList(6)
  25:     newHighScoreList.Scores(7) = scoresList(7)
  26:     newHighScoreList.Scores(8) = scoresList(8)
  27:     newHighScoreList.Scores(9) = scoresList(9)
  28:  
  29:     HighScores = newHighScoreList
  30:  
  31:     _client.SaveHighScoresAsync(newHighScoreList)
  32: End Sub

Gaming Loop

Each gaming loop begins at the start of a particular level.  The gaming loop ends when the level is complete.  This is specified when all enemy missiles within a level are fired. 

C#

   1: void LevelBreakScreen_StartLevelEvent()
   2: {
   3:     meIntermission.Stop();
   4:     LayoutRoot.Children.Remove(meIntermission);
   5:     LevelBreakScreen.Visibility = Visibility.Collapsed;
   6:     GameLoop.Begin();
   7:     StartL

VB

   1: Sub LevelBreakScreen_StartLevelEvent()
   2:     meIntermission.Stop()
   3:     LayoutRoot.Children.Remove(meIntermission)
   4:     LevelBreakScreen.Visibility = Windows.Visibility.Collapsed
   5:     GameLoop.Begin()
   6:     StartLevel(_currentLevel)
   7: End Sub

The game loop repeats based on a level specified timer until all enemy attack lines have been created and destroyed. 

C#

   1: private void GameLoop_Completed(object sender, EventArgs e)
   2: {
   3:     TimeSpan timeSpan = DateTime.Now - _currentTime;
   4:     if (timeSpan.Seconds > _timeElapsed)
   5:     {
   6:         if (_currentLine < _lineCount - 1)
   7:         {
   8:             _currentLine++;
   9:             AddLine(_lineCollection[_currentLine]);
  10:             _currentTime = DateTime.Now;
  11:         }
  12:         if (_linesRemoved == _lineCollection.Count)
  13:         {
  14:             _isRunning = false;
  15:  
  16:         }
  17:  
  18:     }
  19:  
  20:  
  21:     if (_isRunning)
  22:     {
  23:         GameLoop.Begin();
  24:     }
  25:  
  26:  
  27:  
  28: }

VB

   1: Private Sub GameLoop_Completed(ByVal sender As Object, ByVal e As EventArgs)
   2:     Dim timeSpan As TimeSpan = DateTime.Now - _currentTime
   3:  
   4:     If timeSpan.Seconds > _timeElapsed Then
   5:         If _currentLine < _lineCount - 1 Then
   6:             _currentLine = _currentLine + 1
   7:             AddLine(_lineCollection(_currentLine))
   8:             _currentTime = DateTime.Now
   9:         End If
  10:         If _linesRemoved = _lineCollection.Count Then
  11:             _isRunning = False
  12:         End If
  13:     End If
  14:  
  15:     If _isRunning Then
  16:         GameLoop.Begin()
  17:     End If
  18: End Sub

Missile Attack and Defense Lines

Missile attack lines are created during game play.  At the start of a level, instances of all the attack lines within the level are created.  These lines have parameters passed in that determine the starting point, angle, and speed of the attack line.  These lines are drawn to the screen during the game loop in a method called AddLine (see code snippet above). 

Missile defense lines are created when a user clicks either a, s, or d on the keyboard.  The defensive lines are handled in the same manor as attack lines except the end point and angle are determined by the mouse point position at the time of firing.  I also added some interesting sound effects for user enjoyment. 

Collision Detection

Collision detection was probably the most enjoyable part of developing this game.  The reason for this is that in Silverlight 2.0 beta there is a nice little method called HitTest that analyzes two objects and determines if they collide.  Each line has its own storyboard which continuously looks for collisions with other objects.  When a line collides with an object that information is stored for future analysis and the line is destroyed.  Attack lines run until they collide with an object such as a cloud, city, missile base, or the ground.  Defensive lines have a fixed destination and when they reach that destination a cloud is created.  The cloud has an area and if any attack lines collide with that area they of course are destroyed.  The cloud itself has a time period duration in which enemy lines can collide.  It will eventually fade away.  The majority of user interaction is based on this code.

C#

   1: private void missileLineStoryboard_Completed(object sender, EventArgs e)
   2: {
   3:     //Check for collision
   4:     Page page = (this.Parent as Grid).Parent as Page;
   5:  
   6:     if (!IsMissile)
   7:     {
   8:         //check all bottom grid rectangles
   9:         if (CheckCollision(page.recBottom, misLine, page.recBottom.Name) ||
  10:             CheckCollision(page.recLeft, misLine, page.recLeft.Name) ||
  11:             CheckCollision(page.recCenter, misLine, page.recCenter.Name ) ||
  12:             CheckCollision(page.recRight, misLine, page.recRight.Name) ||
  13:             CheckCollision(page.myCloud.recCloud, misLine, page.myCloud.Name) ||
  14:             CheckCollision(page.City1.recCity, misLine, page.City1.Name) ||
  15:             CheckCollision(page.City2.recCity, misLine, page.City2.Name) ||
  16:             CheckCollision(page.City3.recCity, misLine, page.City3.Name) ||
  17:             CheckCollision(page.City4.recCity, misLine, page.City4.Name) ||
  18:             CheckCollision(page.City5.recCity, misLine, page.City5.Name) ||
  19:             CheckCollision(page.City6.recCity, misLine, page.City6.Name))
  20:         {
  21:             _isRunning = false;
  22:             DestroyLineEvent(this);        
  23:         }
  24:  
  25:         if (_isRunning)
  26:         {
  27:             missileLineAnimationX.From = misLine.X2;
  28:             missileLineAnimationY.From = misLine.Y2;
  29:             missileLineStoryboard.Duration = _lineDuration;
  30:             misLine.Y2 = _currentY2Position + Y2Increment;
  31:             misLine.X2 = ((misLine.Y2 - misLine.Y1) / Math.Tan(_angle)) + misLine.X1;
  32:             missileLineAnimationX.To = misLine.X2;
  33:             missileLineAnimationY.To = misLine.Y2;
  34:             _currentY2Position = misLine.Y2;
  35:  
  36:             missileLineStoryboard.Begin();
  37:         }
  38:     }
  39:     else
  40:     {
  41:         _isRunning = false;
  42:         DestroyLineEvent(this);
  43:     }
  44: }
  45:  
  46: private bool CheckCollision(Rectangle control1, Line control2, string control1Name)
  47: {
  48:     bool bCollision = false;
  49:  
  50:     //check for previous cloud location, if not visible ignore hit
  51:    
  52:     Point ptCheck = new Point();
  53:     ptCheck.X = control2.X2;
  54:     ptCheck.Y = control2.Y2;
  55:     List<UIElement> hits1 = (List<UIElement>)control1.HitTest(ptCheck);
  56:     if (hits1.Count > 0)
  57:     {
  58:        
  59:         ControlName = control1Name;
  60:         bCollision = true;
  61:     }
  62:     return bCollision;
  63: }

VB

   1: Private Sub missileLineStoryboard_Completed(ByVal sender As System.Object, ByVal e As System.EventArgs)
   2:     Dim page As Page = DirectCast(DirectCast(Me.Parent, Grid).Parent, Page)
   3:  
   4:     If Not IsMissile Then
   5:         If CheckCollision(page.recBottom, misLine, page.recBottom.Name) Or _
   6:             CheckCollision(page.recLeft, misLine, page.recLeft.Name) Or _
   7:             CheckCollision(page.recCenter, misLine, page.recCenter.Name) Or _
   8:             CheckCollision(page.recRight, misLine, page.recRight.Name) Or _
   9:             CheckCollision(page.myCloud.recCloud, misLine, page.myCloud.Name) Or _
  10:             CheckCollision(page.City1.recCity, misLine, page.City1.Name) Or _
  11:             CheckCollision(page.City2.recCity, misLine, page.City2.Name) Or _
  12:             CheckCollision(page.City3.recCity, misLine, page.City3.Name) Or _
  13:             CheckCollision(page.City4.recCity, misLine, page.City4.Name) Or _
  14:             CheckCollision(page.City5.recCity, misLine, page.City5.Name) Or _
  15:             CheckCollision(page.City6.recCity, misLine, page.City6.Name) Then
  16:  
  17:             _isRunning = False
  18:             RaiseEvent DestroyLineEvent(Me)
  19:  
  20:         End If
  21:  
  22:         If _isRunning Then
  23:             missileLineAnimationX.From = misLine.X2
  24:             missileLineAnimationY.From = misLine.Y2
  25:             missileLineStoryboard.Duration = _lineDuration
  26:             misLine.Y2 = _currentY2Position + Y2Increment
  27:             misLine.X2 = ((misLine.Y2 - misLine.Y1) / Math.Tan(_angle)) + misLine.X1
  28:             missileLineAnimationX.To = misLine.X2
  29:             missileLineAnimationY.To = misLine.Y2
  30:             _currentY2Position = misLine.Y2
  31:  
  32:             missileLineStoryboard.Begin()
  33:         End If
  34:     Else
  35:         _isRunning = False
  36:         RaiseEvent DestroyLineEvent(Me)
  37:     End If
  38: End Sub
  39:  
  40: Private Function CheckCollision(ByVal control1 As Rectangle, ByVal control2 As Line, ByVal control1Name As String) As Boolean
  41:     Dim bCollision As Boolean = False
  42:  
  43:     Dim ptCheck As Point = New Point()
  44:     ptCheck.X = control2.X2
  45:     ptCheck.Y = control2.Y2
  46:     Dim hits1 As List(Of UIElement) = DirectCast(control1.HitTest(ptCheck), List(Of UIElement))
  47:     If hits1.Count > 0 Then
  48:         ControlName = control1Name
  49:         bCollision = True
  50:     End If
  51:  
  52:     Return bCollision
  53: End Function

Scoring and Game Play

Scoring for game play was kind of fun to create.  Default scoring involves points for destroyed attack lines, remaining missiles and remaining cities.  If you destroy multiple attacks lines with one missile shot you will get extra points.  So basically scoring is not fixed, you have the ability to get a different score each time depending on how you destroy attack lines.  As levels progress, the game play gets harder.  At first this game is meant to be easy, but in the higher levels it can get quite difficult.  The last level, level 10, is extremely hard.  Kudos to anyone who can beat it!

Conclusion

The finished product is not only something I can use as a fantastic learning experience, but is something I can enjoy playing myself.  It has definitely surpassed my goals and expectations.  I used new technologies such as WCF and Silverlight and created a game that is fun.  I knew this game was successful when my fellow co-workers spent hours trying to get the highest score during my debugging phase.  Thanks Spencer and Justin!  I've already started thinking of my next gaming application!

If you have any questions about this solution or have suggestions for improvement, feel free to contact me at gary.farr@claritycon.com

The Discussion

Comments closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to send us feedback you can Contact Us.