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.
|