Coding the Player Object for Games


Writing the Player Object

One of the more important aspects of the player is to render it onscreen. You probably already realize that you've got the mesh loaded and already rendering in the select-your-character screen, so it wouldn't make sense to go through the entire loading process again. Instead, you use the objects you've already loaded during that screen. To do so, you need to add a few properties to your SelectLoopyScreen class:
/// 
/// Return the mesh that represents loopy
/// 
public Mesh LoopyMesh
{
    get { return loopyMesh; }
}
/// 
/// Return the texture that represents loopy's
/// color
/// 
public Texture LoopyTexture
{
    get { return loopyTexture; }
}
/// 
/// Return loopy's material
/// 
public Material LoopyMaterial
{
    get { return loopyMaterial; }
}

These properties allow you to take the already loaded mesh, texture, and material for the character and use them in the player's class after you create it. Speaking of which, now is a great time to do that! You've already created a code file for your player object, where you stored the player color enumeration. You can add your player class to that file now. Listing 7.1 contains the initial player class implementation.
Listing 7.1. The Player Class
public class Player : IDisposable
{
    private Mesh playerMesh = null; // Mesh for the player
    private Material playerMaterial; // Material
    private Texture playerTexture = null; // Texture for the mesh
    /// 
    /// Create a new player object
    /// 
    public Player(Mesh loopyMesh, Texture loopyTex,
        Material mat)
    {
        // Store the mesh
        playerMesh = loopyMesh;

        // Store the appropriate texture
        playerTexture = loopyTex;

        // Store the player's material
        playerMaterial = mat;
    }
    #region IDisposable Members
    /// 
    /// Clean up any resources in case you forgot to dispose this object
    /// 
    ~Player()
    {
        Dispose();
    }
    public void Dispose()
    {
        // Suppress finalization
        GC.SuppressFinalize(this);

        // Dispose of the texture
        if (playerTexture != null)
        {
            playerTexture.Dispose();
        }

        if (playerMesh != null)
        {
            playerMesh.Dispose();
        }

        playerTexture = null;
        playerMesh = null;
    }
    #endregion
}

After declaring the items, you need to render your mesh; you simply store them in the constructor so you do not need to re-create them once more. It's interesting to note that the Dispose method cleans up these items, even though the actual creation didn't occur in this class. You might have even noticed that you never cleaned up these objects from the select-character screen earlier. If you did, bravo; if you didn't, keep this thought in mind. To maintain the best performance, you want to always clean up the objects when you are finished with them.
Because the player's mesh and texture are cleaned up when the player object is disposed, this step takes care of the normal case, but what about the case when you quit the game before the game has actually been loaded and no player object is ever created? You want to ensure that the objects are still cleaned up properly. Because the SelectLoopyScreen class has the references to the objects, you can add a new method there to do this cleanup if the player object hasn't been created yet. Add the method from Listing 7.2 to your SelectLoopyScreen class.
Listing 7.2. Cleaning Up the Player Mesh
/// 
/// Clean up the loopy mesh objects
/// 
public void CleanupLoopyMesh()
{
    if (loopyMesh != null)
    {
        loopyMesh.Dispose();
    }
    if (loopyTexture != null)
    {
        loopyTexture.Dispose();
    }
}

After the player object is added to the main game engine, you add calls to the appropriate cleanup mechanism. Before you do this, however, you finish up the player object. One of the first methods to implement is the rendering method. The player needs some representation onscreen, so this is a pretty important method. Add the method in Listing 7.3 to your Player class to allow the player to be rendered.
Listing 7.3. Rendering the Player
public void Draw(Device device, float appTime)
{
    // Set the world transform
    device.Transform.World = rotation * ScalingMatrix *
        Matrix.Translation(pos.X, playerHeight, pos.Z);

    // Set the texture for our model
    device.SetTexture(0, playerTexture);
    // Set the model's material
    device.Material = playerMaterial;

    // Render our model
    playerMesh.DrawSubset(0);
}

There are a few variables and constants in this method that you haven't actually declared yet. Before you go through this method, you should add these to your Player class:
private const float ScaleConstant = 0.1f;
private static readonly Matrix RotationMatrixFacingDown = Matrix.RotationY(
    (float)Math.PI * 3.0f / 2.0f);
private static readonly Matrix RotationMatrixFacingUp = Matrix.RotationY(
    (float)Math.PI / 2.0f);
private static readonly Matrix RotationMatrixFacingLeft = Matrix.RotationY(
    (float)Math.PI * 2);
private static readonly Matrix RotationMatrixFacingRight = Matrix.RotationY(
    (float)Math.PI);
private static readonly Matrix ScalingMatrix = Matrix.Scaling(
    ScaleConstant, ScaleConstant, ScaleConstant);
private Vector3 pos; // The player's real position
private float playerHeight = 0.0f;
private Matrix rotation = RotationMatrixFacingDown;

Obviously, the player will be able to move around the level, so you need to store the player's current position, which is stored in the Vector variable. When you are about to render the player, you translate the player model into the correct location. Notice that you actually use only the X and Z members of this vector: the height of the player (Y) is calculated later to provide a small "bounce"to the player. In addition, the default player model is a bit too large, and it's rotated the wrong way, so you want to both scale and rotate the model at this time as well. You should notice that constants are defined for these transformations so they don't need to be calculated every frame. Also notice that there are four different ways the player can be rotated depending on which direction the player is currently facing.
One of the interesting aspects of these transformations is that the order of the operations is important. In "normal" math, 3x4 is the same as 4x3; however, when creating transformation matrices, this isn't the case. Rotation * Translation is not the same as Translation * Rotation. In the first case, the rotation is performed before the translation, and the effect you want is most likely what you'll get. In the second case, the object is moved (translated) before the rotation is provided, but the center point remains the same, most likely applying the wrong effect.
Aside from that point, the rendering of the player is no different from the other rendering of meshes that you have done earlier. You set the texture to the correct one for the player, you set the material, and finally you call the DrawSubset method. This part is virtually identical to the code that renders the character in the selection screen; the only difference is scaling the character to a smaller size.

Moving the Player

There will be a time, normally when the level first loads, when you need to set the location of the player initially. Because the location is a private variable, you include a property accessor for the position, such as this:
public Vector3 Position
{
    get { return pos; }
    set { pos = moveToPos = value;}
}

What is this moveToPos variable here for? You certainly haven't declared it yet, but you can go ahead and add in the movement variables now:
private Vector3 moveToPos; // Where the player is moving to
private bool isMoving = false;

Aside from the current physical position of the player, you also want to store the location the player is moving to, which can be different from the place the player currently is. To maintain both of these positions, you obviously need a second variable, as you've declared here. Setting the Position property of the player implies that you will also move there, thus setting both variables in that property. You also want a method to update the moveTo variable as well, so include the method in Listing 7.4 to your Player class.
Listing 7.4. Updating the Player Movement
public bool MoveTo(Vector3 newPosition)
{
    if (!isMoving)
    {
        isMoving = true;
        // Just store the new position, it will be used during update
        moveToPos = newPosition;
        return true;
    }
    else
    {
        return false;
    }
}

Here you want to check first whether you are already moving. If you are, there isn't any good reason to try moving again. Assuming you can move, you simply store the position you want to move to (it will be updated in a different method) and return true, which states the move was successful. Otherwise, you return false, which signifies that there was already a movement in progress and the movement wasn't successful.
You also want to rotate and face the direction that the player is currently moving as well. Earlier, the rotation transform constants that you declared had four different values, depending on the direction the player would be facing. Once again, you can store these values in an enumeration, such as the one in Listing 7.5.
Listing 7.5. Player Direction Enumeration
/// 
/// The direction the player model is facing
/// 
public enum PlayerDirection
{
    Down,
    Up,
    Left,
    Right
}

You also add a new method to the Player class that will accept this enumeration as a parameter and use it to update the current rotation of the player. Use the method in Listing 7.6.
Listing 7.6. Updating Player Direction
public void SetDirection(PlayerDirection dir)
{
    switch(dir)
    {
        case PlayerDirection.Down:
            rotation = RotationMatrixFacingDown;
            break;
        case PlayerDirection.Up:
            rotation = RotationMatrixFacingUp;
            break;
        case PlayerDirection.Left:
            rotation = RotationMatrixFacingLeft;
            break;
        case PlayerDirection.Right:
            rotation = RotationMatrixFacingRight;
            break;
    }
}

As you see here, you are simply taking the enumeration and applying the correct transformation matrix to the stored variable. The methodology to accomplish this task is quite readable but could be done slightly more efficiently at the cost of readability. For example, you could eliminate the case statement completely by using an array of matrix constants in place of the four named directional constants, such as the following:
private static readonly Matrix[] RotationMatrices = new Matrix[] {
    Matrix.RotationY((float)Math.PI * 3.0f / 2.0f),
    Matrix.RotationY((float)Math.PI / 2.0f),
    Matrix.RotationY((float)Math.PI * 2),
    Matrix.RotationY((float)Math.PI) };

This code would allow you to update the SetDirection method into a single line:
public void SetDirection(PlayerDirection dir)
{
    rotation = RotationMatrices[(byte)dir];
}

Although it is syntactically identical to the previous "version" of the code, this section is much less readable and only marginally better performing. Sometimes you need to get every bit of performance out of a method, but other times you want it to be more "maintainable." Determining which is the most appropriate quality for a particular scenario is really the crux of good development.
Anyway, movement appears at least partially covered because the current position of the player and the position the player is moving to have been separated, but currently there isn't anywhere that you update the physical position of the player and move it closer to where it's going. Remember, the main game engine has a method called every frame where the game state is updated, and the player's position seems like a great bit of game state that needs updating. Rather than do the work in the game engine class, though, you should add a method to the Player class that will do the work. You find this method in Listing 7.7.
Listing 7.7. Updating the Player
/// 
/// Update the player's position based on time
/// 
/// elapsed time since last frame
/// total time
public void Update(float elapsed, float total)
{
    // Calculate the new player position if needed
    playerHeight = (float)(Math.Abs(Math.Sin(total * BounceSpeed))) *
    MaxBounceHeight;

    if (pos != moveToPos)
    {
        Vector3 diff = moveToPos - pos;
        // Are we close enough to just move there?
        if (diff.LengthSq() > (MaxMovement * elapsed))
        {
            // No we're not, move slowly there
            diff.Normalize();
            diff.Scale(MaxMovement * elapsed);
            pos += diff;
        }
        else
        {
            isMoving = false;
            // Indeed we are, just move there
            pos = moveToPos;
        }
    }
    else
    {
        isMoving = false;
    }
}

This method uses three constants to govern player movement that you will need to add to your class's declarations:
// Maximum speed the player will move per second
private const float MaxMovement = 30.0f;
// Maximum 'bounce' speed
private const float BounceSpeed = 10.0f;
private const float MaxBounceHeight = 0.4f;

These constants control the maximum speed the player can move per second (30 units is the default), the speed at which the player bounces, and the maximum height of the bounce. The playerHeight variable, which is used during the translation for rendering, is calculated first. Taking the absolute value of the sine of the total time the application has been running gives us a consistently updating range of numbers between 0 and 1. Multiplying them by the BounceSpeed constant can artificially increase the range (in this case, from 0 through 10). Increasing this constant causes the bouncing to become quicker, and decreasing it has the opposite effect.
After the player's height is established, you need to check whether you're in the position you're trying to move to. If you are, you set the isMoving variable to false because there isn't anything else to do and you are no longer moving. If you are not, however, there are some things you need to do. First, find the difference vector between where you are and where you want to be. The length of this vector tells you how far you need to go to get to where you want to be.
You'll notice that there is a Length property on the Vector, yet this code uses the LengthSq property instead, which is the Length property squared. Actually, the length of a vector is calculated by taking the square root of x2 + y2 + z2. Obviously, taking the square root is an "expensive" operation, so eliminating that calculation can only help. You take the squared length variable and see whether that is greater than the maximum distance you could move this frame; if so, simply move there.
If you cannot simply move there, you need to normalize the difference vector. Normalization is the act of taking the vector and making it have a unit length (or a length of 1.0f). The "ratio" of the components stays the same; the length is simply scaled to unit size. You can then scale the now normalized difference vector by the maximum movement you can make this frame (based on the elapsed time since the last frame). Adding the newly scaled difference vector to your position moves you toward the position you are trying to get to.

Designing the Blocks

With the code for the player now implemented, you are probably anxious to get it into the game engine and make your player move around the level and play the game. Don't get too far ahead of yourself yet, though: you haven't even designed the level object or the requirements for the level object, namely the blocks that compose the levels. You should design these blocks now. First, add a new code file to your project called block.cs and then add the initial class implementation in Listing 7.8.
Listing 7.8. The Initial Block Implementation
public class Block
{
    private const float BoxSize = 3.0f; // World size of box
    // List of colors available for boxes
    private static readonly Color[] BoxColors = {Color.Red , Color.Blue,
               Color.Green, Color.Yellow, Color.SandyBrown, Color.WhiteSmoke,
               Color.DarkGoldenrod, Color.CornflowerBlue, Color.Pink};

    // List of each possible material object for the box
    private static Material[] boxMaterials;
    // The static mesh of the box
    private static Mesh boxMesh = null;

    private Vector3 pos; // Current position of the box in the level
    private Material material; // Current material color of the box

    // Possible colors of the blocks
    private BlockColor[] colors;
    private int colorIndex;
    private bool canWrap;

    /// 
    /// Create a new instance of the block class
    /// 
    /// D3D Device to use as the render object
    public Block(Device dev, BlockColor[] list, int index, bool wrap)
    {
        if ((list == null) || (list.Length == 0))
            throw new ArgumentNullException("list",
               "You must pass in a valid list");

        // Initialize the static data if needed
        if (boxMesh == null)
        {
            InitializeStaticData(dev);
        }

        // Set the default position
        pos = new Vector3(0,-1.0f, 0);

        // Store the information
        colors = list;
        colorIndex = index;
        canWrap = wrap;

        // Set the default material
        SetBlockColor(colors[colorIndex]);
    }
    /// 
    /// Initialize the static data
    /// 
    /// D3D Device to use as the render object
    private static void InitializeStaticData(Device dev)
    {
        // Create the mesh for each box that will be drawn
        boxMesh = Mesh.Box(dev, BoxSize, BoxSize, BoxSize);
        // Create the material list based on the colors of the boxes
        boxMaterials = new Material[BoxColors.Length];
        for (int i = 0; i < boxMaterials.Length; i++)
        {
            boxMaterials[i].Diffuse = boxMaterials[i].Ambient = BoxColors[i];
        }
    }
    /// 
    /// Clean up any resources used for rendering the blocks
    /// 
    public static void Cleanup()
    {
        if (boxMesh != null)
        {
            boxMesh.Dispose();
        }
        boxMesh = null;
        boxMaterials = null;
    }
}

One of the first things you'll notice is that the various block colors are all stored in a static read-only array. It is entirely possible for you to change these colors and add or remove others. Note that later when you create the level maker, doing so requires changes in there as well, but you haven't gotten there yet. One of the more interesting aspects of block mesh is that you'll notice that it's marked static. Regardless of how many blocks you have displayed onscreen, you only need a single mesh loaded. There's no reason to store many copies of the same data.
Because the blocks are all colored, the colors are controlled through the material that is applied to the rendering device before the block is rendered. Given that the light in the scene is white, if the material is say, blue, only the blue portion of the white light reflects on the block, making it appear blue. There is one material created for each of the block colors, and this array is static much like the colors array because you need only a single instance of this array. Finally, you declare the actual mesh, and again, it is static because you need only one for all blocks.
The actual instance variables for this class are similar to other classes you've already written. Obviously, because there will be many blocks in different places, you want to store the position of this block. You also store the current material of the block so it is colored correctly while rendering.
The last set of variables declared here control how the block reacts in the level. Each level has a minimum of two different block colors that any block can be at any time. You want to store the possible block colors in the order they will be used in this array. You also want to know the index of the color this block currently is. For the more difficult levels, these colors actually "wrap" back to the first index after they make it to the last color. To avoid passing the actual color structure around to specify the colors, you create an enumeration where each color is the index into the color array:
public enum BlockColor : byte
{
    Red = 0,
    Blue = 1,
    Green = 2,
    Yellow = 3,
    SandyBrown = 4,
    WhiteSmoke = 5,
    DarkGoldenRod = 6,
    CornflowerBlue = 7,
    Pink = 8
}

The constructor for the block takes the list of colors for this level (which is simply stored), the color index for this block (which is also stored), whether or not the colors will "wrap" (again, stored), and finally the rendering device. Assuming the static boxMesh variable hasn't been allocated yet, all the static data is initialized now. Because the boxMesh variable is set in that method, it is called only once.
The InitializeStaticData method uses the Mesh.Box method to create a cube with the width, height, and depth sizes, all using the constant declared earlier. You then set up the possible materials using the array of colors declared earlier. With all the static data now created, you're free to continue creating your individual blocks. You store a default position for the blocks and set the current block color to the correct color based on the list of colors passed in and the index of the color. You haven't actually implemented this method yet, but you can find it in here:
private void SetBlockColor(BlockColor color)
{
    material = boxMaterials[(byte)color];
}

As you see, it does nothing more than set the material of the block to the correct material based on the current block color. Because the BlockColor enumeration is just an index into the material and colors array, this method is easy to implement. Finally, you need a way to clean up the mesh the blocks use. Because it is a static method, you don't want to implement IDisposable on the Block class, which would cause the block's mesh to be disposed too soon and too often. You need to call the static method once before the application is closed. As a matter of fact, you should go ahead and add the call to your Dispose implementation in the game engine class now, just so you don't forget later. Directly before the device dispose call, add the cleanup for the blocks:
// Make sure to clean up the blocks
Block.Cleanup();

Because you need to update the position of the blocks, you want to ensure that you have a property to access it, so add this to the Block class:
/// 
/// Current Box Position
/// 
public Vector3 Position
{
    get { return pos; }
    set { pos = value; }
}

There are still a few methods to add to the Block class, which you will find in Listing 7.9.
Listing 7.9. Controlling Blocks
/// 
/// Retrieve the block color
/// 
public BlockColor BlockColor
{
    get { return colors[colorIndex]; }
}
/// 
/// Sets whether or not the block should pulse
/// 
public void SetBlockPulse(bool pulse)
{
    shouldPulse = pulse;
}
/// 
/// Sets the current total time on a block
/// 
public void SetBlockTime(float totalTime)
{
    time = totalTime;
}
/// 
/// Sets the block velocity after game over
/// 
public void SetBlockVelocity(Vector3 vel)
{
    velocity = vel;
}
public void UpdateBlock(float elapsed)
{
    // Update the block position based on velocity
    Vector3 velocityFrame = velocity;
    velocityFrame.Normalize();
    velocityFrame.Scale(velocity.Length() * elapsed);

    // Move the block
    pos += velocityFrame;
}

You obviously need a way to get the color of any given block (which is used to check whether the level was completed), and you also want a way to set whether the block should "pulse." The blocks that are the incorrect color pulse slightly to give the player a visual cue about what blocks still need to be switched. You use the time variable to control the effect of the pulse.
When the game is over, you want all the blocks to "explode" away, to really let the user know he or she failed. You create this explosion by assigning a random velocity to the block. The UpdateBlock method uses a similar method to what you used when you moved the player around. The velocity is normalized and then scaled to the correct velocity based on the elapsed time. The position of the block is then updated with this new velocity. This method is called only after the game is over and only when the level was lost.
These methods all use some variables that haven't been declared yet, so declare them now:
private float time = 0.0f;
private bool shouldPulse = false;
private Vector3 velocity;

With all of this done, you still need a way to render the blocks. You will find the render method in Listing 7.10.
Listing 7.10. Rendering Blocks
/// 
/// Render the current box using instance settings
/// 
public void Draw(Device dev)
{
    // Set the device's material to the color of this box
    dev.Material = material;
    // Move the box into position
    if (shouldPulse)
    {
        float scaling = (float)Math.Abs(Math.Sin(time * MaxBoxScaleSpeed));
        scaling *= MaxBoxScale;
        float scaleFactor = 1.0f + scaling;
        dev.Transform.World = Matrix.Translation(pos) * Matrix.Scaling(
            scaleFactor, 1.0f, scaleFactor);
    }
    else
    {
        // Move the box into position
        dev.Transform.World = Matrix.Translation(pos);
    }
    // Turn off any texture
    dev.SetTexture(0, null);
    // Draw the box
    boxMesh.DrawSubset(0);
}

The rendering of the block is pretty easy to see. To ensure that the block is the correct color, you first set the material. Then you determine whether the block should pulse; if it shouldn'tit's easyyou simply translate the box into position. If the block is pulsing, though, there is a slight difference because you also scale the box. You achieve the pulse effect by quickly scaling the box between normal size and slightly larger using a standard sine wave. After the transformation is complete, you simply use the DrawSubset call to render the box.
The Block class is almost done; you have only one more thing left to do. You need a way to make the block move to the next color when it is stepped on. Include the method from Listing 7.11 in your class.
Listing 7.11. Selecting Next Block Color
public void SelectNextColor()
{
    // Are we at the end, and can wrap?  If so, reset the color index
    if ((colorIndex == (colors.Length - 1)) && (canWrap))
    {
        // Reset color index
        colorIndex = 0;
    }
    // Otherwise, if we're not at the end, increment the color index
    else if (colorIndex != (colors.Length - 1))
    {
        // Increment color index since we haven't gotten to the last one yet
        colorIndex++;
    }

    // Update material
    SetBlockColor(colors[colorIndex]);
}

Here you need to first check whether the color can wrap. If it can, and your index is already on the last color in your list, simply reset the index back to the first. Otherwise, if you haven't made it to the last color in your list, simply increment your index to select the next color. If you are on the last color in your list and you can't wrap, you won't do anything. Next, call the SetBlockColor method to update the material for your block.


Comments

Popular Posts