Share via


FuelCell: Casting Call

Discusses the implementation of the remaining game elements, such as barriers, fuel cells, and an avatar model.

The Complete Sample

The code in this tutorial illustrates the technique described in the text. A complete code sample for this tutorial is available for you to download, including full source code and any additional supporting files required by the sample.

Download FuelCell_3_Sample.zip.

Note

You must download the above sample code in order to access the 3D models used in this tutorial step.

Supporting Cast

It's time to add the remaining models: the fuel cells and the fuel carrier. They represent the various barriers encountered in the game.

The fuel cell model (fuelcell.x) is a simple canister-like object with a single texture (fuelcell.png). Typically, you only need to add the model file and not the texture. The texture file is automatically used when the Content Pipeline processes the model file. The barrier models are similar to the fuel cell model. They each have a specific model and a single texture. Since the game has three barrier types, we will be adding three different models (cube10uR/cylinder10uR/pyramid10uR.x) and a set of textures (BarrierBlue/BarrierPurple/BarrierRed.png). Unlike the fuel cell model, the barrier textures are simple and can be used with any barrier model.

Note

The rather unique model names are the result of keeping the scale relatively uniform among all models. The naming convention begins with the model name and then the radius, measured in the units of the 3D modeling application used. Therefore, pyramid10uR is the name of the pyramid model whose radius is 10 units in length.

  • Right-click the Models directory icon of the Content project.

  • Click Add and then New Item....

  • Navigate to the Models sub-directory of the downloaded source, and add the following files:

    • fuelcell.x and fuelcell.png
    • fuelcarrier.x and carriertextures.png
    • cube10uR.x, cylinder10uR.x, and pyramid10uR.x
    • BarrierBlue.png, BarrierPurple.png, and BarrierRed.png
  • From Solution Explorer, select all .png files in the Models sub-directory.

  • Right-click and select Exclude From Project.

    This prevents the textures from being processed twice.

Implementing the FuelCell Class

Model implementation is similar to what you did in the last step. You'll add some member variables to store the models, load them with the LoadContent method, initialize them (placement, etc.), and then render them on the playing field.

First, the fuel cell class. After the GameObject class, add the following FuelCell class.

class FuelCell : GameObject
{
    public bool Retrieved { get; set; }

    public FuelCell()
        : base()
    {
        Retrieved = false;
    }

    public void LoadContent(ContentManager content, string modelName)
    {
        Model = content.Load<Model>(modelName);
        Position = Vector3.Down;
    }
}

Compared to the GameObject base class, it contains a new member (Retrieved). This is a flag used by the game to determine if the fuel cell has been retrieved. The constructor uses the base constructor, setting Retrieved to false. You'll use this flag later to optimize your drawing code. If it has been found, it no longer needs to be drawn. The LoadContent loads the specified model and then sets the Y-component of the Position member to -1. That value is used as an indicator that the fuel cell has not been initialized.

This next bit of code declares a Draw method that takes a view and projection matrix and draws the fuel cell. There are two important aspects of this new code: the first is related to optimization and the second is related to future proofing. In terms of optimization, notice the check on the value of the Retrieved member. This is the optimization we discussed previously. If the fuel cell has been retrieved by the player, there is no need to draw the fuel cell. This may seem like a minor thing, but changes like this add up over the course of your game's development.

The second is a bit more complicated, but is required for correctly rendering most 3D models: bone transforms. Nearly every 3D model is comprised of a collection of bones. Basically, a model bone is a matrix representing the position of a mesh relative to other bones in the 3D model.

For our purposes, it's sufficient to know that a 3D model has a set of these matrices. To position them properly (and, in more complex cases, to animate them), you'll need to incorporate these transform matrices into the code that draws the fuel cell. You can accomplish this easily by copying the bone transforms of the model being drawn into a temporary set of matrices (using CopyAbsoluteBoneTransformsTo). Then you apply the proper transform matrix to the world matrix before you draw the current sub-mesh of the model. In the code you will add, this is done in the innermost nested foreach loop by multiplying the world matrix by the proper transform matrix.

You're probably wondering why this code is called future proofing. Ironically, the code related to applying bone transforms to the 3D model being rendered is unnecessary for FuelCell! One of the design rules used for FuelCell was to keep everything as simple as possible. This resulted in the rudimentary (and unmoving) models in the game. Each model has a set of bones, but these bones are always rendered statically and cannot be animated. However, this code is necessary to properly render any minimally complex model or one that has movable parts (like a tank rolling across a terrain map). This code makes it easier to be reused in future games without having to remember all this bone transform business. Now, when you copy the code into another 3D game, it just works. Even if the model is very complex!

Now that you understand the code better, add the following code to the FuelCell class.

public void Draw(Matrix view, Matrix projection)
{
    Matrix[] transforms = new Matrix[Model.Bones.Count];
    Model.CopyAbsoluteBoneTransformsTo(transforms);
    Matrix translateMatrix = Matrix.CreateTranslation(Position);
    Matrix worldMatrix = translateMatrix;

    if (!Retrieved)
    {
        foreach (ModelMesh mesh in Model.Meshes)
        {
            foreach (BasicEffect effect in mesh.Effects)
            {
                effect.World = 
                    worldMatrix * transforms[mesh.ParentBone.Index];
                effect.View = view;
                effect.Projection = projection;

                effect.EnableDefaultLighting();
                effect.PreferPerPixelLighting = true;
            }
            mesh.Draw();
        }
    }
}
  • You'll need to automatically scale the fuel cell model by selecting the newly-added model from Solution Explorer, and, on the property page of the model asset, setting the Scale property to .03. The Scale property is found by expanding the Processor field.

One of the cool features of XNA Game Studio (specifically, the content pipeline) are processor parameters. You can change common values for a processor by changing the related property of a selected game asset using the Properties window. If you didn't use this feature, you would need to use a scaling matrix to shrink the fuel cell model before rendering it on the screen.

Did you notice the Matrix declarations at the beginning of the Draw function? You need to transform the world coordinates of our object (in this case, the fuel cell) based on the fuel cell's position in the game. If the translation matrix wasn't used before drawing the fuel cell, it would always be in the center of the playing field.

That completes the implementation of the fuel class. Next stop, the barrier class.

Implementing the Barrier Class

The Barrier class implements the geometrical barriers that are randomly scattered across the playing field. They are an important part of the game because they provide a new experience for every game (since they are placed randomly) and they provide a challenge to the player who is trying to find fuel cells (also randomly placed) before time runs out. In a later step, when collision detection is added, these barriers become impassable and must be driven around.

  • Automatically scale the barrier models by selecting each barrier model from Solution Explorer and setting the Scale property to .3, located on the property page of the model asset. The Scale property is found by expanding the Processor field.

In GameObject.cs, add the following class declaration after the FuelCell class declaration:

class Barrier : GameObject
{
    public string BarrierType { get; set; }

    public Barrier()
        : base()
    {
        BarrierType = null;
    }

    public void LoadContent(ContentManager content, string modelName)
    {
        Model = content.Load<Model>(modelName);
        BarrierType = modelName;
        Position = Vector3.Down;
    }
}

The Barrier class has a new member (BarrierType) that stores, oddly enough, the barrier type. In the FuelCell game, there are three possible barrier types: cubes, cylinders, and pyramids. Similar to the FuelCell class, the LoadContent method is overridden that loads the specified model, stores the barrier type, and sets the Y-component of the Position member to -1 (indicating that the barrier is not placed).

Since barrier objects behave in a similar fashion to fuel cells (that is, they stay in one place and do nothing), we'll use the same drawing code with one change. Add the following Draw method to the Barrier class:

public void Draw(Matrix view, Matrix projection)
{
    Matrix[] transforms = new Matrix[Model.Bones.Count];
    Model.CopyAbsoluteBoneTransformsTo(transforms);
    Matrix translateMatrix = Matrix.CreateTranslation(Position);
    Matrix worldMatrix = translateMatrix;

    foreach (ModelMesh mesh in Model.Meshes)
    {
        foreach (BasicEffect effect in mesh.Effects)
        {
            effect.World = 
                worldMatrix * transforms[mesh.ParentBone.Index];
            effect.View = view;
            effect.Projection = projection;

            effect.EnableDefaultLighting();
            effect.PreferPerPixelLighting = true;
        }
        mesh.Draw();
    }
}

Unlike fuel cells, barriers can't be retrieved and are always visible. Therefore, there is no need to determine if a barrier should be drawn; it is always drawn. This is reflected in the code.

Implementing the Fuel Carrier

In game development terms, the fuel carrier is the avatar of the player. It is the object that represents the player in the game world and is controlled by the player. The FuelCarrier class starts out very simple but, in later steps, you'll add more features like user control and collision detection. For now, it has a few basic methods that load the model and render it on the playing field.

The process for implementing the fuel carrier class is similar to the FuelCell and Barrier class implementations.

  • Automatically scale the fuel carrier model by selecting it from Solution Explorer and setting the Scale property to .1, located on the property page of the model asset. The Scale property is found by expanding the Processor field.

  • After the Barrier class declaration, add the FuelCarrier class declaration:

    class FuelCarrier : GameObject
    {
        public float ForwardDirection { get; set; }
        public int MaxRange { get; set; }
    
        public FuelCarrier()
            : base()
        {
            ForwardDirection = 0.0f;
            MaxRange = GameConstants.MaxRange;
        }
    
        public void LoadContent(ContentManager content, string modelName)
        {
            Model = content.Load<Model>(modelName);
        }
    }
    
  • Implement the Draw method by adding the following code to the FuelCarrier class declaration:

    public void Draw(Matrix view, Matrix projection)
    {
        Matrix[] transforms = new Matrix[Model.Bones.Count];
        Model.CopyAbsoluteBoneTransformsTo(transforms);
        Matrix worldMatrix = Matrix.Identity;
        Matrix rotationYMatrix = Matrix.CreateRotationY(ForwardDirection);
        Matrix translateMatrix = Matrix.CreateTranslation(Position);
    
        worldMatrix = rotationYMatrix * translateMatrix;
    
        foreach (ModelMesh mesh in Model.Meshes)
        {
            foreach (BasicEffect effect in mesh.Effects)
            {
                effect.World = 
                    worldMatrix * transforms[mesh.ParentBone.Index]; ;
                effect.View = view;
                effect.Projection = projection;
    
                effect.EnableDefaultLighting();
                effect.PreferPerPixelLighting = true;
            }
            mesh.Draw();
        }
    }
    

    This Draw method differs from the fuel cell and barrier Draw methods in one important aspect: the calculation of a rotation matrix. In the future, the fuel carrier is controlled by the player. This means that the fuel carrier's orientation is always changing as the player races around collecting fuel cells. You have to account for this in the rendering code so that any changes by the player (such as turning left or right) are reflected in the game world. If fuel carrier orientation is not taken into account, you would have some very weird behavior for your game! At this point, you will add the support for this, but it won't be used until the next step: FuelCell: What's My Motivation.

  • Add the fuel carrier constants to the end of the GameConstants class, located in GameConstants.cs:

    //ship constants
    public const float Velocity = 0.75f;
    public const float TurnSpeed = 0.025f;
    public const int MaxRange = 98;
    

As usual, the Fuel Carrier data members are specific to the class. In this case, there is an orientation property, storing the current direction (in radians) that the fuel carrier is facing. This property is also used by the camera class to orientate along the same vector. The MaxRange member is used later to prevent the fuel carrier from driving off the playing field. This is something that would completely break the game play illusion.

As mentioned earlier, the methods are similar to the implementation code for the fuel cell and barrier classes. However, in the next part, you will add code that allows the player to drive the fuel carrier around the playing field. In fact, the fuel carrier has the singular honor of being the only moving part in the game!

Setting the Stage

It's time to shift our focus back to the main game class, Game1.cs. You're going to add member variables representing the new game objects you added: the fuel carrier, fuel cell, and various barriers. At this stage, we'll display a fuel cell, three barriers (each of a different type), and the fuel carrier on the playing field. Later in the development cycle, you'll add code that randomly generates and places the fuel cells and barriers.

However, before you start modifying this file, rename it FuelCellGame.cs. This follows the naming format of the other project files.

  • In FuelCellGame.cs, after the declaration of the camera and ground variables, add the following code:

    Random random;
    FuelCarrier fuelCarrier;
    FuelCell[] fuelCells;
    Barrier[] barriers;
    
  • After the initialization of the camera and ground variables (located in the Initialize method), add the following code:

    //Initialize and place fuel cell
    fuelCells = new FuelCell[1];
    fuelCells[0] = new FuelCell();
    fuelCells[0].LoadContent(Content, "Models/fuelcell");
    fuelCells[0].Position = new Vector3(0, 0, 15);
    
    //Initialize and place barriers
    barriers = new Barrier[3];
    
    barriers[0] = new Barrier();
    barriers[0].LoadContent(Content, "Models/cube10uR");
    barriers[0].Position = new Vector3(0, 0, 30);
    barriers[1] = new Barrier();
    barriers[1].LoadContent(Content, "Models/cylinder10uR");
    barriers[1].Position = new Vector3(15, 0, 30);
    barriers[2] = new Barrier();
    barriers[2].LoadContent(Content, "Models/pyramid10uR");
    barriers[2].Position = new Vector3(-15, 0, 30);
    
    //Initialize and place fuel carrier
    fuelCarrier = new FuelCarrier();
    fuelCarrier.LoadContent(Content, "Models/fuelcarrier");
    

    This code initializes all our new models and places them in front of the camera. The fuel cell is in the front row and the barriers are in a line behind it.

  • Modify the existing Draw method by adding the following code after the DrawTerrain call:

    fuelCells[0].Draw(gameCamera.ViewMatrix, 
        gameCamera.ProjectionMatrix);
    foreach (Barrier barrier in barriers)
        barrier.Draw(gameCamera.ViewMatrix, 
            gameCamera.ProjectionMatrix);
    fuelCarrier.Draw(gameCamera.ViewMatrix, 
        gameCamera.ProjectionMatrix);
    
            MyBase.Draw(gameTime)
        End Sub
    
        ''' <summary>
        ''' Draws the game terrain, a simple blue grid.
        ''' </summary>
        ''' <param name="model">Model representing the game playing field.</param>
        Private Sub DrawTerrain(ByVal model As Model)
            For Each mesh In model.Meshes
                For Each effect As BasicEffect In mesh.Effects
                    effect.EnableDefaultLighting()
                    effect.PreferPerPixelLighting = True
                    effect.World = Matrix.Identity
    
                    ' Use the matrices provided by the game camera
                    effect.View = gameCamera.ViewMatrix
                    effect.Projection = gameCamera.ProjectionMatrix
                Next effect
                mesh.Draw()
            Next mesh
        End Sub
    End Class
    ''' <summary>
    ''' This is the main type for your game
    ''' </summary>
    Public Class Game1
        Inherits Microsoft.Xna.Framework.Game
    
        Private WithEvents graphics As GraphicsDeviceManager
        Private WithEvents spriteBatch As SpriteBatch
    
        Public Sub New()
            graphics = New GraphicsDeviceManager(Me)
            Content.RootDirectory = "Content"
        End Sub
    
        ''' <summary>
        ''' Allows the game to perform any initialization it needs to before starting to run.
        ''' This is where it can query for any required services and load any non-graphic
        ''' related content.  Calling MyBase.Initialize will enumerate through any components
        ''' and initialize them as well.
        ''' </summary>
        Protected Overrides Sub Initialize()
            ' TODO: Add your initialization logic here
            MyBase.Initialize()
        End Sub
    
        ''' <summary>
        ''' LoadContent will be called once per game and is the place to load
        ''' all of your content.
        ''' </summary>
        Protected Overrides Sub LoadContent()
            ' Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = New SpriteBatch(GraphicsDevice)
    
            ' TODO: use Me.Content to load your game content here
        End Sub
    
        ''' <summary>
        ''' UnloadContent will be called once per game and is the place to unload
        ''' all content.
        ''' </summary>
        Protected Overrides Sub UnloadContent()
            ' TODO: Unload any non ContentManager content here
        End Sub
    
        ''' <summary>
        ''' Allows the game to run logic such as updating the world,
        ''' checking for collisions, gathering input, and playing audio.
        ''' </summary>
        ''' <param name="gameTime">Provides a snapshot of timing values.</param>
        Protected Overrides Sub Update(ByVal gameTime As GameTime)
            ' Allows the game to exit
            If GamePad.GetState(PlayerIndex.One).Buttons.Back = ButtonState.Pressed Then
                Me.Exit()
            End If
    
            ' TODO: Add your update logic here
            MyBase.Update(gameTime)
        End Sub
    
        ''' <summary>
        ''' This is called when the game should draw itself.
        ''' </summary>
        ''' <param name="gameTime">Provides a snapshot of timing values.</param>
        Protected Overrides Sub Draw(ByVal gameTime As GameTime)
            GraphicsDevice.Clear(Color.CornflowerBlue)
    
            ' TODO: Add your drawing code here
            MyBase.Draw(gameTime)
        End Sub
    
    End Class
    ' Copyright (C) Microsoft Corporation. All rights reserved.
    
    Friend Class GameConstants
        'camera constants
        Public Const NearClip As Single = 1.0F
        Public Const FarClip As Single = 1000.0F
        Public Const ViewAngle As Single = 45.0F
        'reuse//code//Part3GameConstantsFuelCarrier//
        'ship constants
        Public Const Velocity As Single = 0.75F
        Public Const TurnSpeed As Single = 0.025F
        Public Const MaxRange As Integer = 98
        'reuse//code//Part3GameConstantsFuelCarrier//
    End Class
    ' Copyright (C) Microsoft Corporation. All rights reserved.
    
    Friend Class GameObject
        Public Property Model As Model
        Public Property Position As Vector3
        Public Property BoundingSphere As BoundingSphere
    
        Public Sub New()
            Model = Nothing
            Position = Vector3.Zero
            BoundingSphere = New BoundingSphere
        End Sub
    End Class
    
    
    'reuse//code//Part3GameObjectFuelCell//
    Friend Class FuelCell
        Inherits GameObject
        Public Property Retrieved As Boolean
    
        Public Sub New()
            MyBase.New()
            Retrieved = False
        End Sub
    
        Public Sub LoadContent(ByVal content As ContentManager, ByVal modelName As String)
            Model = content.Load(Of Model)(modelName)
            Position = Vector3.Down
        End Sub
        'reuse//code//Part3GameObjectFuelCell//
    
        'reuse//code//Part3GameObjectFuelCellDraw//
        Public Sub Draw(ByVal view As Matrix, ByVal projection As Matrix)
            Dim transforms(Model.Bones.Count - 1) As Matrix
            Model.CopyAbsoluteBoneTransformsTo(transforms)
            Dim translateMatrix = Matrix.CreateTranslation(Position)
            Dim worldMatrix = translateMatrix
    
            If Not Retrieved Then
                For Each mesh In Model.Meshes
                    For Each effect As BasicEffect In mesh.Effects
                        effect.World = worldMatrix * transforms(mesh.ParentBone.Index)
                        effect.View = view
                        effect.Projection = projection
    
                        effect.EnableDefaultLighting()
                        effect.PreferPerPixelLighting = True
                    Next effect
                    mesh.Draw()
                Next mesh
            End If
        End Sub
        'reuse//code//Part3GameObjectFuelCellDraw//
    
        'reuse//code//Part3GameObjectFuelCell//
    End Class
    'reuse//code//Part3GameObjectFuelCell//
    
    'reuse//code//Part3GameObjectBarrier//
    Friend Class Barrier
        Inherits GameObject
        Public Property BarrierType As String
    
        Public Sub New()
            MyBase.New()
            BarrierType = Nothing
        End Sub
    
        Public Sub LoadContent(ByVal content As ContentManager, ByVal modelName As String)
            Model = content.Load(Of Model)(modelName)
            BarrierType = modelName
            Position = Vector3.Down
        End Sub
        'reuse//code//Part3GameObjectBarrier//
    
        'reuse//code//Part3GameObjectBarrierDraw//
        Public Sub Draw(ByVal view As Matrix, ByVal projection As Matrix)
            Dim transforms(Model.Bones.Count - 1) As Matrix
            Model.CopyAbsoluteBoneTransformsTo(transforms)
            Dim translateMatrix = Matrix.CreateTranslation(Position)
            Dim worldMatrix = translateMatrix
    
            For Each mesh In Model.Meshes
                For Each effect As BasicEffect In mesh.Effects
                    effect.World = worldMatrix * transforms(mesh.ParentBone.Index)
                    effect.View = view
                    effect.Projection = projection
    
                    effect.EnableDefaultLighting()
                    effect.PreferPerPixelLighting = True
                Next effect
                mesh.Draw()
            Next mesh
        End Sub
        'reuse//code//Part3GameObjectBarrierDraw//
    
        'reuse//code//Part3GameObjectBarrier//
    End Class
    'reuse//code//Part3GameObjectBarrier//
    
    'reuse//code//Part3GameObjectFuelCarrier//
    Friend Class FuelCarrier
        Inherits GameObject
        Public Property ForwardDirection As Single
        Public Property MaxRange As Integer
    
        Public Sub New()
            MyBase.New()
            ForwardDirection = 0.0F
            MaxRange = GameConstants.MaxRange
        End Sub
    
        Public Sub LoadContent(ByVal content As ContentManager, ByVal modelName As String)
            Model = content.Load(Of Model)(modelName)
        End Sub
        'reuse//code//Part3GameObjectFuelCarrier//
    
        'reuse//code//Part3GameObjectFuelCarrierDraw//
        Public Sub Draw(ByVal view As Matrix, ByVal projection As Matrix)
            Dim transforms(Model.Bones.Count - 1) As Matrix
            Model.CopyAbsoluteBoneTransformsTo(transforms)
            Dim worldMatrix = Matrix.Identity
            Dim rotationYMatrix = Matrix.CreateRotationY(ForwardDirection)
            Dim translateMatrix = Matrix.CreateTranslation(Position)
    
            worldMatrix = rotationYMatrix * translateMatrix
    
            For Each mesh In Model.Meshes
                For Each effect As BasicEffect In mesh.Effects
                    effect.World = worldMatrix * transforms(mesh.ParentBone.Index)
    
                    effect.View = view
                    effect.Projection = projection
    
                    effect.EnableDefaultLighting()
                    effect.PreferPerPixelLighting = True
                Next effect
                mesh.Draw()
            Next mesh
        End Sub
        'reuse//code//Part3GameObjectFuelCarrierDraw//
        'reuse//code//Part3GameObjectFuelCarrier//
    End Class
    'reuse//code//Part3GameObjectFuelCarrier//
    
    Friend Class Camera
        Public Property AvatarHeadOffset As Vector3
        Public Property TargetOffset As Vector3
        Public Property ViewMatrix As Matrix
        Public Property ProjectionMatrix As Matrix
    
        Public Sub New()
            AvatarHeadOffset = New Vector3(0, 7, -15)
            TargetOffset = New Vector3(0, 5, 0)
            ViewMatrix = Matrix.Identity
            ProjectionMatrix = Matrix.Identity
        End Sub
    
        Public Sub Update(ByVal avatarYaw As Single, ByVal position As Vector3, ByVal aspectRatio As Single)
            Dim rotationMatrix = Matrix.CreateRotationY(avatarYaw)
    
            Dim transformedheadOffset = Vector3.Transform(AvatarHeadOffset, rotationMatrix)
            Dim transformedReference = Vector3.Transform(TargetOffset, rotationMatrix)
    
            Dim cameraPosition = position + transformedheadOffset
            Dim cameraTarget = position + transformedReference
    
            'Calculate the camera's view and projection 
            'matrices based on current values.
            ViewMatrix = Matrix.CreateLookAt(cameraPosition, cameraTarget, Vector3.Up)
            ProjectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(GameConstants.ViewAngle), aspectRatio,
                                                                   GameConstants.NearClip, GameConstants.FarClip)
        End Sub
    End Class
    #If WINDOWS OrElse XBOX Then
    Friend NotInheritable Class Program
        ''' <summary>
        ''' The main entry point for the application.
        ''' </summary>
        Private Sub New()
        End Sub
        Shared Sub Main(ByVal args() As String)
            Using game As New FuelCellGame
                game.Run()
            End Using
        End Sub
    End Class
    #End If
    

Build and run the project and you will now see, in addition to the playing field, several cool things on the screen. You see some barriers, with a fuel cell slightly behind them, and a funny blue ovoid in the foreground. That is actually the fuel carrier. It's a (very) simple model, but it suits the purpose of the game. The next step implements user control of the game avatar.

See Also

Conceptual

FuelCell Example Game