February, 2011

Posts
  • File → New Project

    Tweaking and Refactoring (TriangleShooter)

    • 0 Comments

    This week, we’ve got a good base for the TriangleShooter game. It shoots, scores, persists high score, has audio, it really gives you something to work off of. To make it easier to work from, this time around, we’re going to tweak a few things and refactor the source to be more readable and modifiable, which will result in a code base you can extend on your own to improve or re-imagine the game. There are still a few things I want to do to the game, as well, and this will get us in a good place to begin doing so. So let’s get started. You can prepare by grabbing the source code from last week over at the SkyDrive share.

    Now before we get to the refactoring, there is one thing that I need to change. The player had been starting at (0,0), the top left of the play space, which was okay most of the time, but could get you killed if an enemy decided to spawn there. Since the enemies spawn around the edges, I want to start by moving the starting location to somewhere away from the edge, to give you a fighting chance at doing better in the game. To do this, we’ll update the initialization of the player in the NewGame function to the following:

    Player NewGame Initialization
    1. player = new Player() { Avatar = txPlayer, Position = new Vector2(100f, 240f), Rotation = 0f };

    Now we’re ready to start refactoring. The big thing we’re going to do here is move related blocks of code out into methods that we’ll call. If you look through the Update method, there are some blocks of code that can easily be pulled out to distinct methods. We’ll end up getting the following methods:

    Update Methods
    1. UpdatePlayer();
    2.  
    3. UpdateEnemies();
    4.  
    5. SpawnEnemies(gameTime);
    6.  
    7. ShootBullets(gameTime);
    8.  
    9. UpdateBullets();

    The source is a simple copy/paste out of Update, giving us the following method bodies:

    UpdatePlayer
    1. private void UpdatePlayer()
    2. {
    3.     foreach (TouchLocation tl in TouchPanel.GetState())
    4.     {
    5.         if (tl.State == TouchLocationState.Pressed)
    6.         {
    7.             if (movementId == 0)
    8.             {
    9.                 movementId = tl.Id;
    10.             }
    11.         }
    12.  
    13.         if (tl.Id == movementId)
    14.         {
    15.             Vector2 direction = tl.Position - player.Position;
    16.  
    17.             if (direction.LengthSquared() > 100)
    18.             {
    19.                 direction.Normalize();
    20.  
    21.                 player.Rotation = (float)(Math.Atan2(direction.Y, direction.X));
    22.  
    23.                 player.Position += direction * 10;
    24.             }
    25.         }
    26.  
    27.         if (tl.State == TouchLocationState.Released)
    28.         {
    29.             if (tl.Id == movementId)
    30.             {
    31.                 movementId = 0;
    32.             }
    33.         }
    34.     }
    35. }
    UpdateEnemies
    1. private void UpdateEnemies()
    2. {
    3.     foreach (Enemy enemy in enemies)
    4.     {
    5.         enemy.Rotation += ((float)Math.PI / 30f) % ((float)Math.PI * 2);
    6.  
    7.         Vector2 enemyDirection = player.Position - enemy.Position;
    8.  
    9.         if (enemyDirection.LengthSquared() > enemy.Speed * enemy.Speed)
    10.         {
    11.             enemyDirection.Normalize();
    12.  
    13.             enemy.Position += enemyDirection * enemy.Speed;
    14.         }
    15.  
    16.         if (new Rectangle((int)enemy.Position.X - enemy.Avatar.Width / 2, (int)enemy.Position.Y - enemy.Avatar.Height / 2, enemy.Avatar.Width, enemy.Avatar.Height).Contains((int)player.Position.X, (int)player.Position.Y))
    17.         {
    18.             triangleColor = Color.Red;
    19.             isPlayerDead = true;
    20.             SaveToFile();
    21.         }
    22.     }
    23. }
    SpawnEnemies
    1. private void SpawnEnemies(GameTime gameTime)
    2. {
    3.     timeToSpawn -= gameTime.ElapsedGameTime;
    4.  
    5.     if (timeToSpawn < TimeSpan.Zero)
    6.     {
    7.         Vector2 spawnPosition;
    8.         switch (random.Next(4))
    9.         {
    10.             case 0:
    11.                 spawnPosition = new Vector2(0, random.Next(480));
    12.                 break;
    13.             case 1:
    14.                 spawnPosition = new Vector2(800, random.Next(480));
    15.                 break;
    16.             case 2:
    17.                 spawnPosition = new Vector2(random.Next(800), 0);
    18.                 break;
    19.             case 3:
    20.                 spawnPosition = new Vector2(random.Next(800), 480);
    21.                 break;
    22.             default:
    23.                 spawnPosition = Vector2.Zero;
    24.                 break;
    25.         }
    26.  
    27.         switch (random.Next(4))
    28.         {
    29.             case 0:
    30.                 enemies.Add(new Enemy() { Position = spawnPosition, Rotation = 0f, Avatar = txEnemy3, Speed = 3f });
    31.                 break;
    32.             case 1:
    33.                 enemies.Add(new Enemy() { Position = spawnPosition, Rotation = 0f, Avatar = txEnemy4, Speed = 3.5f });
    34.                 break;
    35.             case 2:
    36.                 enemies.Add(new Enemy() { Position = spawnPosition, Rotation = 0f, Avatar = txEnemy5, Speed = 4f });
    37.                 break;
    38.             case 3:
    39.                 enemies.Add(new Enemy() { Position = spawnPosition, Rotation = 0f, Avatar = txEnemy6, Speed = 4.5f });
    40.                 break;
    41.             default:
    42.                 break;
    43.         }
    44.  
    45.         timeToSpawn = new TimeSpan(0, 0, 0, 0, random.Next(500) + 250);
    46.     }
    47. }
    ShootBullets
    1. private void ShootBullets(GameTime gameTime)
    2. {
    3.     timeToShoot -= gameTime.ElapsedGameTime;
    4.  
    5.     if (timeToShoot <= TimeSpan.Zero)
    6.     {
    7.         timeToShoot = new TimeSpan(0, 0, 0, 0, ShootDelay);
    8.         bullets.Add(new Bullet() { Avatar = txBullet, Position = new Vector2(player.Position.X + player.Avatar.Width * (float)Math.Cos(player.Rotation), player.Position.Y + player.Avatar.Height * (float)Math.Sin(player.Rotation)), Rotation = player.Rotation, Speed = 15f });
    9.         fire.Play(.5f, 0f, 0f);
    10.     }            
    11. }

    UpdateBullets
    1. private void UpdateBullets()
    2. {
    3.     foreach (Bullet b in bullets.ToList())
    4.     {
    5.         b.Position = new Vector2(b.Position.X + b.Speed * (float)Math.Cos(b.Rotation), b.Position.Y + b.Speed * (float)Math.Sin(b.Rotation));
    6.  
    7.         if (!graphics.GraphicsDevice.Viewport.Bounds.Contains(new Point((int)b.Position.X, (int)b.Position.Y)))
    8.         {
    9.             bullets.Remove(b);
    10.         }
    11.         else
    12.         {
    13.             foreach (Enemy enemy in enemies.ToList())
    14.             {
    15.                 if (new Rectangle((int)enemy.Position.X - enemy.Avatar.Width / 2, (int)enemy.Position.Y - enemy.Avatar.Height / 2, enemy.Avatar.Width, enemy.Avatar.Height).Contains((int)b.Position.X, (int)b.Position.Y))
    16.                 {
    17.                     bullets.Remove(b);
    18.                     enemies.Remove(enemy);
    19.                     enemyHit.Play();
    20.                     score++;
    21.                     if (score > highScore)
    22.                     {
    23.                         highScore = score;
    24.                     }
    25.                     break;
    26.                 }
    27.             }
    28.         }
    29.     }
    30. }

    This makes things easier to update because we won’t be mixing code that doesn’t belong in other blocks. For example, next week, I’ll be adding in code to support the accelerometer, so I can just change up the UpdatePlayer method, and nothing else needs changed!

    Download the latest version of the source code.

  • File → New Project

    Adding Sound Effects (TriangleShooter)

    • 0 Comments

    This week, we’re going to liven the game up a bit by adding sound effects. While I am pretty amazing at drawing triangles and other figures that you can create without any artistic skills, I have even less skills making sounds. Luckily, there are some places we can get some free sounds to use in our game. We’ll be using a collection of sounds from the AppHub’s SoundLab project, so you can grab that, and pick up the source code from last week from the SkyDrive share.

    This one is going to actually be pretty easy. To start with, we’ll grab the sounds from the SoundLab project, and pick a couple that we think might work for the game. You could go with some gun sounds if you are feeling in a more aggressive mood, but I feel that some of the sounds in the UI folder actually work better for the feel of the game. I picked UI_Misc13.wav for the sound of a bullet hitting an enemy, and UI_Misc17.wav for the shooting sound. So the first thing we need to do is grab the files we picked, and drop them into the TriangleShooterContent project. Once they are added in, we can set up a variable for them, load them from the content project, and then play them.

    The variables are of type SoundEffect, so we’ll set one up for firing, and the other for when the enemy gets hit.

    SoundEffect Variables
    1. SoundEffect fire;
    2. SoundEffect enemyHit;

    Loading the content is straightforward and similar to the previous loading of content we’ve done.

    LoadContent
    1. fire = Content.Load<SoundEffect>("UI_Misc17");
    2. enemyHit = Content.Load<SoundEffect>("UI_Misc13");

    And playing them is a simple matter as well. The collision sound is good how it is, so we don’t need to tweak it at all. We can just use the default method signature with no arguments.

    Code Snippet
    1. if (new Rectangle((int)enemy.Position.X - enemy.Avatar.Width / 2, (int)enemy.Position.Y - enemy.Avatar.Height / 2, enemy.Avatar.Width, enemy.Avatar.Height).Contains((int)b.Position.X, (int)b.Position.Y))
    2. {
    3.     bullets.Remove(b);
    4.     enemies.Remove(enemy);
    5.     enemyHit.Play();
    6.     score++;
    7.     if (score > highScore)
    8.     {
    9.         highScore = score;
    10.     }
    11.     break;
    12. }

    The firing sound effect is a bit loud for how often it happens, so we use the overload that allows you to define the volume, pitch modification, and panning. about 50% volume seems good.

    Shooting with a sound effect
    1. if (timeToShoot <= TimeSpan.Zero)
    2. {
    3.     timeToShoot = new TimeSpan(0, 0, 0, 0, ShootDelay);
    4.     bullets.Add(new Bullet() { Avatar = txBullet, Position = new Vector2(player.Position.X + player.Avatar.Width * (float)Math.Cos(player.Rotation), player.Position.Y + player.Avatar.Height * (float)Math.Sin(player.Rotation)), Rotation = player.Rotation, Speed = 15f });
    5.     fire.Play(.5f, 0f, 0f);
    6. }

    And really, that’s it. Well that was easy. Next week, we’ll refactor the code to make it easier to read and make some tweaks to the gameplay.

    Download the latest version of the source code.

  • File → New Project

    Persisting the High Score Between Games (TriangleShooter)

    • 0 Comments

    Last week, we added scoring, and keeping track of the high score over a set of games. The problem is, when you exit the game for any reason, you lose your high score. That’s because everything is unloaded from memory, so when you start your game over, it has nothing to start from. This week, we’ll save your high score to a file, so we can load it back up when you restart your game. To begin, we’ll load the code from last week over at the SkyDrive Share.

    When working with files, we’ll need to add a few libraries. On the Windows Phone, applications have access to a special type of storage called IsolatedStorage, which is a file store that is accessible only by your application. Additionally, we will be storing our high score in an XML format, and accessing it with LINQ. To work with these, we’ll the following three include statements:

    Using Statements
    1. using System.IO;
    2. using System.IO.IsolatedStorage;
    3. using System.Xml.Linq;

    Once we’ve got these, we’ll just need to set up two simple methods. We simply need to save to the file, and load from the file. We’ll then just need to update the game to call the load method when it starts, and the save method at the end of each game.

    SaveToFile
    1. public void SaveToFile()
    2. {
    3.     string fileName = "SavedState.xml";
    4.  
    5.     using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
    6.     {
    7.         using (IsolatedStorageFileStream writeStream = new IsolatedStorageFileStream(fileName, FileMode.Create, store))
    8.         {
    9.             using (StreamWriter writer = new StreamWriter(writeStream))
    10.             {
    11.                 XDocument doc = new XDocument(
    12.                     new XDeclaration("1.0", "utf-8", "yes"),
    13.                     new XElement("State",
    14.                         new XElement("HighScore", highScore.ToString())
    15.                         )
    16.                      );
    17.  
    18.                 writer.Write(doc.ToString());
    19.             }
    20.         }
    21.     }
    22. }

    At the beginning of the SaveToFile method, we choose a file name to save the game state. You then open the Isolated Storage and open a file stream into it. We use FileMode.Create because we just want to overwrite a file if it’s there, and make a new one if it isn’t. The StreamWriter lets us write directly into the file we created in Isolated Storage. We then create a basic XML file that contains the high score inside of a State element.

    LoadFromFile
    1. public void LoadFromFile()
    2. {
    3.     string fileName = "SavedState.xml";
    4.  
    5.     try
    6.     {
    7.         using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
    8.         {
    9.             using (IsolatedStorageFileStream readStream = new IsolatedStorageFileStream(fileName, FileMode.Open, store))
    10.             {
    11.                 using (StreamReader reader = new StreamReader(readStream))
    12.                 {
    13.                     string stateXml = reader.ReadToEnd();
    14.  
    15.                     XDocument doc = XDocument.Parse(stateXml);
    16.  
    17.                     var q = from c in doc.Descendants("State")
    18.                             select (string)c.Element("HighScore");
    19.                     
    20.                     int i = 0;
    21.  
    22.                     foreach (string s in q)
    23.                     {
    24.                         if (int.TryParse(s, out i))
    25.                         {
    26.                             highScore = i;
    27.                         }
    28.                     }
    29.                 }
    30.             }
    31.         }
    32.     }
    33.     catch (IsolatedStorageException)
    34.     {
    35.     }
    36. }

    To load from the file, we use the same file name we defined when we saved our state. We then open the Isolated Storage using a StreamReader, and load its contents into a XDocument. This lets us use LINQ to pull in the elements that we saved. We walk through the elements named HighScore, and parse its contents into the highScore variable. The whole thing is wrapped in a try catch block because the file might not be there, or maybe there is something wrong with the file. We don’t want that to crash the game, so we just let it go.

    Now we need to use these methods. First of all, we just need to load in the file in the Initalize method.

    Initialize
    1. protected override void Initialize()
    2. {
    3.     random = new Random();
    4.  
    5.     highScore = 0;
    6.  
    7.     LoadFromFile();
    8.  
    9.     base.Initialize();
    10. }

    And finally, we need to save it. You could put it in every time the high score is beaten, but file I/O is expensive, so this could cause performance problems. Since we are really storing game high scores, why not drop it into the update method when the player dies.

    Code Snippet
    1. if (new Rectangle((int)enemy.Position.X - enemy.Avatar.Width / 2, (int)enemy.Position.Y - enemy.Avatar.Height / 2, enemy.Avatar.Width, enemy.Avatar.Height).Contains((int)player.Position.X, (int)player.Position.Y))
    2. {
    3.     triangleColor = Color.Red;
    4.     isPlayerDead = true;
    5.     SaveToFile();
    6. }

    And there you go, your high score is saved across application launches. Remember that if you restart the emulator, your isolated storage will be blown away, so you’ll lose your high score. This will also happen in you uninstall the game and then redeploy.

    Next week, we’ll be adding sound effects, so stay tuned.

    Download the latest version of the source code.

  • File → New Project

    Scoring (TriangleShooter)

    • 0 Comments

    Time to add scoring! To add a competitive edge to the game, let’s add in a running total of how many enemies you’ve shot. We’ll also keep track of the high score across games so you can try to beat your last score. To do this, we’ll make use of a Sprite Font. A Sprite Font is a font specially prepared by taking a set of characters and converting them to textures. So let’s get started. To begin, we’ll want to grab the code from last week over at the SkyDrive share.

    The first thing that we’ll want to do is create a new Sprite Font. Right-click on the TriangleShooterContent project, and choose Sprite Font. Under name, type “Font.spritefont”, and then click Add. It will open in the editor, in all it’s XML glory. As you can see by looking at the file, it describes a specific version of a font. You define the Font’s name, size, spacing, whether to use kerning or not, the style, a default character, and the portion of the font to include. The document is well commented, and is easy enough to understand. The one thing that might catch you up is the CharacterRegions. Because games require a lot of efficiency, you want to minimize any overhead possible. This includes characters from a font you wouldn’t use. This is especially important when you want to use non-Latin characters, such as Chinese or Japanese, because they won’t be included in the default Character Region. If you’re going to be using a lot of non-Latin characters, I suggest you take a look at the Localization Sample on AppHub.

    We’ll change two things. First, change the FontName field to Kootenay. Second, change the Size field to 32. This will give us a readable font for the phone.

    Next, we’ll need to set up our variables. We’ll be counting the number of enemies you shoot, so well use an integer named score, and an integer named highScore to keep track of the record. We’ll also need a variable to hold our font.

    Variable Declaration
    1. int score;
    2. int highScore;
    3.  
    4. SpriteFont font;

    We’ll initialize the highScore in the Initialize method

    Initialize
    1. highScore = 0;

    And the score in the NewGame method.

    NewGame
    1. score = 0;

    Then load up the font in the LoadContent method

    LoadContent
    1. font = Content.Load<SpriteFont>("Font");

    Now we’re ready to start keeping score. Inside of the Update method, where we do the collision for bullets, we add a few lines to a successful collision. We increment the score, then check if the score is greater than the high score. If that’s the case, we update the high score to equal the current score. All together, the updated bullet collision detection part of the Update method looks like the following

    Bullet Collision
    1. foreach (Bullet b in bullets.ToList())
    2. {
    3.     b.Position = new Vector2(b.Position.X + b.Speed * (float)Math.Cos(b.Rotation), b.Position.Y + b.Speed * (float)Math.Sin(b.Rotation));
    4.  
    5.     if (!graphics.GraphicsDevice.Viewport.Bounds.Contains(new Point((int)b.Position.X, (int)b.Position.Y)))
    6.     {
    7.         bullets.Remove(b);
    8.     }
    9.     else
    10.     {
    11.         foreach (Enemy enemy in enemies.ToList())
    12.         {
    13.             if (new Rectangle((int)enemy.Position.X - enemy.Avatar.Width / 2, (int)enemy.Position.Y - enemy.Avatar.Height / 2, enemy.Avatar.Width, enemy.Avatar.Height).Contains((int)b.Position.X, (int)b.Position.Y))
    14.             {
    15.                 bullets.Remove(b);
    16.                 enemies.Remove(enemy);
    17.                 score++;
    18.                 if (score > highScore)
    19.                 {
    20.                     highScore = score;
    21.                 }
    22.                 break;
    23.             }
    24.         }
    25.     }
    26. }

    Finally, we add a couple of lines to the Draw method. We use the spriteBatch.DrawString method. The High score is left aligned, so we just tell it to draw at Vector2.Zero, the top left position. For the Score, we want to right align, which means we have to set the offset based on the width of the string. To do this, we use the font.MeasureString function, and subtract it from the width of the Viewport. The lines look like this:

    Draw
    1. spriteBatch.DrawString(font, score.ToString(), new Vector2(graphics.GraphicsDevice.Viewport.Width, 0), Color.White, 0f, new Vector2(font.MeasureString(score.ToString()).X, 0), 1f, SpriteEffects.None, 1f);
    2.  
    3. spriteBatch.DrawString(font, "High: " + highScore.ToString(), Vector2.Zero, Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, 1f);

    With those changes, we now have scoring, and the updated game looks like this:

    image

    When you die, the score resets, but the high score remains. If you reset the game, though, you’ll lose your high score. That’s why next week we’ll be looking at persistent storage, and save the high score to a file we can load when the game starts.

    Download the latest version of the source code.

Page 1 of 1 (4 items)