Exercise 1: Basic XNA Game Studio Game With Game State Management

If you have ever wanted to program your own games, Microsoft® XNA™ Game Studio 4.0 (installed as a part of the prerequisites) is for you.

Whether you are a student, hobbyist or an independent game developer — you can create and share great games using the XNA Game Studio.

XNA Game Studio 4.0 is a game development product from Microsoft that is built on top of Microsoft Visual Studio 2010 Express for Windows Phone 7™, allowing game developers to utilize the simplicity of .NET based programming languages such as C# and the power and robustness of Visual Studio 2010 as the IDE (Integrated Development Environment) for their games development.

XNA Game Studio 4.0 includes the XNA Framework and the XNA Framework Content Pipeline.The XNA Framework is the runtime engine and class libraries (an extension to the .NET runtime and class libraries) which provide a robust game-focused Application Programming Interface (API), which simplifies the development of games for the Xbox 360™, Windows® based PCs and now for the Windows Phone 7™ Series®.

The XNA Content Pipeline is a set of content importers and processors, integrated into the development environment, which provides an easy and flexible way to import, load, process and use three-dimensional (3D) models, textures, images, sounds, and other assets in your game. The XNA Content Pipeline is extensible, allowing you to create custom content importers and/or processors to add support for potentially any kind of resources and asset formats, or to add custom data to existing asset types in loading time to be further utilized in runtime.

The XNA Game Studio is indeed an easy-to-use development environment and programming framework developed by Microsoft to help developers make games faster and easier, however, it is not a "drag-and-drop" visual game creation tool. It is a programming environment and you are required to know to program in C# and to have object-oriented programming skills in order to use it.

The XNA Framework is not a game engine. It does not include camera management, state/screen/level management, physics, collision monitoring, or other features often found in game engines. It is a game development framework, meaning that the way your game works depends solely on your programming.

During this lab, you will build a full 3D game for the Windows Phone 7™. The game you will build, “Marble Maze”, is a single player game in which the player guides a marble through a 3D maze, and attempts to reach the end in the shortest time possible, while avoiding dropping the marbles into holes in the ground (which will make the marble respawn in the last visited checkpoint). The player makes the marble roll through the maze by tilting the device, which in turn affects the tilt of the maze game-board. Once the user reaches the end of the maze, the result time is compared against the fastest times stored on the device. If the time is in the top-ten high-scores, players will be able to record their name into the high-score table.

XNA Game Studio Game Basics

A game usually has three phases:

  • Initializing and Loading – In this phase, we load resources, initialize game-related variables, and perform any other tasks that have to be performed before the game actually begins. This phase occurs only once in the game’s life cycle.
  • Update – In this phase, we update the game-world state. Usually this means calculating the new position/orientation of game objects according to the game’s physics, handling user input and acting accordingly, triggering sound effects, updating health, ammo, and other statuses, updating the score and performing other game-related logic. This phase occurs repeatedly throughout the time that the game engine is active, as part of the game's main loop.
  • Draw – In this phase, we draw the current game scene to the output graphic device, as a single frame, visually representing the current game state. This phase occurs repeatedly throughout the time that the game engine is active, as part of the game's main loop.

In the XNA Framework, the Update and Draw phases are executed up to 60 times per second by default on a PC or Xbox 360™ and up to 30 times per second on a Zune™, Zune HD™ or Windows Phone 7™ device.

General Architecture

The "Marble Maze" game uses the game screen management architecture from the Game State Management sample (originally found at http://create.msdn.com/en-US/education/catalog/sample/game_state_management), which provides some of the assets for this lab. The game includes the following screens:

  • Main Menu screen (MainMenuScreen class)
  • High Scores Table screen(HighScoreScreen class)
  • Gameplay screen (GameplayScreen class)
  • Paused (PauseScreen class)
  • Accelerometer calibration screen (CalibrationScreen class)

The Game performs game-specific content loading just before displaying the gameplay screen, so as to avoid any noticeable delay before the game begins.

When launched, the game’s first action is to load and display the background screen and then the main menu screen. Once the main menu screen is loaded, the user can access the game itself, or view the high score.

The completed game will look as follows:

Figure 1

Finished Marble Maze Game

Task 1 – 3D Drawing

During this task, you will enhance your MarbleMazeGame XNA game project and add 3D drawing capabilities to it.

Before we can actually draw 3D models, we should fully understand the Windows Phone 7 3D axes system.

The traditional three axes represent the Windows Phone 7 coordinate system: X, Y, and Z.

Moving along the X axis means progressing from left-to-right, thus the X value increases as we go further to the right, and vice-versa.

The Y axis acts the same way, bottom-to-top, with the Y value increasing as we move upwards.

The Z axis represent the depth dimension. It increases as we move the virtual drawing point towards the phone's screen, and vice-versa.

Figure 2

Axes- X, Y, Z in Portrait mode

The above figure illustrates the axes system when drawing in portrait mode. As you will see in the next figure, which shows the axes system when drawing in landscape mode, the phone's current position does not change the axes system as the Y axis always represents drawing from the ground up, the X axis from left-to-right and the Z axis from the phone toward the user.

Figure 3

Axes- X, Y, Z in Landscape mode

The main implication of the above is that screen drawing is always performed so that it looks intuitive to the phone user; no matter how the phone is being held (the picture is always correctly aligned).

A programmer rendering the 3D model to the screen will probably not be able to ignore the device’s orientation, however, as the current orientation will probably change the proportions at which to draw.

  1. Start Visual Studio 2010 Express for the Windows Phone or Visual Studio 2010.
  2. Open the Source\Ex1-MarbleMazeGame\Begin solution file, and review the code.
  3. Open the class file named Camera.cs under the “Objects” folder.
  4. Change the new class to derive from the GameComponent class (defined in the "Microsoft.Xna.Framework" namespace). We use this base class in order to inherit a certain set of methods, which logically fit the camera as a game component that is not drawn to the screen.

    C#

    namespace MarbleMazeGame { public class Camera : GameComponent { ... } }
  5. Add the following code to the class, defining the view port location and projection. Sadly, it is outside the scope of this lab to explain just what the view port and project are. These are fairly standard terms which are explained in most 3D rendering background materials:

    C#

    #region Initializtion public Camera(Game game, GraphicsDevice graphics) : base(game) { this.graphicsDevice = graphics; } /// <summary> /// Initialize the camera /// </summary> public override void Initialize() { // Create the projection matrix Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(50), graphicsDevice.Viewport.AspectRatio, 1, 10000); // Create the view matrix View = Matrix.CreateLookAt(position, target, Vector3.Up); base.Initialize(); } #endregion
  6. Now that we have our camera through which to view 3D objects, we can create the objects themselves.
  7. Open the class file named DrawableComponent3D.cs under the “Objects” folder.
  8. Override the base class’s LoadContent functionality to load the actual 3D model resources:

    C#

    protected override void LoadContent() { // Load the model Model = Game.Content.Load<Model>(@"Models\" + modelName); // Copy the absolute transforms AbsoluteBoneTransforms = new Matrix[Model.Bones.Count]; Model.CopyAbsoluteBoneTransformsTo(AbsoluteBoneTransforms); base.LoadContent(); }

    This code loads the object's model from the game content project (we will add these models to the content project at a later stage) and transforms the model in order for it to be properly positioned.

  9. Add custom 3D drawing logic to the class by overriding the Draw method:

    C#

    public override void Draw(GameTime gameTime) { foreach (ModelMesh mesh in Model.Meshes) { foreach (BasicEffect effect in mesh.Effects) { // Set the effect for drawing the component effect.EnableDefaultLighting(); effect.PreferPerPixelLighting = preferPerPixelLighting; // Apply camera settings effect.Projection = Camera.Projection; effect.View = Camera.View; // Apply necessary transformations effect.World = FinalWorldTransforms; } // Draw the mesh by the effect that set mesh.Draw(); } base.Draw(gameTime); }

    This code goes through all the meshes in the model, applies mesh effects for each of them and draws them.

  10. Add update functionality to the class, by introducing the following methods:

    C#

    public override void Update(GameTime gameTime) { // Update the final transformation to properly place the component in the // game world. UpdateFinalWorldTransform(); base.Update(gameTime); } protected virtual void UpdateFinalWorldTransform() { FinalWorldTransforms = Matrix.Identity * Matrix.CreateFromYawPitchRoll(Rotation.Y, Rotation.X, Rotation.Z) * OriginalWorldTransforms * Matrix.CreateTranslation(Position); }

    The above code updates the components transformation matrix according to its current state. We will not update 3D object states until the next exercise.

  11. Now that we have a "DrawableComponent3D" class, we will concentrate on the "Maze" and "Marble" derivatives to manage and display the corresponding 3D objects.
  12. Open the class Marble.cs under "Objects" folder.
  13. Add a Draw override method, to replace the base implementation with one that properly renders a marble:

    C#

    public override void Draw(GameTime gameTime) { var originalSamplerState = GraphicsDevice.SamplerStates[0]; // Cause the marble's textures to linearly clamp GraphicsDevice.SamplerStates[0] = SamplerState.LinearClamp; foreach (var mesh in Model.Meshes) { foreach (BasicEffect effect in mesh.Effects) { // Set the effect for drawing the marble effect.EnableDefaultLighting(); effect.PreferPerPixelLighting = preferPerPixelLighting; effect.TextureEnabled = true; effect.Texture = m_marbleTexture; // Apply camera settings effect.Projection = Camera.Projection; effect.View = Camera.View; // Apply necessary transformations effect.World = AbsoluteBoneTransforms[mesh.ParentBone.Index] * FinalWorldTransforms; } mesh.Draw(); } // Return to the original state GraphicsDevice.SamplerStates[0] = originalSamplerState; }
  14. Open the "Maze" class under the "Objects" project folder and examine this class. The code is fairly similar to what we have done in the “Marble” class.
  15. Finally, we need to create the gameplay screen which will actually contain and draw all the objects which we have just created.
  16. Open the class GameplayScreen under the folder “Screens”.
  17. Localize InitializeCamera method, inside Loading region and add the highlighted lines.

    C#

    private void InitializeCamera() { //Createthe camera camera=newCamera(ScreenManager.Game, ScreenManager.GraphicsDevice); camera.Initialize(); }

    These new methods simply initialize the various 3D objects during the screen’s loading phase.

  18. Add custom update and drawing logic to the gameplay screen by introducing the following overrides:

    C#

    public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { // Update all the component of the gamemaze.Update(gameTime); marble.Update(gameTime); camera.Update(gameTime); } public override void Draw(GameTime gameTime) { ScreenManager.GraphicsDevice.Clear(Color.Black); ScreenManager.SpriteBatch.Begin(); // Drawing sprites changes some render states around, which don't play // nicely with3dmodels. // In particular, we need to enable the depth buffer. DepthStencilStatedepthStensilState =        new DepthStencilState() { DepthBufferEnable =true}; ScreenManager.GraphicsDevice.DepthStencilState = depthStensilState; // Draw all the game components  maze.Draw(gameTime); marble.Draw(gameTime); ScreenManager.SpriteBatch.End(); base.Draw(gameTime); }

    These overrides defer most of the works to the 3D objects themselves in order to render them to the screen.

  19. Finally, we modified the main game class to introduce the gameplay screen using the ScreenManager. Open the “MarbleMazeGame.cs” file and examine its code.
  20. Build and deploy the project. Instead of a blank screen, you should now see the game’s 3D objects, the maze and the marble, on the display.

    Figure 4

    The game objects rendered on the screen

Task 2 – 3D Movement and Camera

While our game now presents the user with the various game elements on screen, it is not much of a game since the user is unable to interact with it. In the final game the user will be able to move the maze in order to navigate the marble across it and we will now focus on allowing the user to do just that. The game uses accelerometer input in the device and keyboard while running in emulator in order to navigate the marble across the maze.

Understanding Accelerometer Input

When reading accelerometer input we should note that there is a major difference from the previously described drawing axes system: the accelerometer axes follow the device, and are not orientation agnostic like the drawing axes.

Please see the following figure that illustrates the how the accelerometer’s X values are changed when tilting the device around the accelerometer’s Y axis:

Figure 5

Changing the accelerometer’s X value by tilting the device sideways

As you can see in the figure, rotating the phone clockwise over the Y axis causes the accelerometer to return larger X values. Tilting it counter-clockwise will return increasingly smaller values.

Again, note that the accelerometer’s Y axis remains the same axis, going across the phone from its buttons and to the other side, regardless of the device’s orientation!

The next figure shows the changes in accelerometer input when tilting the phone over the X axis:

Figure 6

Changing the accelerometer’s Y value by tilting the device up or down

The accelerometer’s Z values are a bit different, as the Z values do not change by rotation over an axis, but rather by movement along an axis.

The accelerometer will return negative Z values when the phone is being lifted upwards from the ground, and positive values when the phone is being "dropped" downwards, as you can see in the following figure:

Figure 7

Accelerometer returning positive Z values when the whole phone is lowered

The accelerometer’s X and Y values remain constant when tilting it at a specific angle (for example, tilting the phone 45 degrees to the right will always return 0.5 as the accelerometer’s X value. Z values, on the other hand, represent actual motion and not the current height of the phone! Positive/negative Z values represent that the phone is being moved downwards/upwards respectively at a certain rate and will return to 0 when the phone rests at a certain height.

  1. Open the “GameplayScreen.cs” file under the “Screens” project folder of the “MarbleMazeGame” project and add the some additional fields to the GameplayScreen class:

    C#

    Readonly float angularVelocity = MathHelper.ToRadians(1.5f); Vector3? accelerometerState=Vector3.Zero;

    We will use the above fields to interact with the device’s built-in accelerometer.

  2. Navigate to the GameplayScreen’s LoadContent method and alter it to look like the following (old code is colored gray):

    C#

    public override void LoadContent(){LoadAssets(); Accelerometer.Initialize(); base.LoadContent();}

    You may wonder why we initialize the accelerometer in the content loading phase, instead of the initialization phase. The reason is that it is recommended to initialize the accelerometer, or more precisely, call its “Start” method, as late as possible. The content loading phase is the last phase before the update/draw cycles begin and so we perform the initialization there. We could perform the initialization during the first update cycle, but that would mean needlessly adding a conditional statement to the update loop.

  3. Open the file “GameplayScreen.cs” under Screens folder and override the HandleInput method in order to allow the gamesplay screen to react to user input:

    C#

    public override void HandleInput(InputState input) { if (input == null) throw new ArgumentNullException("input"); // 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)); } }

    While the above method is long, it is fairly simple. We check whether we are running on an emulator or on an actual device and handle the Accelerometer class’s input differently in both cases since when using an emulator accelerometer data is keyboard generated. We also make sure to limit the maze’s rotation to 30 degrees in both cases.

  4. Compile and deploy your game. You should now be able to rotate the rendered elements using keyboard or accelerometer input.

    Figure 8

    The gameplay screen, after performing some rotation

    Note:
    To get the emulator to respond to keyboard input, you must first press Page-Up while the emulator is in focus.

    Now that we can rotate the maze, it is time to change the camera so that it will follow the marble. This behavior will be useful once the ball actually rolls through the maze.

  5. Open the Camera.cs file located in the Objects project folder and alter the Camera class’s fields. Since we no longer want a fixed camera, we can modify the “position” and “target” fields and add additional fields. Eventually, the class should contain the following fields (fields which were previously present and were not changed are colored gray):

    C#

    private Vector3 position = Vector3.Zero; private Vector3 target = Vector3.Zero; private GraphicsDevice graphicsDevice; public Vector3 ObjectToFollow {get;set; } public Matrix Projection { get; set; } public Matrix View { get; set; } private readonly Vector3 cameraPositionOffset = new Vector3(0, 450, 100); private readonly Vector3 cameraTargetOffset = new Vector3(0, 0, -50);
  6. Alter the Camera class’s Initialize override, since we can no longer use it to set the camera’s view as now the view has to follow the marble (and “Initialize” only occurs once). To do this, remove the assignment to the View property, so that the method looks like the following:

    C#

    public override void Initialize() { // Create the projection matrix Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(50),graphicsDevice.Viewport.AspectRatio, 1, 10000); base.Initialize(); }
  7. Add an override for the Update method. This is where we will make sure the camera follows the marble:

    C#

    public override void Update(GameTime gameTime) { // Make the camera follow the object position = ObjectToFollow + cameraPositionOffset; target = ObjectToFollow + cameraTargetOffset; // Create the view matrix View = Matrix.CreateLookAt(position, target,Vector3.Up); base.Update(gameTime); }

    The above code positions the camera at a position relative to the “ObjectToFollow”, and points it at a target position, which is also relative to “ObjectToFollow”. To make sure that “ObjectToFollow” is always properly set, we will need to modify the Marble class.

  8. Open the Marble.cs file from the Objects project folder and override for the Update method in the Marble class:

    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)); }
  9. The final step will be to associate the maze with the marble. Open the GameplayScreen.cs file in the Screens project folder and alter the GameplayScreen class’s InitializeMarble method to look like the following:

    C#

    private void InitializeMarble() { marble = newMarble(ScreenManager.GameasMarbleMazeGame) { Position =newVector3(100, 0, 0), Camera = camera, Maze = maze }; marble.Initialize(); }
  10. Compile and deploy your project. The camera should now follow the marble. Camera behavior might seem a little strange but this is because the marble is currently stuck in space instead of properly interacting with the maze. We will fix this in the next task.

    Figure 9

    The camera now follows the marble

Task 3 – Physics and Collision

While we have made the game interactive in the previous task, it is not very fun to play as the only thing with which a user can interact is the game’s camera. The focus of this task will be to make the game fully playable by adding physics and collision detection so that the user will be able to navigate the marble through the maze.

We will begin by creating a custom content processor which will enhance the maze model with additional information that will help us when we implement collision detection between the marble and the maze.

  1. Open the project name MarbleMazePipeline
  2. Now open the MarbleMazeProcessor.cs file. The MableMazeProcessor class contained in this file will serve as the content processor implementation. This content processor will attach to a model’s “Tag” property a dictionary that maps the model’s mesh names to a set of vertices, defined in the corresponding mesh. We will use this information later on for collision calculations.
  3. Add a helper method to the MableMazeProcessor class:

    C#

    void FindVertices(NodeContent node) { //Isthis node a mesh? MeshContent mesh =node as MeshContent; if(mesh !=null) { stringmeshName = mesh.Name; List<Vector3> meshVertexs = new List<Vector3>(); // Look up the absolute transform of the mesh. MatrixabsoluteTransform = mesh.AbsoluteTransform; // Loop over all the pieces of geometry in the mesh. foreach(GeometryContentgeometryinmesh.Geometry) { // Loop over all the indices in this piece of geometry. //Everygroup of three indices represents one triangle. foreach(intindexingeometry.Indices) { // Look up the position of this vertex. Vector3vertex =geometry.Vertices.Positions[index]; // Transform from local into world space. vertex=Vector3.Transform(vertex, absoluteTransform); // Store this vertex. meshVertexs.Add(vertex); } } tagData.Add(meshName, meshVertexs); } // Recursively scan over the children of this node. foreach(NodeContentchildinnode.Children) { FindVertices(child); } }

    The above method simply scans a model recursively and builds the dictionary mentioned in step 2. Note that as one of the comments in the code specify, we are assured that each consecutive set of three vertices defines a triangle which belongs to the mesh.

  4. Finally, we will override the Process method in order to perform our custom content processing, by using the helper method we have just defined:

    C#

    public override ModelContent Process(NodeContent input, ContentProcessorContext context) { FindVertices(input); ModelContentmodel =base.Process(input, context); model.Tag = tagData; returnmodel; }
  5. Right click the maze model file, “maze1.FBX”, under the content project’s “Models” folder in “MarbleMazeGameContent” project, and select Properties.
  6. In the properties window, select “MarbleMazeProcessor” as the content processor.
  7. Now it is time to add physics to the game. The game’s physics will eventually comprise for most of the game logic, controlling the way the marble rolls across the maze, collides with walls or falls through openings in the maze’s floor. We will begin by adding a code asset which we will later use for collision detection.
  8. Examine the file name IntersectDetails.cs under Misc folder. The structure defined inside (IntersectDetails) will be used to store collision information, namely what sort of collision occurred and which parts of the maze were involved in it.
  9. We will move on to updating the DrawableComponent3D class, which serves as a base to all entities that are affected by physics.Open the “DrawableComponent3D.cs” file from the “Objects” project folder and change the DrawableComponent3D class’s field definitions by adding additional fields:

    C#

    Public const floatgravity = 100 * 9.81f; Public const floatwallFriction = 100 * 0.8f; Protected IntersectDetails intersectDetails = new IntersectDetails(); Protected float staticGroundFriction = 0.1f; Public Vector3 Velocity =Vector3.Zero; Public Vector3 Acceleration =Vector3.Zero;
  10. Add the highlighted line to the DrawableComponent3D class’s Update override:

    C#

    public override void Update(GameTime gameTime){// Perform physics calculationsCalcPhysics(gameTime);    ...}

    As you can see, the only thing changed is that we now update the component’s physics as part of the update phase.

  11. Add the method which appears in the above step, CalcPhysics, to the class:

    C#

    protected virtual void CalcPhysics(GameTime gameTime) { CalculateCollisions(); CalculateAcceleration(); CalculateFriction(); CalculateVelocityAndPosition(gameTime); }

    The above method denotes a certain order of physics calculations. We will first calculate collisions, acceleration and friction, and only then will we calculate the component’s actual velocity and position in light of previous. We will now implement the four methods above, which inheriting classes will be able to override in case they require calculations that differ from the default.

  12. Since a general component does not move in any particular way, we will simply add all the above methods as abstract:

    C#

    protected abstract void CalculateFriction(); protected abstract void CalculateAcceleration(); protected abstract void CalculateVelocityAndPosition(GameTime gameTime); protected abstract void CalculateCollisions();

    This concludes our treatment of the DrawableComponent3D. We will move on to the Maze class.

  13. Open the “Maze.cs” file inside the “Objects” project folder. Add the following fields to the Maze class:

    C#

    publicList<Vector3> Ground = new List<Vector3>(); publicList<Vector3> Walls = new List<Vector3>(); publicList<Vector3> FloorSides = new List<Vector3>(); publicLinkedList<Vector3> Checkpoints = new LinkedList<Vector3>(); publicVector3StartPoistion; publicVector3End;

    The first three fields will be used to store vertices belonging to the ground, walls and “floor sides” (the pits’ inner walls) arranged into triangles as previously discussed while we were adding a custom content processor. That custom content processor supplies the data that we will use to populate these fields.

  14. Override the LoadContent method in the Maze class:

    C#

    protected override void LoadContent() { base.LoadContent(); // Load the start & end positions of the maze from the bone StartPoistion = Model.Bones["Start"].Transform.Translation; End = Model.Bones["Finish"].Transform.Translation; // Get the maze's triangles from its mesh Dictionary<string, List<Vector3>> tagData = (Dictionary<string, List<Vector3>>)Model.Tag; Ground = tagData["Floor"]; FloorSides = tagData["floorSides"]; Walls = tagData["walls"]; // Add checkpoints to the maze Checkpoints.AddFirst(StartPoistion); foreach (var bone in Model.Bones) { if (bone.Name.Contains("spawn")) { Checkpoints.AddLast(bone.Transform.Translation); } } }

    The above method is rather straightforward and it simply populates the fields introduced in the previous step with data added to the model by our custom content processor. The final section of code defines checkpoints where the marble will respawn after falling into a pit, assuming the checkpoint has been activated by having the marble roll over it.

  15. As the maze itself is not affected by physics in any way, we will simply add the following empty method implementations. Locate the Overriding physics calculations region and unmark the 4 override methods (CalculateCollisions, CalculateVelocityAndPosition, CalculateFriction, CalculateAcceleration):
  16. Finally, we will add a helper method called GetCollisionDetails to the Maze class:

    C#

    public void GetCollisionDetails(BoundingSphere BoundingSphere, ref IntersectDetails intersectDetails, bool light) { intersectDetails.IntersectWithGround =TriangleSphereCollisionDetection.IsSphereCollideWithTringles(Ground,        BoundingSphere, out intersectDetails.IntersectedGroundTriangle, true); intersectDetails.IntersectWithWalls =TriangleSphereCollisionDetection.IsSphereCollideWithTringles(Walls,BoundingSphere,out intersectDetails.IntersectedWallTriangle, light); intersectDetails.IntersectWithFloorSides =TriangleSphereCollisionDetection.IsSphereCollideWithTringles( FloorSides, BoundingSphere,out intersectDetails.IntersectedFloorSidesTriangle, true); }

    This method will allow us to give the maze a bounding sphere and get back intersection details that will tell us with which parts of the maze the sphere collides.

    We will now advance to the Marble class, which will require the most work when we come to implement its physics.

  17. Open the “Marble.cs” file under the “Objects” project folder and add the following property to the Marble class:

    C#

    public BoundingSphere BoundingSphereTransformed { get { BoundingSphereboundingSphere =Model.Meshes[0].BoundingSphere; boundingSphere = boundingSphere.Transform(AbsoluteBoneTransforms[0]); boundingSphere.Center += Position; return boundingSphere; } }

    This property will return the marble’s bounding sphere with the marble’s 3D transformations taken into account, which is required for the bounding sphere to match the marble’s representation in the game world.

  18. Override the DrawableComponent3D class’s UpdateFinalWorldTransform in the Marble class:

    C#

    protected override void UpdateFinalWorldTransform() { // Calculate the appropriate rotation matrix to represent the marble // rolling inside the maze rollMatrix *=Matrix.CreateFromAxisAngle(Vector3.Right, Rotation.Z) *Matrix.CreateFromAxisAngle(Vector3.Forward, Rotation.X); // Multiply by two matrices which will place the marble in its proper // position and align it to the maze (which tilts due to user input) FinalWorldTransforms = rollMatrix *Matrix.CreateTranslation(Position) *Matrix.CreateFromYawPitchRoll(Maze.Rotation.Y, Maze.Rotation.X, Maze.Rotation.Z); }

    This override will cause the marble model to rotate according to its “Rotation” value, which will cause it to appear as if it is actually rolling while it moves.

  19. Override the CalculateCollisions method. We will simply take the marble’s bounding sphere and give it to the maze in order to perform collision calculations and store them in one of the Marble class’s fields.

    C#

    protected override void CalculateCollisions() { Maze.GetCollisionDetails(BoundingSphereTransformed,refintersectDetails, false); if(intersectDetails.IntersectWithWalls) { foreach(vartriangleinintersectDetails.IntersectedWallTriangle) { Axisdirection =CollideDirection(triangle); if((direction &Axis.X) ==Axis.X && (direction&Axis.Z) ==Axis.Z) { Maze.GetCollisionDetails(BoundingSphereTransformed, Ref intersectDetails,true); } } } }
  20. Override the CalculateAcceleration method. This method will modify the marble’s acceleration in light of the maze’s tilt:

    C#

    protected override void CalculateAcceleration() { if (intersectDetails.IntersectWithGround) { // We must take both the maze's tilt and the angle of the floor // section beneath the marble into account angleX = 0; angleZ = 0; if (intersectDetails.IntersectedGroundTriangle != null) { intersectDetails.IntersectedGroundTriangle.Normal(out normal); angleX = (float)Math.Atan(normal.Y / normal.X); angleZ = (float)Math.Atan(normal.Y / normal.Z); if (angleX > 0) { angleX = MathHelper.PiOver2 - angleX; } else if (angleX < 0) { angleX = -(angleX + MathHelper.PiOver2); } if (angleZ > 0) { angleZ = MathHelper.PiOver2 - angleZ; } else if (angleZ < 0) { angleZ = -(angleZ + MathHelper.PiOver2); } } // Set the final X, Y and Z axis acceleration for the marble Acceleration.X = -gravity * (float)Math.Sin(Maze.Rotation.Z - angleX); Acceleration.Z = gravity * (float)Math.Sin(Maze.Rotation.X - angleZ); Acceleration.Y = 0; } else { // If the marble is not touching the floor, it is falling freely Acceleration.Y = -gravity; } if (intersectDetails.IntersectWithWalls) { // Change the marble's acceleration due to a collision with a maze // wall UpdateWallCollisionAcceleration( intersectDetails.IntersectedWallTriangle); } if (intersectDetails.IntersectWithFloorSides) { // Change the marble's acceleration due to collision with a pit wall UpdateWallCollisionAcceleration( intersectDetails.IntersectedFloorSidesTriangle); } }

    The only non-trivial part of the method is the part directly below the first comment, which simply calculates the floor’s own angle to have it affect the overall slope on which the marble is currently placed. Near the end of the method, we use a helper function to update the acceleration values in light of collisions. We will now implement this helper method, but first we introduce another method.

  21. Add the following method to the Marble class:

    C#

    protected Axis CollideDirection(Triangle collideTriangle) { if(collideTriangle.A.Z == collideTriangle.B.Z &&collideTriangle.B.Z == collideTriangle.C.Z) { returnAxis.Z; } else if(collideTriangle.A.X == collideTriangle.B.X &&collideTriangle.B.X == collideTriangle.C.X) { returnAxis.X; } else if(collideTriangle.A.Y == collideTriangle.B.Y &&collideTriangle.B.Y == collideTriangle.C.Y) { returnAxis.Y; } returnAxis.X |Axis.Z; }

    This method simply inspects a triangle’s points in order to determine the plane it is on and returns the axis that is perpendicular to it.

  22. Now add the following method to the Marble class, which will alter acceleration in light of collisions:

    C#

    protected void UpdateWallCollisionAcceleration(IEnumerable<Triangle> wallTriangles) { foreach(vartriangleinwallTriangles) { Axis direction = CollideDirection(triangle); // Decrease the acceleration in x-axis of the component if((direction &Axis.X) ==Axis.X) { if(Velocity.X > 0)Acceleration.X -= wallFriction; else if(Velocity.X < 0) } // Decrease the acceleration in z-axis of the component if((direction &Axis.Z) ==Axis.Z) { if(Velocity.Z > 0)Acceleration.Z -= wallFriction; else if(Velocity.Z < 0)Acceleration.Z += wallFriction; } }

    The method simply gives the marble an acceleration component which is inverse to the direction it hit a wall.

  23. Override the CalculateFriction method to introduce specific friction calculations to the Marble class:

    C#

    protected override void CalculateFriction() { if(intersectDetails.IntersectWithGround) { if(Velocity.X > 0) { Acceleration.X -= staticGroundFriction * gravity *(float)Math.Cos(Maze.Rotation.Z - angleX); } else if(Velocity.X < 0) { Acceleration.X += staticGroundFriction * gravity * } if(Velocity.Z > 0) { Acceleration.Z -= staticGroundFriction * gravity *(float)Math.Cos(Maze.Rotation.X - angleZ); } else if(Velocity.Z < 0) { Acceleration.Z += staticGroundFriction * gravity *(float)Math.Cos(Maze.Rotation.X - angleZ); } } }

    The above method simply adds an acceleration component inverse to the marble’s current velocity and proportional to the slope on which the marble is currently placed.

  24. The last thing to do in order to fully support the marble’s physics is to update its velocity and position by overriding CalculateVelocityAndPosition:

    C#

    protected override void CalculateVelocityAndPosition(GameTime gameTime) { // Calculate the current velocity float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds; Vector3 currentVelocity = Velocity; Velocity = currentVelocity + (Acceleration * elapsed); // Set a bound on the marble's velocity Velocity.X =MathHelper.Clamp(Velocity.X, -250, 250); Velocity.Z =MathHelper.Clamp(Velocity.Z, -250, 250); if(intersectDetails.IntersectWithGround) { Velocity.Y = 0; } if(intersectDetails.IntersectWithWalls) { UpdateWallCollisionVelocity(intersectDetails.IntersectedWallTriangle,ref currentVelocity); } if(intersectDetails.IntersectWithFloorSides) { UpdateWallCollisionVelocity(intersectDetails.IntersectedFloorSidesTriangle, ref currentVelocity); } // If the velocity is low, simply cause the marble to halt if(-1< Velocity.X && Velocity.X < 1) { Velocity.X = 0; } if(-1< Velocity.Z && Velocity.Z < 1) { Velocity.Z = 0; } // Update the marble's position UpdateMovement((Velocity +currentVelocity) / 2, elapsed); }

    The above method changes the marble’s velocity according to its calculated acceleration and the time interval between two consecutive calls to the method. The method also alters the marble’s velocity in case of a collision using a helper method we will soon implement and halts the marble if its speed is sufficiently low. The marble’s position is also updated by a helper method, which we will implement shortly.

  25. Add the following method to the Marble class:

    C#

    protected void UpdateWallCollisionVelocity(IEnumerable<Triangle> wallTriangles,ref Vector3 currentVelocity) { foreach(vartriangleinwallTriangles) { Axisdirection = CollideDirection(triangle); // Swap the velocity between x & z if the wall is diagonal if((direction &Axis.X) ==Axis.X && (direction &Axis.Z) ==Axis.Z) { float tmp = Velocity.X; Velocity.X = Velocity.Z; Velocity.Z = tmp; tmp = currentVelocity.X; currentVelocity.X = currentVelocity.Z * 0.3f; currentVelocity.Z = tmp * 0.3f; } // Change the direction of the velocity in the x-axis else if((direction &Axis.X) ==Axis.X) { if((Position.X > triangle.A.X && Velocity.X < 0) ||(Position.X < triangle.A.X && Velocity.X > 0)) { Velocity.X = -Velocity.X * 0.3f; currentVelocity.X = -currentVelocity.X * 0.3f; } } // Change the direction of the velocity in the z-axis else if((direction &Axis.Z) ==Axis.Z) { if((Position.Z > triangle.A.Z && Velocity.Z < 0) ||(Position.Z < triangle.A.Z && Velocity.Z > 0)) { Velocity.Z = -Velocity.Z * 0.3f; currentVelocity.Z = -currentVelocity.Z * 0.3f; } } } }

    The above method simply reverses the marble’s velocity upon hitting a straight wall while also reducing it. If, however, the wall is diagonal then the marble’s velocity will be shifted between the X and Z axes, assuming the maze only has diagonal walls the angle of which is 45 degrees.

  26. Add the last physics-related method to the Marble class:

    C#

    private void UpdateMovement(Vector3 deltaVelocity,float deltaTime){ // Calculate the change in the marble's position Vector3 deltaPosition = deltaVelocity * deltaTime; // Before setting the new position, we must make sure it is legal BoundingSphere nextPosition =this.BoundingSphereTransformed; nextPosition.Center += deltaPosition; IntersectDetails nextIntersectDetails =new IntersectDetails(); Maze.GetCollisionDetails(nextPosition,ref nextIntersectDetails, true); nextPosition.Radius += 1.0f; // Move the marble Position += deltaPosition; // If the floor not straight then we must reposition the marble vertically Vector3 forwardVecX =Vector3.Transform(normal,Matrix.CreateRotationZ(-MathHelper.PiOver2)); Vector3 forwardVecZ =Vector3.Transform(normal,Matrix.CreateRotationX(-MathHelper.PiOver2)); bool isGroundStraight =true; if(forwardVecX.X != -1 && forwardVecX.X != 0) { Position.Y += deltaPosition.X / forwardVecX.X * forwardVecX.Y; isGroundStraight =false; } if(forwardVecZ.X != -1 && forwardVecZ.X != 0) { Position.Y += deltaPosition.Z / forwardVecZ.Z * forwardVecZ.Y; isGroundStraight =false; } // If the marble is already inside the floor, we must reposition it if(isGroundStraight && nextIntersectDetails.IntersectWithGround) { Position.Y = nextIntersectDetails.IntersectedGroundTriangle.A.Y +BoundingSphereTransformed.Radius; } // Finally, we "roll" the marble in accordance to its movement if(BoundingSphereTransformed.Radius != 0) { Rotation.Z = deltaPosition.Z / BoundingSphereTransformed.Radius; Rotation.X = deltaPosition.X / BoundingSphereTransformed.Radius; } }

    The above method simply repositions the marble according to its current velocity, but then corrects its position to avoid having it go through a wall or a diagonal portion of the floor, as the marble must move up and down to accommodate the slopes of the maze’s floor.

  27. Open the class GameplayScreen and navigate to the LoadContent method and modify it to look like the following:

    C#

    public override void LoadContent() { LoadAssets(); timeFont = ScreenManager.Game.Content.Load<SpriteFont>(@"Fonts\MenuFont"); Accelerometer.Initialize(); base.LoadContent(); }
  28. Navigate to the InitializeMaze method and add the following highlighted code to initialize the first checkpoint, which is actually the maze’s start location:

    C#

    private void InitializeMaze() { ... //Save the last checkpoint lastCheackpointNode = maze.Checkpoints.First; }
  29. Modify the GameplayScreen class’s InitializeMarble method to set the marble’s initialize position to the maze’s start location. Add the highlighted code:

    C#

    private void InitializeMarble()
    FakePre-83877062991c4586b15f792f481f9a74-10d7ea1114d742f2a8de5834106b8a21FakePre-3584142ce6c648a2a12fc7b96087e95f-d862b428a8f24dac8dfeeb6d8bf4d166FakePre-2ba93bea971c42868148ddaed371fab1-f32cc2953618431286a665bc3dd75a38FakePre-1b7d1eb3fb95408b9f13fb9d35774eed-e087ec3bfc7444e5b58168602c853b50...FakePre-55a8ff8cf55d4c89b7193c5d9989648f-16e9e1154cbf4627920c15c7e03bf461FakePre-9067b8b4843e4535b0b2911e6890fc09-089797a6d267419e9d485d03e1efbf68FakePre-5d86ed046b6f44a0aca0a13f98f0eb70-a59f2bb351dc4480b673dbe0a0c4f079
  30. Revise the HandleInput method, by changing it to the following implementation:

    C#

    public override void HandleInput(InputState input) { if (input == null) throw new ArgumentNullException("input"); // 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)); } }

    The main changes are that input will no longer be handled if the game is not active, and that the accelerometer input is affected by a calibration vector. We will introduce the purpose of the calibration vector in the next exercise. Also, if the game is over then a tap method will initiate the game ending sequence using a helper method we will soon implement.

  31. Revise the Update method to perform the additional checks regarding the game’s state:

    C#

    public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { // 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); }

    Other than keeping track of the total game time, we also use helper methods, which we will soon implement, to check whether the ball has fallen into a pit, to update the last passed checkpoint and to see whether the game has ended. An additional helper method handles the game’s end sequence.

  32. Localize the UpdateLastCheackpoint method and uncomment its content. This method examines all checkpoints further away in the maze than the current one and assuming the marble is close enough to one of them, it is set as the current checkpoint

    C#

    private void UpdateLastCheackpoint() { ... }
  33. Locate the CheckFallInPit method and uncomment its content. This method resets the marble’s location to the last checkpoint in case it has indeed fallen inside a pit.

    C#

    private void CheckFallInPit() { ... }
  34. Locate the CheckGameFinish method and uncomment its content. This method checks whether the game has ended. This only happens when the player reaches the maze’s end:

    C#

    Private void CheckGameFinish() { ... }
  35. Finally, we will revise the Draw method to display the total elapsed time and to only work when the game is active. The elapsed time serves as the player’s score in the eventual game, as the aim of the game is to complete the maze in as short a time as possible:

    C#

    public override void Draw(GameTime gameTime) { ScreenManager.GraphicsDevice.Clear(Color.Black); ScreenManager.SpriteBatch.Begin(); // 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); ScreenManager.SpriteBatch.End(); base.Draw(gameTime); }
  36. Compile and deploy the game. The game should now be fully playable, though the game experience will be lacking. Currently, the game begins very abruptly, does not end when the player reaches the end of the maze, and has no sounds. In the next exercise, we will address these issues by adding a menu system, a high-score table that will display once the game ends and sound playback. We will also introduce a calibration screen, which will allow the accelerometer to be calibrated once the game is deployed to an actual device.