Share via


Exercise 2: Game Polish and Menus

In the previous exercise, we implemented a fully playable game. However, as we have stated at the end of the final task, the game severely lacks polish in its current state. Our first task in this exercise is to improve the game’s presentation by incorporating sound.

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. Additionally, we add a highscore screen to keep track of the best scores achieved in the game.

We will also add a calibration screen, which will make it possible to set any orientation as the “idle” state at which the maze is not tilted.

Task 1 –Sounds

  1. Open the starter solution located in the Source\Ex2-Polishing\Begin folder.
  2. We now need to add sound resources to our content project. Before we can play sounds we need to initialize the AudioManager and load the sounds. Open the MarbleMazeGame.cs file and add the following line to the end of the MarbleMazeGame class’s constructor:

    C#

    AudioManager.Initialize(this);
  3. Add the following function to the MarbleMazeGame class:

    C#

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

    This will cause the AudioManager to load all of its associated sounds so that they will be ready for playback.

  4. Open the “GameplayScreen.cs” under the “Screens” project folder and navigate to the UpdateLastCheackpoint method. We will change the method so that each time a checkpoint is passed, a sound will play (old code is colored gray):

    C#

    private void UpdateLastCheackpoint() { BoundingSphere marblePosition = marble.BoundingSphereTransformed; var tmp = lastCheackpointNode; while (tmp.Next != null) { // If the marble close to checkpoint save the checkpoint if (Math.Abs(Vector3.Distance(marblePosition.Center, tmp.Next.Value)) <= marblePosition.Radius * 3) { AudioManager.PlaySound("checkpoint"); lastCheackpointNode = tmp.Next; return; } tmp = tmp.Next; } }
  5. Open the “Marble.cs” file under the “Objects” project folder and navigate to the Update method. Modify the method to look like the following:

    C#

    public override void Update(GameTime gameTime) { base.Update(gameTime); // Make the camera follow the marble Camera.ObjectToFollow = Vector3.Transform(Position, Matrix.CreateFromYawPitchRoll(Maze.Rotation.Y, Maze.Rotation.X, Maze.Rotation.Z)); PlaySounds(); }

    The “PlaySounds” helper method will be responsible for playing sounds related to the marble’s movement. We will implement it in the next step.

  6. Add an implementation for the PlaySounds method:

    C#

    private void PlaySounds() { // Calculate the pitch by the velocity float volumeX = MathHelper.Clamp(Math.Abs(Velocity.X) / 400, 0, 1); float volumeZ = MathHelper.Clamp(Math.Abs(Velocity.Z) / 400, 0, 1); float volume = Math.Max(volumeX, volumeZ); float pitch = volume - 1.0f; // Play the roll sound only if the marble roll on maze if (intersectDetails.IntersectWithGround && (Velocity.X != 0 || Velocity.Z != 0)) { if (AudioManager.Instance["rolling"].State != SoundState.Playing) AudioManager.PlaySound("rolling", true); // Update the volume & pitch by the velocity AudioManager.Instance["rolling"].Volume = Math.Max(volumeX, volumeZ); AudioManager.Instance["rolling"].Pitch = pitch; } else { AudioManager.StopSound("rolling"); } // Play fall sound when fall if (Position.Y < -50) { AudioManager.PlaySound("pit"); } // Play collision sound when collide with walls if (intersectDetails.IntersectWithWalls) { AudioManager.PlaySound("collision"); AudioManager.Instance["collision"].Volume = Math.Max(volumeX, volumeZ); } }

    This method is responsible for playing several sounds. As the marble rolls, a rolling sound will be played and will have its pitch and volume adjusted according to the marble’s current velocity. The method plays additional sounds when the marble hits the wall or falls into a pit.

  7. Compile the project and deploy it. The game should now include sounds.

Task 2 – Additional Screens and Menus

We may have drastically improved the game experience during the previous task, but the game is still not complete, as it displays the gameplay screen abruptly when launched, 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 class name BackgroundScreen 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); }

    This code simply sets values for some of the properties derived from GameScreen, which control how the screen is brought in and out of view.

  3. 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(); spriteBatch.Draw(background,newVector2(0, 0), Color.White * TransitionAlpha); spriteBatch.End(); }
  4. Now that we have a background screen, it is time to add a menu that will be displayed over it. Open the class “MainMenuScreen” in the “Screens” project folder.
  5. Change the constructor as below.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() : base("") { IsPopup = true; // Create our menu entries. MenuEntry startGameMenuEntry = new MenuEntry("Play"); MenuEntry highScoreMenuEntry = new MenuEntry("High Score"); MenuEntry exitMenuEntry = new MenuEntry("Exit"); // Hook up menu event handlers. startGameMenuEntry.Selected += StartGameMenuEntrySelected; highScoreMenuEntry.Selected += HighScoreMenuEntrySelected; exitMenuEntry.Selected += OnCancel; // Add entries to the menu. MenuEntries.Add(startGameMenuEntry); MenuEntries.Add(highScoreMenuEntry); MenuEntries.Add(exitMenuEntry); }

    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 above code sets the handlers for all menu entries. In the next step, we add the methods that are specified as event handlers.

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

    C#

    void HighScoreMenuEntrySelected(object sender, EventArgs e) { foreach (GameScreen screen in ScreenManager.GetScreens()) screen.ExitScreen(); ScreenManager.AddScreen(new BackgroundScreen(), null); ScreenManager.AddScreen(new HighScoreScreen(), null); } void StartGameMenuEntrySelected(object sender, EventArgs e) { foreach (GameScreen screen in ScreenManager.GetScreens()) screen.ExitScreen(); ScreenManager.AddScreen(new LoadingAndInstructionScreen(), null); } protected override void OnCancel(PlayerIndex playerIndex) { HighScoreScreen.SaveHighscore(); ScreenManager.Game.Exit(); }

    Notice the difference between the first two methods and last method. While the first two are actual event handler, OnCancel is actually called from a different event handler, which is also called OnCancel and is implemented in the base class. The various handlers refer to screens and methods which do not exist yet. We will implement them during the course of this task.

  7. Open the file “LoadingAndInstructionScreen.cs” under the “Screens” project folder.
  8. Add the following constructor to the class. Since this screen responds to user taps on the display, we need to enable tap gestures:

    C#

    public LoadingAndInstructionScreen() { EnabledGestures = GestureType.Tap; TransitionOnTime = TimeSpan.FromSeconds(0); TransitionOffTime = TimeSpan.FromSeconds(0.5); }
  9. Override the “LoadContent” method to load the instruction set image and a font which we will later use:

    C#

    public override void LoadContent() { background = Load<Texture2D>(@"Textures\instructions"); font = Load<SpriteFont>(@"Fonts\MenuFont"); // Create a new instance of the gameplay screen gameplayScreen = new GameplayScreen(); gameplayScreen.ScreenManager = ScreenManager; }
  10. Override the HandleInput method as shown in the following code segment:

    C#

    public override void HandleInput(InputState input) { if (!isLoading) { if (input.Gestures.Count > 0) { if (input.Gestures[0].GestureType == GestureType.Tap) { // Start loading the resources in additional thread thread = new Thread( new 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 == ThreadState.Stopped && !IsExiting) { // Exit the screen and show the gameplay screen // with pre-loaded assets foreach (GameScreen screen in ScreenManager.GetScreens()) screen.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.White); } 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() { timeFont = ScreenManager.Game.Content.Load<SpriteFont>(@"Fonts\MenuFont"); Accelerometer.Initialize(); base.LoadContent(); }
  14. So far, we have created three additional screens and now it is time to make them visible. To do that, we will alter the game class “MarbleMazeGame”. Open the file, “MarbleMazeGame.cs”, and navigate to the MarbleMazeGame class’s constructor and add the highlighted lines as below:

    C#

    public MarbleMazeGame() { ... graphics.SupportedOrientations = DisplayOrientation.LandscapeLeft; // Add two new screens screenManager.AddScreen(new BackgroundScreen(), null); screenManager.AddScreen(new MainMenuScreen(), null); // Initialize sound system AudioManager.Initialize(this); }

    Notice that we have replaced the line which adds the gameplay screen, and instead now add the background and main menu screens.

  15. We need to implement one final screen which is referenced by the menu screen, the high score screen. Under the Screens project folder open the class called HighScoreScreen.cs and locate the HighScoreScreen class.
  16. Add the following constructor to the HighScoreScreen class:

    C#

    public HighScoreScreen() { EnabledGestures = GestureType.Tap; }
  17. Override HandleInput with the following code:

    C#

    public override void HandleInput(InputState input) { if (input == null) throw new ArgumentNullException("input"); if (input.IsPauseGame(null)) { Exit(); } // Return to main menu when tap on the phone if (input.Gestures.Count > 0) { GestureSample sample = input.Gestures[0]; if (sample.GestureType == GestureType.Tap) { Exit(); input.Gestures.Clear(); } } }

    This will cause the screen to exit when the user taps the display or uses the device’s “back” button. Exiting the screen is handled by the “Exit” method which we will implement next.

  18. Override the Draw method to show the highscores table on the screen:

    C#

    public override void Draw(Microsoft.Xna.Framework.GameTime gameTime) { ScreenManager.SpriteBatch.Begin(); // Draw the title ScreenManager.SpriteBatch.DrawString(highScoreFont, "High Scores", new Vector2(30, 30), Color.White); // Draw the highscores table for (int i = 0; i < highScore.Length; i++) { ScreenManager.SpriteBatch.DrawString(highScoreFont, String.Format("{0}. {1}", i + 1, highScore[i].Key), new Vector2(100, i * 40 + 70), Color.YellowGreen); ScreenManager.SpriteBatch.DrawString(highScoreFont, String.Format("{0:00}:{1:00}", highScore[i].Value.Minutes, highScore[i].Value.Seconds), new Vector2(500, i * 40 + 70), Color.YellowGreen); } ScreenManager.SpriteBatch.End(); base.Draw(gameTime); }

    So far we have added very little logic to the screen that actually manages the high-score table. We will now turn our attention to that matter.

  19. Add the following method to the HighScoreScreen class. It will check if a score belongs in the high-score table by comparing it with the worst score on the table:

    C#

    public static bool IsInHighscores(TimeSpan gameTime) { // If the score is less from the last place score return gameTime < highScore[highscorePlaces - 1].Value; }
  20. Add a method to insert new scores into the high-score table:

    C#

    public static void PutHighScore(string playerName, TimeSpan gameTime) { if (IsInHighscores(gameTime)) { highScore[highscorePlaces - 1] = new KeyValuePair<string, TimeSpan>(playerName, gameTime); OrderGameScore(); } }

    A new score is inserted by removing the lowest score and then ordering the table.

  21. Add the following method to store the high-score table on the device:

    C#

    public static void SaveHighscore() { // Get the place to store the data using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication()) { // Create the file to save the data using (IsolatedStorageFileStream isfs = new IsolatedStorageFileStream("highscores.txt", FileMode.Create, isf)) { // Get the stream to write the file using (StreamWriter writer = new StreamWriter(isfs)) { for (int i = 0; i < highScore.Length; i++) { // Write the scores writer.WriteLine(highScore[i].Key); writer.WriteLine(highScore[i].Value.ToString()); } // Save and close the file writer.Flush(); writer.Close(); } } } }

    Note that we first access the game’s isolated storage, which is the only place where the game is allowed to store data on the device.

  22. Add the following method to load the high-score table:

    C#

    public static void LoadHighscore() { // Get the place the data stored using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication()) { // Try to open the file if (isf.FileExists("highscores.txt")) { using (IsolatedStorageFileStream isfs = new IsolatedStorageFileStream("highscores.txt", FileMode.Open, isf)) { // Get the stream to read the data using (StreamReader reader = new StreamReader(isfs)) { // Read the highscores int i = 0; while (!reader.EndOfStream) { string[] line = new[] { reader.ReadLine(), reader.ReadLine() }; highScore[i++] = new KeyValuePair<string, TimeSpan>(line[0], TimeSpan.Parse(line[1])); } } } } } OrderGameScore(); }

    When loading the high-score table we attempt to find the file created by the save operation in the game’s isolated storage. If the file does not exist the high-score table will revert to its default values.

  23. The high-score screen is ready, we just need to initialize it. Open the “MableMazeGame.cs” file and navigate to the LoadContent method. Alter the method in the following manner:

    C#

    protected override void LoadContent() { AudioManager.LoadSounds(); HighScoreScreen.LoadHighscore(); base.LoadContent(); }
  24. Compile and deploy the project. When the game launches you will now see the main menu. Each entry should work as expected, though the game itself will still not end properly. We will fix this in the next task.

    Figure 10

    High-scores screen

  25. The final part of this task is to add an additional screen, the pause screen. This screen will allow the user to pause the game and is fairly similar to the main menu screen. Open the file name PauseScreen.cs under the “Screen” folder.
  26. Add the following highlighted lines to the constructor of the class:

    C#

    public PauseScreen() : base("Game Paused") { // Create our menu entries. MenuEntry returnGameMenuEntry = new MenuEntry("Return"); MenuEntry restartGameMenuEntry = new MenuEntry("Restart"); MenuEntry exitMenuEntry = new MenuEntry("Quit Game"); // Hook up menu event handlers. returnGameMenuEntry.Selected += ReturnGameMenuEntrySelected; restartGameMenuEntry.Selected += RestartGameMenuEntrySelected; exitMenuEntry.Selected += OnCancel; // Add entries to the menu. MenuEntries.Add(returnGameMenuEntry); MenuEntries.Add(restartGameMenuEntry); MenuEntries.Add(exitMenuEntry); }

    The pause screen displays a menu with three entries. One allowing the user to return to the game, another allowing the user to restart the game and a final one allowing the user to return to the main menu.

  27. Pay attention to the following methods: ReturnGameMenuEntrySelected,RestartGameMenuEntrySelected, OnCancel. These methods are used by the pause screen’s menu entries. Notice how the first handler, which is fired when the user wishes to return to the game, restores IsActive value and resumes all paused sounds. Also notice how the second handler uses a method of the gameplay screen which we have yet to implement.
  28. Open the “GameplayScreen.cs” file from the “Screens” project folder and navigate to the GameplayScreen class. Localize the Restart method and add the following highlighted lines. This method simply resets some variables which will in effect reset the game itself.

    C#

    internal void Restart() { marble.Position = maze.StartPoistion; marble.Velocity =Vector3.Zero; marble.Acceleration =Vector3.Zero; maze.Rotation =Vector3.Zero; IsActive =true; gameOver =false; gameTime =TimeSpan.Zero; lastCheackpointNode= maze.Checkpoints.First; }
  29. The final step is to revise the GameplayScreen class to utilize the new pause screen. Add the following method:

    C#

    private void PauseCurrentGame() { IsActive = false; // Pause the sounds AudioManager.PauseResumeSounds(false); ScreenManager.AddScreen(new BackgroundScreen(), null); ScreenManager.AddScreen(new PauseScreen(), null); }

    This method will pause all currently playing sounds, make the game inactive and advance to the pause screen.

  30. Modify the GameplayScreen class's constructor:

    C#

    public GameplayScreen() { TransitionOnTime = TimeSpan.FromSeconds(0.0); TransitionOffTime = TimeSpan.FromSeconds(0.0); EnabledGestures = GestureType.Tap; }
  31. Modify the GameplayScreen class’s HandleInput method:

    C#

    public override void HandleInput(InputState input) { if (input == null) throw new ArgumentNullException("input"); if (input.IsPauseGame(null)) { if (!gameOver) PauseCurrentGame(); else FinishCurrentGame(); } if (IsActive) { if (input.Gestures.Count > 0) { GestureSample sample = input.Gestures[0]; if (sample.GestureType == GestureType.Tap) { if (gameOver) FinishCurrentGame(); } } if (!gameOver) { // Rotate the maze according to accelerometer data Vector3 currentAccelerometerState = Accelerometer.GetState().Acceleration; if (Microsoft.Devices.Environment.DeviceType == DeviceType.Device) { //Change the velocity according to acceleration reading maze.Rotation.Z = (float)Math.Round(MathHelper.ToRadians( currentAccelerometerState.Y * 30), 2); maze.Rotation.X = -(float)Math.Round(MathHelper.ToRadians( currentAccelerometerState.X * 30), 2); } else if (Microsoft.Devices.Environment.DeviceType == DeviceType.Emulator) { Vector3 Rotation = Vector3.Zero; if (currentAccelerometerState.X != 0) { if (currentAccelerometerState.X > 0) Rotation += new Vector3(0, 0, -angularVelocity); else Rotation += new Vector3(0, 0, angularVelocity); } if (currentAccelerometerState.Y != 0) { if (currentAccelerometerState.Y > 0) Rotation += new Vector3(-angularVelocity, 0, 0); else Rotation += new Vector3(angularVelocity, 0, 0); } // Limit the rotation of the maze to 30 degrees maze.Rotation.X = MathHelper.Clamp(maze.Rotation.X + Rotation.X, MathHelper.ToRadians(-30), MathHelper.ToRadians(30)); maze.Rotation.Z = MathHelper.Clamp(maze.Rotation.Z + Rotation.Z, MathHelper.ToRadians(-30), MathHelper.ToRadians(30)); } } } }
  32. Add the FinishCurrentGame method

    C#

    private void FinishCurrentGame() { IsActive = false; foreach (GameScreen screen in ScreenManager.GetScreens()) screen.ExitScreen(); if (HighScoreScreen.IsInHighscores(gameTime)) { // Show the device's keyboard Guide.BeginShowKeyboardInput(PlayerIndex.One, "Player Name", "Enter your name (max 15 characters)", "Player", (r) => { string playerName = Guide.EndShowKeyboardInput(r); if (playerName != null && playerName.Length > 15) playerName = playerName.Substring(0, 15); HighScoreScreen.PutHighScore(playerName, gameTime); ScreenManager.AddScreen(new BackgroundScreen(), null); ScreenManager.AddScreen(new HighScoreScreen(), null); }, null); return; } ScreenManager.AddScreen(new BackgroundScreen(), null); ScreenManager.AddScreen(new HighScoreScreen(), null); }

    The updated method will allow the user to type in his name in case he has achieved a high score.

  33. Compile and deploy the project. You should now be able to pause the game by pressing the device’s “back” button while in the gameplay screen. Additionally, all of the pause screen’s menu items should function properly. Note that pausing the game will not pause the timer that measures the player’s performance. We will fix this later in the lab.

    Figure 11

    Pause screen

Task 3 – “3-2-1-Go!” Countdown Timer and Game Over Screen

In this task, we will focus on making the gameplay screen appear and exit more smoothly when the game starts or ends.

  1. Open the “GameplayScreen.cs” file under the “Screens” project folder and change the Update to contain the following code:

    C#

    public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { if (IsActive && !gameOver) { if (!startScreen) { // Calculate the time from the start of the game this.gameTime += gameTime.ElapsedGameTime; CheckFallInPit(); UpdateLastCheackpoint(); } // Update all the component of the game maze.Update(gameTime); marble.Update(gameTime); camera.Update(gameTime); CheckGameFinish(); base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); } if (startScreen) { if (startScreenTime.Ticks > 0) { startScreenTime -= gameTime.ElapsedGameTime; } else { startScreen = false; } } }

    The above code introduces a delay between the first moment the gameplay screen is display and until the game actually begins. It is also responsible for adding a delay after the user reaches the end of the maze before ending the game.

  2. Update the Draw method to look like the following:

    C#

    public override void Draw(GameTime gameTime) { ScreenManager.GraphicsDevice.Clear(Color.Black); ScreenManager.SpriteBatch.Begin(); if (startScreen) { DrawStartGame(gameTime); } if (IsActive) { // Draw the elapsed time ScreenManager.SpriteBatch.DrawString(timeFont, String.Format("{0:00}:{1:00}", this.gameTime.Minutes, this.gameTime.Seconds), new Vector2(20, 20), Color.YellowGreen); // Drawing sprites changes some render states around, which don't // play nicely with 3d models. // In particular, we need to enable the depth buffer. DepthStencilState depthStensilState = new DepthStencilState() { DepthBufferEnable = true }; ScreenManager.GraphicsDevice.DepthStencilState = depthStensilState; // Draw all the game components maze.Draw(gameTime); marble.Draw(gameTime); } if (gameOver) { AudioManager.StopSounds(); DrawEndGame(gameTime); } ScreenManager.SpriteBatch.End(); base.Draw(gameTime); }

    Here we call function that draw on screen prompts before the game begins and after it ends.

  3. Add the following methods to the GameplayScreen class:

    C#

    private void DrawEndGame(GameTime gameTime)
    FakePre-7f642ada4aab46c28ff52f1a8926e71c-a4764b61228c470da323bdd51830614dFakePre-afc135b57bde4f5fafff57f62cbc79f0-17c12b3428924019a55cc15758fc9bcdFakePre-e001636e437a4b98b42f32bb0b631f46-56549936cd28468c8a078d4042ea9aecFakePre-9a5db63834ec4ed2a03a05b6855ed36c-6d37680abc5145269a70117303569c49FakePre-67e9200b20e14c3ba81e73714ac30701-e779401463c44bdb8cce9a00b22315a7FakePre-7a28a97b30c84a23856461fc43d511c9-9c9adf9192364c8f98ed8938300dfb74FakePre-76e6c2816cf046bb8931498025747697-fe43c3d4241d4741b69f19f23fd9ac19FakePre-baee9d80e0264c3f896a06d1e1c22140-b5fb4908682c4b22a9243210e04d06a0FakePre-8d6792c5f62f41709196be3099c62eda-a1731e52350f40998a5bab0da03cfffaFakePre-0429e48ab49b4b02bd221205475b43c1-5456982e516b4b1bbb3efacb2927f57fFakePre-1b72cc4e71bf4169bfff9f42e6870fd3-098f766bc9f74c31807123e175304c13FakePre-7c0ef90ce7ac4535bf19ef2d9c9208ce-30a87b70c2ac42ffa589811c1ab0b9aeFakePre-68dbca96187a46d69f5e5d982f195427-da3435dbd4d9402bb795eab2d6739203FakePre-07debae515034d338b0315679d44b666-222d482dad4a4b58b007a70f09f85b7bFakePre-7029531b37054224973fcb727dcaf6e2-3471d30909f54590b7a9ff14a2c12064FakePre-78999968c71144fe8387c6e6419ee7a8-7d633004abf54950be3c7e16539efcceFakePre-89307a0474364dc0930842fdb25ff1a0-bc54f6ef60f74e04a70d63b83de21a19FakePre-e5386d091e9f4f3cb3528185e7377afb-cbcdbeb520084b88b2496f5165f82e56FakePre-5c30f17a6cf64c34ab7cb7d1ca6bb33c-3bf8c487374547b7839c30f53aed17baFakePre-bb5b60148f7c40beaa40544e83b0ffb1-0b1480840b30465eac63377568d025d1FakePre-d7b1a82454e54a43b8a3bf29afb1edfc-be578c9ea80f4aeb9b3337c9e9833c6eFakePre-2257962637b34b08a30b65d32497b133-9fae152e45e749da82f2a91ce4f76258FakePre-4af16c9298fc4675b72db24d713b4a0e-fa67532373524626962e55f8e7bccaddFakePre-07cb0c63a94448efbfea186805ba6662-c695f32d0bb14beb829c3800c75159a5FakePre-4ce96cab30fc48c29d7c0eb151af2982-03c50b3c3b234e9b9a866b8fcd3d9a2b

  4. Revise the HandleInput method by changing the initial conditional and Tap handling:

    C#

    public override void HandleInput(InputState input){ if (input == null) throw new ArgumentNullException("input"); if (input.IsPauseGame(null)) { if (!gameOver) PauseCurrentGame(); else FinishCurrentGame(); } if(IsActive && !startScreen){ ...

    This will cause input to be ignored during the game’s initial countdown.

  5. Modify the GameplayScreen class’s Restart method:

    C#

    internal void Restart() { marble.Position = maze.StartPoistion; marble.Velocity = Vector3.Zero; marble.Acceleration = Vector3.Zero; maze.Rotation = Vector3.Zero; IsActive = true; gameOver = false; gameTime = TimeSpan.Zero; startScreen = true; startScreenTime = TimeSpan.FromSeconds(4); lastCheackpointNode = maze.Checkpoints.First; }
  6. Compile and deploy the project. The game should now be fully operational. The game will begin and end smoothly and you will be able to save your high-scores. The only remaining task will be to add the calibration screen.

Task 4 - Calibration Screen

Our final task will be to add a calibration screen which will allow the user to calibrate the accelerometer to eliminate “white noise”.

  1. Open under the “Screens” project folder and the file called “CalibrationScreen.cs”.
  2. Add the following constructor to the class:

    C#

    public CalibrationScreen(GameplayScreen gameplayScreen) { TransitionOnTime = TimeSpan.FromSeconds(0); TransitionOffTime = TimeSpan.FromSeconds(0.5); IsPopup = true; this.gameplayScreen = gameplayScreen; }
  3. Override the LoadContent method

    C#

    public override void LoadContent() { background = Load<Texture2D>(@"Images\titleScreen"); font = Load<SpriteFont>(@"Fonts\MenuFont"); // Start calibrating in additional thread thread = new Thread( new ThreadStart(Calibrate)); isCalibrating = true; startTime = DateTime.Now; thread.Start(); }
  4. Override the Update method

    C#

    public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { // If additional thread is running, skip if (!isCalibrating) { gameplayScreen.AccelerometerCalibrationData = accelerometerCalibrationData; foreach (GameScreen screen in ScreenManager.GetScreens()) if (screen.GetType() == typeof(BackgroundScreen)) { screen.ExitScreen(); break; } (ScreenManager.GetScreens()[0] as GameplayScreen).IsActive = true; ExitScreen(); } base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); }

    All the above method does is wait for the calibration process to end, store the data in the gameplay screen and reactivate it.

  5. Override the Draw method to display a prompt while calibrating:

    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 (isCalibrating) { string text = "Calibrating..."; 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.White); } spriteBatch.End(); }
  6. Add the following method that calibrates the accelerometer:

    C#

    private void Calibrate() { //Initialize the accelerometer accelerometer = new Microsoft.Devices.Sensors.Accelerometer(); if (accelerometer.State == SensorState.Initializing || accelerometer.State == SensorState.Ready) { accelerometer.ReadingChanged += (s, e) => { accelerometerState = new Vector3((float)e.X, (float)e.Y, (float)e.Z); samplesCount++; accelerometerCalibrationData += accelerometerState; if (DateTime.Now >= startTime.AddSeconds(5)) { accelerometer.Stop(); accelerometerCalibrationData.X /= samplesCount; accelerometerCalibrationData.Y /= samplesCount; accelerometerCalibrationData.Z /= samplesCount; isCalibrating = false; } }; } accelerometer.Start(); }

    In this method, the Calibration Screen accumulates the accelerometer readings for 5 seconds and calculates the average value of those readings. We will use this average value to normalize the accelerometer input while playing.

  7. All we need to do now is hook the calibration screen into the gameplay screen. Navigate to the GameplayScreen class’s constructor and change it to the following:

    C#

    public GameplayScreen() { TransitionOnTime = TimeSpan.FromSeconds(0.0); TransitionOffTime = TimeSpan.FromSeconds(0.0); EnabledGestures = GestureType.Tap | GestureType.DoubleTap; }
  8. Add the following method to the GameplayScreen class:

    C#

    private void CalibrateGame(){ IsActive =false; // Pause the sounds AudioManager.PauseResumeSounds(false); ScreenManager.AddScreen(new BackgroundScreen(),null); ScreenManager.AddScreen(new CalibrationScreen(this),null); }

    This method simply activates the calibration screen.

  9. Add the following field to the GameplayScreen class:

    C#

    public Vector3 AccelerometerCalibrationData = Vector3.Zero;
  10. Update the HandleInput method one last time to launch the calibration screen when the device is double-tapped:

    C#

    public override void HandleInput(InputState input) { if (input == null) throw new ArgumentNullException("input"); if (input.IsPauseGame(null)) { if (!gameOver) PauseCurrentGame(); else FinishCurrentGame(); } if (IsActive && !startScreen) { if (input.Gestures.Count > 0) { GestureSample sample = input.Gestures[0]; if (sample.GestureType == GestureType.Tap) { if (gameOver) FinishCurrentGame(); } } if (!gameOver) { if (Microsoft.Devices.Environment.DeviceType == DeviceType.Device) { // Calibrate the accelerometer upon a double tap if (input.Gestures.Count > 0) { GestureSample sample = input.Gestures[0]; if (sample.GestureType == GestureType.DoubleTap) { CalibrateGame(); input.Gestures.Clear(); } } } // Rotate the maze according to accelerometer data Vector3 currentAccelerometerState = Accelerometer.GetState().Acceleration; currentAccelerometerState.X -= AccelerometerCalibrationData.X; currentAccelerometerState.Y -= AccelerometerCalibrationData.Y; currentAccelerometerState.Z -= AccelerometerCalibrationData.Z; if (Microsoft.Devices.Environment.DeviceType == DeviceType.Device) { //Change the velocity according to acceleration reading maze.Rotation.Z = (float)Math.Round(MathHelper.ToRadians( currentAccelerometerState.Y * 30), 2); maze.Rotation.X = -(float)Math.Round(MathHelper.ToRadians( currentAccelerometerState.X * 30), 2); } else if (Microsoft.Devices.Environment.DeviceType == DeviceType.Emulator) { Vector3 Rotation = Vector3.Zero; if (currentAccelerometerState.X != 0) { if (currentAccelerometerState.X > 0) Rotation += new Vector3(0, 0, -angularVelocity); else Rotation += new Vector3(0, 0, angularVelocity); } if (currentAccelerometerState.Y != 0) { if (currentAccelerometerState.Y > 0) Rotation += new Vector3(-angularVelocity, 0, 0); else Rotation += new Vector3(angularVelocity, 0, 0); } // Limit the rotation of the maze to 30 degrees maze.Rotation.X = MathHelper.Clamp(maze.Rotation.X + Rotation.X, MathHelper.ToRadians(-30), MathHelper.ToRadians(30)); maze.Rotation.Z = MathHelper.Clamp(maze.Rotation.Z + Rotation.Z, MathHelper.ToRadians(-30), MathHelper.ToRadians(30)); } } } }
  11. Compile and deploy the game. You should now be able to access the calibration screen while playing by double-tapping the display, but only while running on an actual device.

    Congratulations! The game is now fully operational.