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:

 /// <summary> /// Return the mesh that represents loopy /// </summary> public Mesh LoopyMesh {     get { return loopyMesh; } } /// <summary> /// Return the texture that represents loopy's /// color /// </summary> public Texture LoopyTexture {     get { return loopyTexture; } } /// <summary> /// Return loopy's material /// </summary> 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     /// <summary>     /// Create a new player object     /// </summary>     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     /// <summary>     /// Clean up any resources in case you forgot to dispose this object     /// </summary>     ~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
 /// <summary> /// Clean up the loopy mesh objects /// </summary> 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
 /// <summary> /// The direction the player model is facing /// </summary> 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
 /// <summary> /// Update the player's position based on time /// </summary> /// <param name="elapsed">elapsed time since last frame</param> /// <param name="total">total time</param> 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.



Beginning 3D Game Programming
Beginning 3D Game Programming
ISBN: 0672326612
EAN: 2147483647
Year: 2003
Pages: 191
Authors: Tom Miller

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net