Exercise 2: Game polish, menus and play background music

In the previous exercise, we implemented a game with playable logic. While the game is fully playable in its current state, the game experience lacks polish. Our first task in this exercise is to improve the game’s presentation by incorporating sound and animation.

Later in the exercise, we add additional elements that are part of the game but are not part of the actual gameplay screen. We add a main menu and an instructions screen, and we give the user the ability to pause the game and display a pause screen.

In this exercise, we also will add background music to the game.

This feature allows the player (our user) to:

  • Select a song from those available on the phone that immediately starts playing in the game background without affecting game performance
  • Select a different song and continue the game at any stage of the game

To implement this feature we will use:

  • XNA media framework classes MediaSource and MediaLibrary to get the list of the available songs
  • XNA media framework class MediaPlayer to play the song
  • The menu system that we will develop during taks 2 in this exercise, to provide the player with a menu from which to select a song from the songs list

Task 1 – Polishing the Game – Sounds and Animations

  1. Open the starter solution located in the Source\Ex2-PolishAndMenus\Begin folder.
  2. Open the Animation class under the “Utility” project folder.
  3. Add the following properties to the Animation class:

    C#

    public int FrameCount { get { return sheetSize.X * sheetSize.Y; } } public int FrameIndex { get { return sheetSize.X * currentFrame.Y + currentFrame.X; } set { if (value >= sheetSize.X * sheetSize.Y + 1) { throw new InvalidOperationException( "Specified frame index exeeds available frames"); } currentFrame.Y = value / sheetSize.X; currentFrame.X = value % sheetSize.X; } } public bool IsActive { get; private set; }

    To clarify the meaning of the preceding properties: The “FrameCount” property simply returns the amount of frames contained in the animation represented by the Animation object. The “Offset” property is used to draw the animation at a specified offset by adding the offset value to the position passed to the existing Draw call. The “FrameIndex” property returns the index of the animation’s current frame or sets it. The “IsActive” property can be used to pause the animation by setting it to false.

  4. Add a new method to the Animation class and name it Update. Note that this method is not an override and will need to be explicitly called in order to advance the animation:

    C#

    public void Update() { if (IsActive) { if (FrameIndex >= FrameCount - 1) { IsActive = false; FrameIndex = FrameCount - 1; // Stop at last frame } else { // Remember that updating "currentFrame" will also // update the FrameIndex property. currentFrame.X++; if (currentFrame.X >= sheetSize.X) { currentFrame.X = 0; currentFrame.Y++; } if (currentFrame.Y >= sheetSize.Y) currentFrame.Y = 0; } } }

    The preceding method simply advances the animation by a single frame, stopping the animation if it has reached the final frame.

  5. Add the following to Draw methods to the animation class:

    C#

    public void Draw(SpriteBatch spriteBatch, Vector2 position, SpriteEffects spriteEffect) { Draw(spriteBatch, position, 1.0f, spriteEffect); } public void Draw(SpriteBatch spriteBatch, Vector2 position, float scale, SpriteEffects spriteEffect) { spriteBatch.Draw(animatedCharacter, position + Offset, new Rectangle( FrameSize.X * currentFrame.X, FrameSize.Y * currentFrame.Y, FrameSize.X, FrameSize.Y), Color.White, 0f, Vector2.Zero, scale, spriteEffect, 0); }

    The preceding methods simply draw the portion of the animation sheet that matches the current frame, with the second override allowing the animation to be scaled.

  6. Add a final method to the Animation class and name it PlayFromFrameIndex. This method is used for playing an animation from a specified frame.

    C#

    public void PlayFromFrameIndex(int frameIndex) { FrameIndex = frameIndex; IsActive = true; }

    We now have a class that represents an animation and encapsulates its functionality. Next, we add an additional class to support sound playback.

  7. Open the AudioManager class under the “Utility” project folder.
  8. Add a method and name it Initialize. This method is used to initialize the singleton instance and register it with the game:

    C#

    public static void Initialize(Game game) { audioManager = new AudioManager(game); if (game != null) { game.Components.Add(audioManager); } }

  9. Add a new method and name it LoadSounds. This method is responsible for loading a predefined set of sound assets:

    C#

    public static void LoadSounds() { string soundLocation = "Sounds/"; audioManager.soundNames = new string[,] { {"CatapultExplosion", "catapultExplosion"}, {"Lose", "gameOver_Lose"}, {"Win", "gameOver_Win"}, {"BoulderHit", "boulderHit"}, {"CatapultFire", "catapultFire"}, {"RopeStretch", "ropeStretch"}}; audioManager.soundBank = new Dictionary<string, SoundEffectInstance>(); for (int i = 0; i < audioManager.soundNames.GetLength(0); i++) { SoundEffect se = audioManager.Game.Content.Load<SoundEffect>( soundLocation + audioManager.soundNames[i, 0]); audioManager.soundBank.Add( audioManager.soundNames[i, 1], se.CreateInstance()); } }

  10. Add the following methods to the AudioManager class:

    C#

    public static void PlaySound(string soundName) { // if the music sound exists, start it if (audioManager.soundBank.ContainsKey(soundName)) audioManager.soundBank[soundName].Play(); } public static void PlaySound(string soundName, bool isLooped) { // If the sound exists, start it if (audioManager.soundBank.ContainsKey(soundName)) { if (audioManager.soundBank[soundName].IsLooped != isLooped) audioManager.soundBank[soundName].IsLooped = isLooped; audioManager.soundBank[soundName].Play(); } } public static void StopSound(string soundName) { // if the music sound exists, start it if (audioManager.soundBank.ContainsKey(soundName)) audioManager.soundBank[soundName].Stop(); } public static void StopSounds() { var soundEffectInstances = from sound in audioManager.soundBank.Values where sound.State != SoundState.Stopped select sound; foreach (var soundeffectInstance in soundEffectInstances) soundeffectInstance.Stop(); } public static void PauseResumeSounds(bool isPause) { SoundState state = isPause ? SoundState.Paused : SoundState.Playing; var soundEffectInstances = from sound in audioManager.soundBank.Values where sound.State == state select sound; foreach (var soundeffectInstance in soundEffectInstances) { if (isPause) soundeffectInstance.Play(); else soundeffectInstance.Pause(); } } public static void PlayMusic(string musicSoundName) { // stop the old music sound if (audioManager.musicSound != null) audioManager.musicSound.Stop(true); // if the music sound exists if (audioManager.soundBank.ContainsKey(musicSoundName)) { // get the instance and start it audioManager.musicSound = audioManager.soundBank[musicSoundName]; if (!audioManager.musicSound.IsLooped) audioManager.musicSound.IsLooped = true; audioManager.musicSound.Play(); } }

    The above methods allows playing and stopping one of the sounds loaded using the “LoadSounds” method. The final method enables support for background, and will not be used in the course of this exercise.

  11. Open the Catapult.cs under the "Catapult" folder and examine the class’s fields. Two fields are commented out using the prefix “UNCOMMENT”. Uncomment these fields to make them available. The field section should now look as follows:

    C#

    ... public string Name; // In some cases the game need to start second animation while first animation is still running; // this variable define at which frame the second animation should start Dictionary<string, int> splitFrames; Texture2D idleTexture; Dictionary<string, Animation> animations; SpriteEffects spriteEffects; ...

    These new fields will be used to map animations to a specific name and to define important frames in the course of specific animations.

  12. Navigate to the Catapult class’s constructor and uncomment the final two lines. The constructor should now look as follows:

    C#

    public Catapult(Game game, SpriteBatch screenSpriteBatch,
    FakePre-ae98cb62e8d14491961b68f1552f740c-7cf5bbcb6ce74b98b44308dc1a5fbbf8FakePre-f9629a82f3eb4faf86a90f619a38788f-b868148435954b14a42b9d4ba8f264b0FakePre-9042089054c54816b0ff6c0492c94f54-4a3c3f98687e42ca8b8eeffe2733d334FakePre-6e3516bc6ac94f38a18c0c98aae54979-621f0b60a0594acb8be3fcfc85f8e569FakePre-b04ddd453cbf41a1a78de819ea6a97ef-6d0de905d16740e4b19573e0734cd400FakePre-fad85c31c232478da98f94c3de8f1dc6-61c0e290514a494c8b94162175b82d30FakePre-f112745a8b8444368f489ef4edbfd110-315b3c90165447e28e180fad26a6fbbdFakePre-e4f1bb629b43449585879095789c4e79-7fe7d18b20614aa2bce7967b728fc0a9FakePre-46766677cc8b43588448de2afab94a16-8d8b6c1a5341444da3e4e4b918cf57c6FakePre-8cc8ea3f5f3e494392911736dc855a80-7643b9a968554f5da7ef4c7c65384c5fsplitFrames = new Dictionary<string, int>();FakePre-baf3709ab7d84e51829ee91c22e9339a-2274d75b270149d0bfc006a539fa3876FakePre-eb3cf7c01bbf4890b0357aaeeb2ebbe2-13a4dbf7eb514a6f957fbb2ca071c4e1

  13. Navigate to the Catapult class’s Initialize method. Locate the comment “// TODO: Update hit offset” and change the code directly below it to the following:

    C#

    ... Vector2 projectileStartPosition; if (isAI) projectileStartPosition = new Vector2(630, 340); else projectileStartPosition = new Vector2(175, 340); projectile = new Projectile(curGame, spriteBatch, "Textures/Ammo/rock_ammo", projectileStartPosition, animations["Fire"].FrameSize.Y, isAI, gravity); projectile.Initialize(); AnimationRunning = false; stallUpdateCycles = 0; ...

  14. Further modify the Initialize method by adding the following code at the top of the method:

    C#

    public override voidInitialize() { // Load multiple animations form XML definition XDocument doc = XDocument.Load("Content/Textures/Catapults/AnimationsDef.xml"); XName name = XName.Get("Definition"); var definitions = doc.Document.Descendants(name); // Loop over all definitions in XML foreach (var animationDefinition in definitions) { bool? toLoad = null; bool val; if (bool.TryParse(animationDefinition.Attribute("IsAI").Value, out val)) toLoad = val; // Check if the animation definition need to be loaded for current // catapult if (toLoad == isAI || null == toLoad) { // Get a name of the animation string animatonAlias = animationDefinition.Attribute("Alias").Value; Texture2D texture = curGame.Content.Load<Texture2D>( animationDefinition.Attribute("SheetName").Value); // Get the frame size (width & height) Point frameSize = new Point(); frameSize.X = int.Parse( animationDefinition.Attribute("FrameWidth").Value); frameSize.Y = int.Parse( animationDefinition.Attribute("FrameHeight").Value); // Get the frames sheet dimensions Point sheetSize = new Point(); sheetSize.X = int.Parse( animationDefinition.Attribute("SheetColumns").Value); sheetSize.Y = int.Parse( animationDefinition.Attribute("SheetRows").Value); // If definition has a "SplitFrame" - means that other animation // should start here - load it if (null != animationDefinition.Attribute("SplitFrame")) splitFrames.Add(animatonAlias, int.Parse(animationDefinition.Attribute("SplitFrame").Value)); // Defing animation speed TimeSpan frameInterval = TimeSpan.FromSeconds((float)1 / int.Parse(animationDefinition.Attribute("Speed").Value)); Animation animation = new Animation(texture, frameSize, sheetSize); // If definition has an offset defined - means that it should be // rendered relatively to some element/other animation - load it if (null != animationDefinition.Attribute("OffsetX") && null != animationDefinition.Attribute("OffsetY")) { animation.Offset = new Vector2(int.Parse( animationDefinition.Attribute("OffsetX").Value), int.Parse(animationDefinition.Attribute("OffsetY").Value)); } animations.Add(animatonAlias, animation); } } // Define initial state of the catapult currentState = CatapultState.Idle; ...

    The preceding code goes over the animation definition XML file, which is one of the assets contained in the project, and translates the animations defined in the file into instances of the Animation class we have defined.

    Note:
    The animation definition XML is located in the CatapultGameContent project under Textures and then under Catapults.

  15. Replace the Catapult class’s CheckHit method. This version of the method takes the catapults size into account, instead of using constants, and it also plays back sounds when a catapult is hit by a projectile:

    C#

    private bool CheckHit() { bool bRes = false; // Build a sphere around a projectile Vector3 center = new Vector3(projectile.ProjectilePosition, 0); BoundingSphere sphere = new BoundingSphere(center, Math.Max(projectile.ProjectileTexture.Width / 2, projectile.ProjectileTexture.Height / 2)); // Check Self-Hit - create a bounding box around self Vector3 min = new Vector3(catapultPosition, 0); Vector3 max = new Vector3(catapultPosition + new Vector2(animations["Fire"].FrameSize.X, animations["Fire"].FrameSize.Y), 0); BoundingBox selfBox = new BoundingBox(min, max); // Check enemy - create a bounding box around the enemy min = new Vector3(enemy.Catapult.Position, 0); max = new Vector3(enemy.Catapult.Position + new Vector2(animations["Fire"].FrameSize.X, animations["Fire"].FrameSize.Y), 0); BoundingBox enemyBox = new BoundingBox(min, max); // Check self hit if (sphere.Intersects(selfBox) && currentState != CatapultState.Hit) { AudioManager.PlaySound("catapultExplosion"); // Launch hit animation sequence on self Hit(); enemy.Score++; bRes = true; } // Check if enemy was hit else if (sphere.Intersects(enemyBox) && enemy.Catapult.CurrentState != CatapultState.Hit && enemy.Catapult.CurrentState != CatapultState.Reset) { AudioManager.PlaySound("catapultExplosion"); // Launch enemy hit animaton enemy.Catapult.Hit(); self.Score++; bRes = true; currentState = CatapultState.Reset; } return bRes; }

  16. Replace the Catapult class’s Hit method with the following:

    C#

    public void Hit() { AnimationRunning = true; animations["Destroyed"].PlayFromFrameIndex(0); animations["hitSmoke"].PlayFromFrameIndex(0); currentState = CatapultState.Hit; }

  17. Add an additional reference to the CatapultGame project. The reference is for the Microsoft.Phone assembly.
  18. Replace the Catapult class’s Update method with the following:

    C#

    public override void Update(GameTime gameTime) { bool isGroundHit; bool startStall; CatapultState postUpdateStateChange = 0; if (gameTime == null) throw new ArgumentNullException("gameTime"); // The catapult is inactive, so there is nothing to update if (!IsActive) { base.Update(gameTime); return; } switch (currentState) { case CatapultState.Idle: // Nothing to do break; case CatapultState.Aiming: if (lastUpdateState != CatapultState.Aiming) { AudioManager.PlaySound("ropeStretch", true); AnimationRunning = true; if (isAI == true) { animations["Aim"].PlayFromFrameIndex(0); stallUpdateCycles = 20; startStall = false; } } // Progress Aiming "animation" if (isAI == false) { UpdateAimAccordingToShotStrength(); } else { animations["Aim"].Update(); startStall = AimReachedShotStrength(); currentState = (startStall) ? CatapultState.Stalling : CatapultState.Aiming; } break; case CatapultState.Stalling: if (stallUpdateCycles-- <= 0) { // We've finished stalling; fire the projectile Fire(ShotVelocity); postUpdateStateChange = CatapultState.Firing; } break; case CatapultState.Firing: // Progress Fire animation if (lastUpdateState != CatapultState.Firing) { AudioManager.StopSound("ropeStretch"); AudioManager.PlaySound("catapultFire"); StartFiringFromLastAimPosition(); } animations["Fire"].Update(); // If in the "split" point of the animation start // projectile fire sequence if (animations["Fire"].FrameIndex == splitFrames["Fire"]) { postUpdateStateChange = currentState | CatapultState.ProjectileFlying; projectile.ProjectilePosition = projectile.ProjectileStartPosition; } break; case CatapultState.Firing | CatapultState.ProjectileFlying: // Progress Fire animation animations["Fire"].Update(); // Update projectile velocity & position in flight projectile.UpdateProjectileFlightData(gameTime, wind, gravity, out isGroundHit); if (isGroundHit) { // Start hit sequence postUpdateStateChange = CatapultState.ProjectileHit; animations["fireMiss"].PlayFromFrameIndex(0); } break; case CatapultState.ProjectileFlying: // Update projectile velocity & position in flight projectile.UpdateProjectileFlightData(gameTime, wind, gravity, out isGroundHit); if (isGroundHit) { // Start hit sequence postUpdateStateChange = CatapultState.ProjectileHit; animations["fireMiss"].PlayFromFrameIndex(0); } break; case CatapultState.ProjectileHit: // Check hit on ground impact. if (!CheckHit()) { if (lastUpdateState != CatapultState.ProjectileHit) { VibrateController.Default.Start( TimeSpan.FromMilliseconds(100)); // Play hit sound only on a missed hit; // a direct hit will trigger the explosion sound. AudioManager.PlaySound("boulderHit"); } // Hit animation finished playing if (animations["fireMiss"].IsActive == false) { postUpdateStateChange = CatapultState.Reset; } animations["fireMiss"].Update(); } else { // Catapult hit - start longer vibration on any catapult hit. // Remember that the call to "CheckHit" updates the catapult's // state to "Hit". VibrateController.Default.Start( TimeSpan.FromMilliseconds(500)); } break; case CatapultState.Hit: // Progress hit animation if ((animations["Destroyed"].IsActive == false) && (animations["hitSmoke"].IsActive == false)) { if (enemy.Score >= winScore) { GameOver = true; break; } postUpdateStateChange = CatapultState.Reset; } animations["Destroyed"].Update(); animations["hitSmoke"].Update(); break; case CatapultState.Reset: AnimationRunning = false; break; default: break; } lastUpdateState = currentState; if (postUpdateStateChange != 0) { currentState = postUpdateStateChange; } base.Update(gameTime); }

    The preceding version of the method now contains code to support animation and sound playback. In some places, additional logic is added, since we now have to wait for animations to finish playing back before advancing the state of the catapult. Additionally, the method now utilizes several helper methods, which we implement.

    Note:
    Take the time to examine the preceding method, because it demonstrates how to take animation times into consideration.

  19. Create a new method called UpdateAimAccordingToShotStrength:

    C#

    private void UpdateAimAccordingToShotStrength() { var aimAnimation = animations["Aim"]; int frameToDisplay = Convert.ToInt32(aimAnimation.FrameCount * ShotStrength); aimAnimation.FrameIndex = frameToDisplay; }

    This method translates the current shot strength into a frame in the catapult’s aiming animation. This makes the catapult arm stretch further as the user increases the shot power.

  20. Create a new method called AimReachedShotStrength:

    C#

    private bool AimReachedShotStrength() { return (animations["Aim"].FrameIndex == (Convert.ToInt32(animations["Aim"].FrameCount * ShotStrength) - 1)); }

    The preceding method complements the “UpdateAimAccordingToShotStrength” method, checking whether the current aim animation frame matches the shot strength.

  21. Create a new method called StartFiringFromLastAimPosition:

    C#

    private void StartFiringFromLastAimPosition() { int startFrame = animations["Aim"].FrameCount - animations["Aim"].FrameIndex; animations["Fire"].PlayFromFrameIndex(startFrame); }

    The preceding method takes the current aim animation frame, translating it to the corresponding firing animation frame and activating the firing animation.

  22. Now that the final version of the Catapult’s Update method is ready, replace the Draw method with the following:

    C#

    public override void Draw(GameTime gameTime) { if (gameTime == null) throw new ArgumentNullException("gameTime"); // Using the last update state makes sure we do not draw // before updating animations properly. switch (lastUpdateState) { case CatapultState.Idle: DrawIdleCatapult(); break; case CatapultState.Aiming: case CatapultState.Stalling: animations["Aim"].Draw(spriteBatch, catapultPosition, spriteEffects); break; case CatapultState.Firing: animations["Fire"].Draw(spriteBatch, catapultPosition, spriteEffects); break; case CatapultState.Firing | CatapultState.ProjectileFlying: case CatapultState.ProjectileFlying: animations["Fire"].Draw(spriteBatch, catapultPosition, spriteEffects); projectile.Draw(gameTime); break; case CatapultState.ProjectileHit: // Draw the catapult DrawIdleCatapult(); // Projectile hit animation animations["fireMiss"].Draw(spriteBatch, projectile.ProjectileHitPosition, spriteEffects); break; case CatapultState.Hit: // Catapult hit animation animations["Destroyed"].Draw(spriteBatch, catapultPosition, spriteEffects); // Projectile smoke animation animations["hitSmoke"].Draw(spriteBatch, catapultPosition, spriteEffects); break; case CatapultState.Reset: DrawIdleCatapult(); break; default: break; } base.Draw(gameTime); }

    The main change is drawing the animations relevant to the current catapult state.

  23. Open the GameplayScreen.cs file and navigate to the GameplayScreen class’s Update method. Locate the “TODO” marker comments and replace the surrounding code to look like the following:

    C#

    public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
    FakePre-da9b31879f6f478884f7aca47494ad2b-72d5b10276194544bb43f0239494ca4eFakePre-8ebff074a30f449a872cbc46b42fa3ea-a00cd5fbb50a46739d3d7c723d978d88FakePre-f8edc1fca8574a89a47e43ec7b023d21-72f89fab5aa34ec1aa2ea624c36b3555FakePre-27260e1de1174266a91e5e33cb3dbfb5-573c10b943374fa7afe3e42cf679a4c9AudioManager.PlaySound("gameOver_Win"); AudioManager.PlaySound("gameOver_Lose");FakePre-a173e7be2dd14153848d1df7c9f79fec-af05a671c950477b936d49be36076b0eFakePre-08e6cf17a5b84d28bf6b508a955fd78d-1cfa17f1801749dcb794493e69b968b9FakePre-6f0f59ebe95f42beb9182aa2914fd68f-7c4be5f3f0604e229728acb2df825d17AudioManager.PlaySound("gameOver_Win"); AudioManager.PlaySound("gameOver_Lose");FakePre-e34489c857c34ac6bc93a1e512c4b667-f3d3d27f9b0543e8aa90b011d18be74bFakePre-606a39cac94a4570a52a812e7de3c227-cbb2f230b2e747e784b07dd7c7b2d6cbFakePre-a5fe40281dbd487a8908d2eb901e593e-c9c6d4a399a24c499535c46cb788106fFakePre-2a0a9efa2ef244e68e29873f3983bd7b-b314ba23525c494e8300360582606853FakePre-48837eb460af427a92fe96165c1627f2-bb13a06683a3480f92f53bc54d010c96

  24. Open the CatapultGame.cs file and navigate to the CatapultGame class’s constructor. Restore the constructor’s final line so that the surrounding code will look as follows:

    C#

    ...
    FakePre-3df8fd4754a84c48b9690e2d35507fcb-f4aba186ac664c07829bdf8c1a8377dfFakePre-6ecf6a224d7e44389d1011afb5b7dd3a-47b4a85c76c64d2281bef2976baf4e7dFakePre-fca562d8e605416393e7e60f8dad84b1-58fb9ff998c04bbcb50720535cff8b53AudioManager.Initialize(this);FakePre-47fb135f591749b8b5d31ae8ca17a3c3-c76359704bc04e598b487bcbaac8bc6cFakePre-c587c4d768244607af7e9d7d1c10af0d-b15017d7469a4f0a9c70a5be8c5ccc7b

  25. Override the LoadContent method in the CatapultGame class, in order to load the game’s sounds:

    C#

    protected override void LoadContent() { AudioManager.LoadSounds(); base.LoadContent(); }

  26. Compile the project and deploy it. The game should now include sound and animation in addition to being completely playable.

Task 2 – Adding Additional Screens and Menus

We may have drastically improved the game experience during the previous task, but the game is still not done, because when launched, it displays the gameplay screen abruptly, and there is currently no way to replay once the game is over (short of restarting the program). Additionally, the user cannot pause the game.

In this task, we add additional screens and menus, and we connect them to each other.

  1. Open the file BackgroundScreen.cs under the “Screens” project folder.
  2. Define a class constructor as follows:

    C#

    public BackgroundScreen() { TransitionOnTime = TimeSpan.FromSeconds(0.0); TransitionOffTime = TimeSpan.FromSeconds(0.5); }

  3. The preceding code simply sets values to some of the properties derived from GameScreen, which control how the screen is brought in and out of view.
  4. Override the base class’s “LoadContent” method to load the background image:

    C#

    public override void LoadContent() { background = Load<Texture2D>("Textures/Backgrounds/title_screen"); }

  5. Add custom drawing logic to the class by overriding the Draw method:

    C#

    public override void Draw(GameTime gameTime) { SpriteBatch spriteBatch = ScreenManager.SpriteBatch; spriteBatch.Begin(); // Draw Background spriteBatch.Draw(background, new Vector2(0, 0), new Color(255, 255, 255, TransitionAlpha)); spriteBatch.End(); }

  6. Now that we have a background screen, it is time to add a menu that will be displayed over it. Open the class called “MainMenuScreen” in the “Screens” project folder. Change the constructor with the following one. It defines the menu entries that this menu screen displays, and it causes it not to hide the background screen by setting the IsPopup property to true:

    C#

    public MainMenuScreen()
    FakePre-b4741214bd9c4c2b859cf8afb67819b9-659b6416ecb24f5c97eb8c5297c48d12FakePre-18243a643d42459981944960d1449648-cbd3cdebf07d484e8ecc842ada771bd7IsPopup = true;FakePre-d1bc3c3892d843f7a972f58b1513ca7f-a37817ffdb2f40b5a958201b66436f23FakePre-5b12cd029c634dae8988c0fccb90c8f8-fac04f8bb0994690940684922a89f24eFakePre-8ffbb32348394aa386b5f52896964956-a6007470c8dc499b882c97767c1c6df8FakePre-342162fd4b2444bc9a39e560bf6d544c-bb797057ec184936bb202493718bc53fFakePre-55a8d625249345ea8e7b9394e64b5c90-c660fcdd2d1d454a993f6632b5e60bc2FakePre-b7d6e528a97744e5956a7f7208022662-eb60fce36c1644bbbf7501cc175b898dFakePre-22b696d8ed0343c2b62b1a9759245899-40357d63b899465dbbe5a80010ff1debFakePre-39d09670d5cb440bb6cbf4b6eed45407-17a289e6578644e1865d0bf90954ecb6FakePre-1659db2d03354b2ba0c4f66989b9d924-c5f89e86369047f4b2c7faeb23ecba8bFakePre-ada7609a42b144a9afa878af5c3732ec-204a788afd5147d098887a8f49a96736FakePre-19d1c4d4a146440783e33fab595ceae0-21a8266a86304662a1b51cf64f0c4eb6FakePre-9f0ea34eea744b73881da646bce5f5d8-e4ab74fccd4a48feb5bf7ee8f02a6b30FakePre-1de33a0ef3f2428fa78f08ca3f4673b3-5cbfcdbbae824606b22176910941a282

    A menu screen contains MenuEntry objects which depict the menu’s items. Each entry contains an event handler, which fires when the user selects the entry from the menu. You can see how the preceding code sets the handlers for both menu entries. In the next step, we add the methods that are specified as event handlers.

  7. Create the event handlers by implementing the following methods in the class:

    C#

    // Handles "Play" menu item selection void StartGameMenuEntrySelected(object sender, EventArgs e) { ScreenManager.AddScreen(new InstructionsScreen(), null); } // Handles "Exit" menu item selection protected override void OnCancel(PlayerIndex playerIndex) { ScreenManager.Game.Exit(); }

    Notice the difference between the two method signatures. While StartGameMenuEntrySelected is an actual event handler, OnCancel is actually called from a different event handler, which is also called OnCancel and is implemented in the base class. Also, notice that StartGameMenuEntrySelected’s body adds a screen that we will soon create.

  8. Open the class called “InstructionsScreen” in the “Screens” project folder and add the following constructor to the class. Since this screen will respond to user taps on the display, we have to enable tap gestures:

    C#

    public InstructionsScreen() { EnabledGestures = GestureType.Tap; TransitionOnTime = TimeSpan.FromSeconds(0); TransitionOffTime = TimeSpan.FromSeconds(0.5); }

  9. Override the LoadContent method to load the instruction set image:

    C#

    public override void LoadContent() { background = Load<Texture2D>("Textures/Backgrounds/instructions"); font = Load<SpriteFont>("Fonts/MenuFont"); }

  10. Override the “HandleInput” method as shown in the following code:

    C#

    public override void HandleInput(InputState input) { if (isLoading == true) { base.HandleInput(input); return; } foreach (var gesture in input.Gestures) { if (gesture.GestureType == GestureType.Tap) { // Create a new instance of the gameplay screen gameplayScreen = new GameplayScreen(); gameplayScreen.ScreenManager = ScreenManager; // Start loading the resources in additional thread thread = new System.Threading.Thread( new System.Threading.ThreadStart(gameplayScreen.LoadAssets)); isLoading = true; thread.Start(); } } base.HandleInput(input); }

    The preceding method waits for a tap from the user in order to dismiss the instructions screen. We would like to display the gameplay screen next, but waiting for it to load its assets will cause a noticeable delay between the tap and the appearance of the gameplay screen. Therefore, we will create an additional thread to perform the gameplay screen’s asset initialization. We will display a loading prompt until the process finishes, and then display the gameplay screen. Let us move on to the Update method where we will wait for all assets to load.

  11. Override the “Update” method with the following code:

    C#

    public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { // If additional thread is running, skip if (null != thread) { // If additional thread finished loading and the screen is not exiting if (thread.ThreadState == System.Threading.ThreadState.Stopped && !IsExiting) { isLoading = false; // Exit the screen and show the gameplay screen // with pre-loaded assets ExitScreen(); ScreenManager.AddScreen(gameplayScreen, null); } } base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); }

  12. Override the “Draw” method to display the instructions image, and also the loading prompt while the game’s assets are loading:

    C#

    public override void Draw(GameTime gameTime) { SpriteBatch spriteBatch = ScreenManager.SpriteBatch; spriteBatch.Begin(); // Draw Background spriteBatch.Draw(background, new Vector2(0, 0), new Color(255, 255, 255, TransitionAlpha)); // If loading gameplay screen resource in the // background show "Loading..." text if (isLoading) { string text = "Loading..."; Vector2 size = font.MeasureString(text); Vector2 position = new Vector2( (ScreenManager.GraphicsDevice.Viewport.Width - size.X) / 2, (ScreenManager.GraphicsDevice.Viewport.Height - size.Y) / 2); spriteBatch.DrawString(font, text, position, Color.Black); spriteBatch.DrawString(font, text, position - new Vector2(-4, 4), new Color(255f, 150f, 0f)); } spriteBatch.End(); }

  13. Now that the instructions screen loads the gameplay screen’s assets, there is no longer a need to perform that operation in the GameplayScreen class. Open the GameplayScreen.cs file and navigate to the “LoadContent” method. Change the method to the following:

    C#

    public override void LoadContent() { base.LoadContent(); // Start the game Start(); }

  14. So far, we have created three additional screens and now it is time to make them visible. To do that, we are required to alter the game class “CatapultGame”. Open the file, CatapultGame.cs, and navigate to the CatapultGame class’s constructor. Locate the “TODO” marker comment in the constructor body and replace the code directly below it so that the surrounding code looks like the following:

    C#

    ... //Switch to full screen for best game experience graphics.IsFullScreen = true; // TODO: Start with menu screen screenManager.AddScreen(new BackgroundScreen(), null); screenManager.AddScreen(new MainMenuScreen(), null); AudioManager.Initialize(this); ...

  15. Compile and deploy the project. You will see the game’s main menu. Pressing Play advances the game to the instructions screen and, from there, to the actual game. Pressing Exit terminates the game.

    Figure 5. The game’s main menu

    The final part of this task is to add an additional screen, the pause screen.

  16. Under the “Screens” folder, open the file called PauseScreen.cs and change the constructor with this one:

    C#

    public PauseScreen(GameScreen backgroundScreen, Player human, Player computer)
    FakePre-f859b2a2bf664a90ade87d855b2cda99-2e2dc9d22e704803a01a0ad2adaeda9eFakePre-4e6e5ff6efd84394a7ec3cd92267c0e8-3423b7da1b5c4f738c84a80c06c7204aIsPopup = true;FakePre-bf4e95ebbaed4793a0553e9d67bc5432-22451b6e11f34dbeb0959719b35e9bacFakePre-6943d3225ffd43758ee4838f94252803-46ff31e0370d4cc1a62750297ff49319FakePre-1d2b6ee7bcd54d6cb7815dca6497bd71-01dc149a37bd492ba10368a6f1c710c4FakePre-1b4a3a62d9f541708ea33dcde7eb6689-58852d7fdc454e4d919fb7dfc3afa7d5FakePre-e416444ac5044777bb62b569a8f62cdd-f2641c8e5df64f09b80f1c9f954bc17eFakePre-f2634117e4bb444896ff8b4744e72ceb-8d491597299f4b74a84978fd17c6d927FakePre-910d6f46d48847af9d81a4eb949fab79-c4bd59295ffc41c1a7f86bac9197e398FakePre-b7baf02cfadf494d970c0129439c0b3c-991b2af6b96d4a44bf8bf576128a52e2FakePre-19dea011c5a74e3482dd25263431315e-d6ebe1089ebf4bd38454eee2cda4b250FakePre-4e44f2757d4746b988282e7b336e8598-00ba2ac5191c49f38b1a580f32b46ec1FakePre-a6317fa51d5747558d2cdbeb16d67261-42ccf35467ea49d988be5890bdc7bdb5FakePre-70b734843d5b4b1ba58c16e6ca6a22fd-f3867f8ecd664e22974057411917e0f9FakePre-a50c3d45fc9241b7bda81c4146784dc5-5d04cdf4ffe245d4b69629f7ed4b40a9FakePre-ffe73b45b63a4abeb9a1c38dc828abd6-1d0ba7836e4d430391175581ea198e69FakePre-02e9a9d2c5794322a11099cfe10a8534-676b21cd10f547c8a8c1bfe548bb0a27FakePre-a852fa8056864ae8b917422971b1c223-86516d8af49e443bbe5c8ae6df0b591bFakePre-3aa210e564cd4857a8975a9255254c20-cacb2cf075e84ff9a23f50f145ef06cfFakePre-5196851187d34dd0887cbbff4f7115c1-fff5490bc747452b8dac317b7934ceddFakePre-838773a90d3e42f29ee085538b93cd54-1566c3fa0e62416ead0798668a32c586FakePre-ff9f24e87e1a4390a45047270e28a343-891b4ffed0c049c5ae93618c8353499aFakePre-b8791517c30048119f310da389a6dd9c-d15d168d73424ea4b959907a44a2db7dFakePre-e1b56c3dd1554f55854c28cc0a2814e7-bfbbbe99494245988614e8b0a6b60d12FakePre-a25c2be8b31444ee9094065ee83f648b-10b8a06c8a3b441dbf50269cb642fb8aFakePre-4bad4e70188a4eed81cf7b23c356bee2-ae5532c67f5b4f46a5ec77495e87f87aFakePre-a1668dd981fd49f798376ad2b08ef2ed-886064d4d2aa4b3a8469a0bb1cddd070FakePre-2a83139604c54684af66cb709f9308d0-200c7dd7cfba4e44be3d2829c7616804FakePre-a90aece28c4a40ad9b2103831014a773-225b5cfe2864429a84e183d0a7399716FakePre-902dfd515d5b4a99b284598db7021bdc-cd427b974f0f4ee9bacd3d174d0c5fea

    This constructor resembles that of the MainMenuScreen class, with added logic. The PauseScreen’s constructor remembers the value of each of the two players’ IsActive property, and it sets the property to false for both players. This effectively causes the game screen to freeze. Additionally, all currently playing sounds will be paused.

  17. Add the following two event handlers to the class:

    C#

    void StartGameMenuEntrySelected(object sender, EventArgs e) { human.Catapult.IsActive = prevHumanIsActive; computer.Catapult.IsActive = prevCompuerIsActive; if (!(human as Human).isDragging) AudioManager.PauseResumeSounds(true); else { (human as Human).ResetDragState(); AudioManager.StopSounds(); } backgroundScreen.ExitScreen(); ExitScreen(); } protected override void OnCancel(PlayerIndex playerIndex) { AudioManager.StopSounds(); ScreenManager.AddScreen(new MainMenuScreen(), null); ExitScreen(); }

    Notice how the first handler, which is fired when the user wishes to return to the game, restores both player’s IsActive value and resumes all paused sounds.

  18. Finally, override the “UpdateMenuEntryLocations” method to properly position the menu entries on screen, adding the following marked snippet:

    C#

    protected override void UpdateMenuEntryLocations() { base.UpdateMenuEntryLocations(); foreach (var entry in MenuEntries) { Vector2 position = entry.Position; position.Y += 60; entry.Position = position; } }

  19. The final step is to revise the GameplayScreen class to utilize the new pause screen. Open GameplayScreen.cs and navigate to the “PauseCurrentGame” method. Change the method to look like this:

    C#

    private void PauseCurrentGame()
    FakePre-e7b72e4627f54fada1277e06fd214c24-7ec9ba16a9a646b4a04ecbbc6667a74dvar pauseMenuBackground = new BackgroundScreen();FakePre-bc7a949a5fbc4378b9d4c9fd5efd9aba-0d5bbbb4280d48ba898c8adfd98b1be2FakePre-55a6ad1fd08f417d91d4780939208e8e-89b48924e2a84625a1d1b41753177197FakePre-188f81f986eb4f54aa843b2e7cb9a818-42c01fe917284a9d9032c65e075c59c9FakePre-0b29f19aa2b84644bb25a59b5b4274aa-a8d087e96e204808a2388cc5ff687581FakePre-6aec9c438c4145e78585675cd89d7bd2-3276f2016d334d129a0c8cfc57237886FakePre-e1e16be6d38b4a5f82ddbad9fb8eb7ad-6c09ca0c544c4ba39136ddc263549b86FakePre-f30f17116a394155ba87a59b74391671-fef1dbdc3d7844c79704fcda5ebd74e3FakePre-f1c0411bf2b0447190286d8bd8efa8cd-b6ace8e226824f13a6740db2195e8c82FakePre-ab5f1a723d41458f98f73543b2b29dda-d27985a55d1b4c0988811b3fc43b98c4FakePre-9ffc2f0431b84038ad00f0f0c8b924bd-3876f20b61b345d38ada52e444800affFakePre-6de184a252a74f0a8fd00790d58ac2bf-c7090ce0c80148548f275669540e857d

  20. Compile and deploy the project. From the gameplay screen, you should now be able to use the back arrow key on the device to pause the game. The pause menu allows you to return to the game or exit to the main menu.

Task 3 – Implementing the Feature Class

The XNA 2D Game Development with XNA lab introduced the Game State Manager as a way to control the flow between the game’s different menus (screens). Each screen represents a menu or a logical segment of the game. For example, the first screen is the main menu, the next screen is a game segment, and so on.

In this task, you will add a new screen and create a new class, the MusicSelectionScreen, that wraps together the menu and media functionalities. In the next task you will use this class from the game UI

  1. In the Screens folder, open the class named MusicSelectionScreen and add the fields region to the class:

    C#

    #region Fields IList<MediaSource> mediaSourcesList; MediaLibrary mediaLibrary; GameScreen backgroundScreen; #endregion

    Note:
    Note the highlighted fields that store the media access objects. The Media Library class gives you access to the device media library (for example, the song list).

  2. Replace the existing initialization region to the class:

    C#

    #region Initialization public MusicSelectionScreen(GameScreen backgroundScreen) : base("Main") { IsPopup = true; this.backgroundScreen = backgroundScreen; // Get the default media source mediaSourcesList = MediaSource.GetAvailableMediaSources(); // Use only first one mediaLibrary = new MediaLibrary(mediaSourcesList[0]); // Create maximum 5 entries with music from music collection for (int i = 0; i < mediaLibrary.Songs.Count; i++) { if (i == 5) break; Song song = mediaLibrary.Songs[i]; // Create menu entry for the song. MenuEntry songMenuEntry = new MenuEntry(song.Name); // Hook up menu event handler songMenuEntry.Selected += OnSongSelected; // Add song to the menu MenuEntries.Add(songMenuEntry); } // Create our menu entries. MenuEntry cancelMenuEntry = new MenuEntry("Cancel"); // Hook up menu event handlers. cancelMenuEntry.Selected += OnCancel; // Add entries to the menu. MenuEntries.Add(cancelMenuEntry); } #endregion

    Note also that we initialized the media objects MediaSourcesList and MediaLibrary. We will use them to get the available songs. The MediaLibrary object provides the songs via its property Songs. We just pick the first five songs in this sample, but you can easily extend this functionality. Also, be sure to review the MediaLibrary class for its initial properties, which give you access to song art – for example album pictures.

  3. Add the "Event Handlers" region to the class:

    C#

    #region Event Handlers for Menu Items /// <summary> /// Handles Song selection and exits the menu /// </summary> private void OnSongSelected(object sender, EventArgs e) { var selection = from song in mediaLibrary.Songs where song.Name == (sender as MenuEntry).Text select song; Song selectedSong = selection.FirstOrDefault(); if (null != selectedSong) MediaPlayer.Play(selectedSong); backgroundScreen.ExitScreen(); ExitScreen(); } /// <summary> /// Handles "Exit" menu item selection /// </summary> protected override void OnCancel(Microsoft.Xna.Framework.PlayerIndex playerIndex) { backgroundScreen.ExitScreen(); ExitScreen(); } #endregion

    The OnSongSelected function handles the song selection event. It retrieves the correct song object from the mediaLibrary, per the song in the menu entry that we populated earlier in the constructor, and plays it using the MediaPlayer class.

    This concludes this task. The MusicSelectionScreen class contains all the required functionality to choose and play a song. In the next tasks, we will embed this class in the game and test its functionality.

Task 4 – Using the MusicSelectionScreen Class in the Game

We will use the new feature by selecting the correspondent item in the game menu. To achieve this, we need to change the existing game main menu class--remember, we are extending an existing game.

  1. Open the MainMenuScreen.cs file.
  2. Add the following statement to the Using Statements region:

    C#

    using Microsoft.Xna.Framework.Media;

    This statement allowed us access MediaPlayer. As we will see below, we need access MediaPlayer in this class to stop music when we will exit the application.

  3. Add the following code inside the MainMenuScreen constructor:

    C#

    MenuEntry selectBackgroundMusic = new MenuEntry("Select Background Music"); selectBackgroundMusic.Selected += SelectBackgroundMusicMenuEntrySelected; MenuEntries.Add(selectBackgroundMusic);

  4. This step added a "Select Background Music" to the game menu and assigned an event handler to its selected event.

  5. Add the following code inside:

    C#

    /// Handles "Select Background Music" menu item selection /// </summary> /// <param name="sender"></param> /// <param name="e"></param> void SelectBackgroundMusicMenuEntrySelected(object sender, EventArgs e) { var backgroundScreen = new BackgroundScreen(); ScreenManager.AddScreen(backgroundScreen, null); ScreenManager.AddScreen(new MusicSelectionScreen(backgroundScreen), null); }

    This step implemented the "Select Background Music" menu item handler. This uses the Game State Manager, represented by the ScreenManager object, and adds a new screen, represented as a Screen class. In our case, this is the MusicSelectionScreen.

  6. Replace the OnCancel event with the following code:

    C#

    /// <summary>
    FakePre-054278651b7f47a29db3eb8522330818-5a918b3f82844911a46f42045f063254FakePre-dc341f9e547b4d5c9768b81b331c3a5f-8d8dc2ac5d764327b2e6024e5829adbeFakePre-1bd29eecada24caea1d0a7fbf7abdc3e-69af3799bc1d456fb2063ad5fb0d8727FakePre-99b2e9eaab5a4f98b0890edc2f94e7d6-95fe74f769b94b9fa5f6dfd58c3b36e0FakePre-bf16575f1b9d4a04bff3447c584af20b-3778cea3f1294c499cdf15d7b45701ebif (MediaPlayer.State == MediaState.Playing)FakePre-255746859b364c3d837d008e688937e9-47a558e3b56d4b6f9a0c580fe54ceb25FakePre-d7acc1df376646d290fb9df21579211a-bc0651b6a02c46e08fce4f9d58d7ac81FakePre-1f784359472f4e66b35a7ddcb35764d4-afb1494f4c59446db50135a98693c8a8

    On this step we added to the OnCancel event handler two lines, they are highlighted, which stop the background music when the player cancels the game.

  7. Go ahead and compile and run the game. You should see the following screen in the emulator. This is the first game screen, showing the game’s main menu. Note that now it contains the new menu item – “Select Background Music”:

    Figure 6

    Main Screen with the new item

  8. Selecting "Select Background Music" launches the MusicSelectionScreen from the previous task. The following picture shows this screen; note the list of songs.

    Figure 7

    Background Music Selection screen

    When we select a song, the screen is closed and we return to the game’s main menu. You should hear the selected song in the background.

    Congratulations! The game is now playing background music.