I wanted to introduce you to XNA Game Components before we dig into the next tutorial. Game Components are something that you are bound to run into as your XNA programs become more and more complicated. In order to organize your code so that you can find things and make sense of it all, you will start creating objects. We created an object for the PlayerCharacter for example. That allowed all the code for that purpose to be put into one class. Furthermore, we put that class in it's own file. Then you can pretty much forget about the code inside that object and just copy the class file into your new project. Then you can create an instance of that object in your main file and get all the abilities of that object without having to recreate it or even worry about how the methods of that object work. It's a great way to separate different parts of the code to make them more manageable.
But as you keep doing this, you'll start noticing that almost all of these new classes you have been creating share a whole lot in common. They all need to go through an initialization. Many of them will need to load content. Almost all of them will have a method similar to the Update method. And quite a few of them will need to Draw themselves.
Every object that gets drawn on the screen will have code in the Draw method. Putting the Draw code for each object in the class for that object and then calling it from the Draw method, allows the draw code to be kept with the object's class and helps the Draw method stay fairly clean and straight forward.
Anyway, that's where Game Components come in. XNA has special classes to handle this situation called Game Components. It's really very similar to just creating your own class, except alot of the common code is built into the game components.
For example, a Game Component has an Update class which you override to put in your own Update code. But you don't even have to tell the Game Component to Update.
You've probably noticed the "base.Update(gameTime);" at the end of the Update method and wondered what that's for. The answer is: Game Components. That line of code calls the Game Components that you have defined. Or rather, it calls the Update methods of any Game Components you've defined.
Initialize, Load, Unload, and Draw also have similar code at the end that call any game components that you define. So, you don't actually have to call your game components because XNA is setup to do it automatically.
Game Components help you to organize your code and keep your main Game1.cs code clean and easy to read.
There are two different types of Game Componets. There is GameComponent and DrawableGameComponent. The difference is that regular GameComponents don't draw anything to the screen, so they don't have a Draw method.
Since XNA Game Components have an official title, they sound kind of like they might be difficult to setup. But actually they're pretty straight forward. We're going to modify the PlayerCharacter class to be a Game Component.
The PlayerCharacter class doesn't draw anything to the screen. It has a bounding box, and you could possibly put that in the PlayerCharacter class to have the class draw it's own bounding box, but that's not the way this program was written.
However, I've decided to make PlayerCharacter a DrawableGameComponent in case we want to expand it later and draw something with this object. For example, we may want to expand PlayerCharacter to draw an avatar and work with a 3rd person camera.
I'm going to not make any big changes to PlayerCharacter partially just to show you how similar the PlayerCharacter class and the PlayerCharacter Game Component are. You'll see it takes very few code changes to make it a Game Component.
The first change to Character.cs is to change the declaration of the class to inherit from DrawableGameComponent. Doing so will give it all the capabilities of a DrawableGameComponent.
class PlayerCharacter : DrawableGameComponent
In order to support drawing directly from inside the component we're going to declare a GraphicsDevice in the class declarations:
private GraphicsDevice GraphicsCard;
The constructor gets changed quite a bit to import the game class into the Game Component:
public PlayerCharacter(Vector3 StartingPosition, Vector2 FacingTowards, Game game) : base(game)
We need to create an Initialize method that's overridden from the DrawableGameComponent class.
public override void Initialie()
{
GraphicsCard = Game.GraphicsDevice;
base.Initialize();
}
Notice that we can pull the GraphicsDevice straight from the main Game1 class. We get it here so that it can be used locally.
After that you override LoadContent(), UnloadContent(), and Update() not because we're using them, but just to show you that you can. The only one that we're really using for the PlayerCharacter class is Update, but it caused problems trying to move the existing Update over there. I didn't want to spend a lot of time trying to figure out how to completely rewrite the Update. So, I decided not to mess with it for this example. It works this way, and that's the main thing.
There are even fewer changes to the main Game1 class. We may declare things slightly different in future programs when we use Game Services with Game Components. But for now, we'll keep changes to a minimum.
In the constructor, we need to move the instantiation of the Character object.
Character = new PlayerCharacter(new Vector3(0f, 0f, 0f), new Vector2(0f, -1f), this);
Components.Add(Character);
Also, remove the Character instantiation from the Initialization method. It's being replaced by this in the constructor and added as a Game Component. This will make all those base. calls call our new Game Component.
Notice that the old Character instantiation is only different in that it has a 3rd parameter for the game object.
And I think that's pretty much it. You've changed a class into a Game Component.
Mainly, I wanted you to see there's not much difference between a class and a Game Component and how to convert a class. We'll be using Game Components from now on.
The Complete Source Code for Character.cs:
using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Media; namespace Holodeck { class PlayerCharacter : DrawableGameComponent { private GraphicsDevice GraphicsCard; private float Height = 1.75f; //Height at the top of the character's head. private float EyeLevel = 1.64f; //Height of the character's eyes. private BoundingBox Bounding; //Bounding box to define collisions with. private Vector3 Position = new Vector3(0f, 0f, 0f); //3D coordinates of where the character's feet are at. private Vector2 FaceTowards = new Vector2(0f, -1f); //Direction that the character's body is facing private float HeadPitch = 0f; //Radians that the character's head is pitched from straight ahead. private float MaxHeadPitch = MathHelper.ToRadians(80f); //Max head pitch angle upwards from level. private float HeadYaw = 0f; //Radians that the character's head is yawed from straight ahead. private Matrix CharactersViewMatrix; //View Matrix for what the character is looking at. private float WalkingVelocity = 2.7f; //Speed that the character will walk at in m/s. 1 to 1.5 is avg. private float StrafingVelocity = 2.0f; //Speed that the character will side step at per m/s. private float TurnRate = MathHelper.ToRadians(60f); //Radians to turn per second. private float HeadTurnRate = MathHelper.ToRadians(60f); //Radians to turn per second. public enum States { Standing, Walking, Backing, StrafingLeft, StrafingRight, Running, Sitting, Lying } public States State; public PlayerCharacter(Vector3 StartingPosition, Vector2 FacingTowards, Game game) : base(game) //=================================================================================== // PlayerCharacter() // // Purpose: Constructor for the class initializes the class on creation. // Parameters: StartingPosition - Sets the character's initial starting position. // FacingTowards - Sets the initial 2D direction the character is facing. // // Returns: Constructor // // Notes: I got to realizing how CPU expensive it is to calculate a new view for the // character every time the screen is drawn and decided to hold the value // as a field. That required it to be initialized. //=================================================================================== { Position = StartingPosition; FaceTowards = FacingTowards; UpdateBoundingBox(); //Create a bounding box around the character for collision. State = States.Standing; //Start the character out standing still. SetCharactersView(); } //=================================================================================== public override void Initialize() { GraphicsCard = Game.GraphicsDevice; base.Initialize(); } protected override void LoadContent() { base.LoadContent(); } protected override void UnloadContent() { base.UnloadContent(); } public override void Update(GameTime gameTime) { base.Update(gameTime); } public override void Draw(GameTime gameTime) { base.Draw(gameTime); } private void UpdateBoundingBox() { Vector3[] BoundingPoints = new Vector3[2]; float CharacterWidth = 0.5f; Vector3 LeftLowerFront; Vector3 RightUpperBack; Matrix ChangeMatrix; LeftLowerFront = new Vector3(-(CharacterWidth / 2f), 0f, (CharacterWidth / 2)); RightUpperBack = new Vector3((CharacterWidth / 2f), Height, -(CharacterWidth / 2)); ChangeMatrix = Matrix.CreateTranslation(Position); LeftLowerFront = Vector3.Transform(LeftLowerFront, ChangeMatrix); RightUpperBack = Vector3.Transform(RightUpperBack, ChangeMatrix); BoundingPoints[0] = LeftLowerFront; BoundingPoints[1] = RightUpperBack; Bounding = BoundingBox.CreateFromPoints(BoundingPoints); } public Vector3 IsAt //=================================================================================== // Property: IsAt // // Purpose: Allows the calling program to set the position of the character at will. // // Returns: Position // // Notes: This is very useful in collision detection. Another method requests movement // but nothing actually moves the character. So, the calling program can "see" // where the character is "trying" to go, and then decide whether to grant that // request or not. The calling program can even "partially" grant the request // because the IsAt property allows the caller to put the character in any // position it likes. This can also be used to make the character follow the // height of the terrain when walking by adding terrain height to the Y component. // // If a Position change occurs the view matrix needs to be updated or the camera // will not move to the new position. //=================================================================================== { get { return Position; } set { Position = value; //Move the character to a new spot. SetCharactersView(); //Update the character's view matrix. UpdateBoundingBox(); //Update bounding box position. } } //=================================================================================== public BoundingBox Boundingbox //=================================================================================== // Property: Boundingbox // // Purpose: Allows the calling program to get the character's bounding box. // // Returns: Bounding // // Notes: //=================================================================================== { get { return Bounding; } private set { ;} } //=================================================================================== public Matrix View //=================================================================================== // Property: View // // Purpose: Gives direct access to the character's view matrix. // // Returns: CharactersViewMatrix // // Notes: This has the potential to be used for something like "casting a spell" // where the character steps out of their body and looks back at themselves. // // It's primarily here, though, just to allow the calling program to get // the character's view matrix and I even considered not allowing a set. //=================================================================================== { get { return CharactersViewMatrix; } set { CharactersViewMatrix = value; } } //=================================================================================== public States CurrentState //=================================================================================== // Property: CurrentState // // Purpose: Manages the "state" of the character. // // Returns: State // // Notes: Right now, this allows for different states, such as walking, running, // sitting, etc. This is primarily for states of "motion". //=================================================================================== { get { return State; } set { State = value; } } //=================================================================================== private Matrix SetCharactersView() //=================================================================================== // SetCharactersView() // // Purpose: To easily get a view matrix for what the character is looking at. // Parameters: // Returns: CharactersViewMatrix - A "Look At" matrix which contains a view matrix for // what the character is looking at. // // Notes: This is provided to make it easy to get a look at view matrix for the character. // It is meant to simulate the character's head, which means LookTowards should be // limited to //=================================================================================== { //Creates a "lookat" vector in front of the character's face that looks where the head faces. CharactersViewMatrix = Matrix.CreateLookAt(EyePosition(), EyePosition() + LookTowards(), Vector3.Up); return CharactersViewMatrix; } //=================================================================================== public Vector3 EyePosition() //=================================================================================== // EyePosition() // // Purpose: To easily get a 3D position vector that represents the position of the // character's eyes. // Parameters: // Returns: Eyes - A 3D position vector for character's eye position. // // Notes: Position is where the character's feet are at and it doesn't make sense to // do something like place a camera there. You need to know where "eye level" // is at. This can then be used to calculate a "LookAt" vector for the // camera's view matrix since you can use EyePosition as the "tail" of the // vector. You can "translate" the LookAt vector by the EyePosition and // thereby create a LookAt immediately in front of the eyes. //=================================================================================== { Vector3 Eyes = new Vector3(0f, EyeLevel, 0f); Eyes = Position + Eyes; return Eyes; } //=================================================================================== private Vector3 LookTowards() //=================================================================================== // LookTowards() // // Purpose: To provide a 3D vector in the direction the character's head is facing. // Parameters: // Returns: LookingAt // // Notes: This should return a normalized vector that points in the direction that // the character is looking towards. This is different than the direction that // the character's body is facing. Adding the result vector to the position of // the character's eyes will give a "LookAt" vector for the camera matrix. // // Calculating the answer is a little tricky. FaceTowards is a 2D vector // that represents the direction the character's body is facing, which is // different from the direction the head is facing. The body is never allowed // to face up or down. So, FaceTowards.X is X in 3D, bute FaceTowards.Y is Z in // 3D. A little confusing but this keeps us from having problems with the body // facing in some odd upward or downward position due to a bug in the code. // (I had to think about what I'm going to do if the character is allowed to // lie down but I think I will assign that to a "fixed" camera.) // // The rotation formula is: // x = x*cos(angle) - y*sin(angle) // y = x*sin(angle) + y*cos(angle) // // For anyone new to the rotation formula, notice it takes two formulas to // rotate something in 2D space. http://en.wikipedia.org/wiki/Rotation_(mathematics) // // I started to use the rotation formula and then realized that I could project // the 2D vector into 3D space easily. Once in 3D space you can use rotation // matrices. // // I'm assuming that HeadYaw and pitch will be kept to realistic angles elsewhere. // // This is a lot of math to perform //=================================================================================== { Vector3 LookingAt; //Return value. Vector3 Right; Matrix LookAtMatrix = Matrix.Identity; //Empty matrix. Matrix YawMatrix = Matrix.Identity; //Empty matrix. LookingAt = new Vector3(FaceTowards.X, 0f, FaceTowards.Y); //Project FaceTowards into 3D space. Right = LookingAt; //Prepare to create a right facing vector from LookingAt YawMatrix = Matrix.CreateFromAxisAngle(Vector3.Up, HeadYaw); //Load matrix with yaw rotation. //Right is rotated 90 degrees from LookingAt to "make it" a right angle with LookingAt. Right = Vector3.Transform(Right, Matrix.CreateFromAxisAngle(Vector3.Down, MathHelper.ToRadians(90))); Right = Vector3.Transform(Right, YawMatrix); //Now rotate it by the yaw formula matrix. LookAtMatrix = Matrix.CreateFromAxisAngle(Right, HeadPitch); //Now we can pitch with the right vector. LookingAt = Vector3.Transform(LookingAt, LookAtMatrix); //Apply Pitch rotation. return LookingAt; } //=================================================================================== private Vector3 RequestWalk(GameTime Time) //=================================================================================== // RequestWalk() // // Purpose: Returns a vector of the distance change and direction that a walk causes. // Parameters: Time - GameTime object to get miliseconds since last frame. // Returns: MovePerFrame - Position change that a frame of walking causes. // // Notes: Position + MovePerFrame is where the character would walk to in one frame. // Instead of just going there, we're returning a vector for MovePerFrame as a // "suggested" spot to walk to. This allows the calling game to reject the // request if a wall or some object forbids the request from happening. //=================================================================================== { Vector3 MovePerFrame; MovePerFrame = new Vector3(FaceTowards.X, 0f, FaceTowards.Y); MovePerFrame.Normalize(); //FaceTowards "should" have been normalized, but I'm not taking chances. MovePerFrame *= (WalkingVelocity * (Time.ElapsedGameTime.Milliseconds / 1000f)); //Velocity/FPS. return MovePerFrame; //Ask to move the walking distance per frame in Facing direction. } //=================================================================================== public void TurnLeft(GameTime Time) //=================================================================================== // TurnLeft() // // Purpose: Turns the character's body to the left. // Parameters: Time - GameTime object to get miliseconds since last frame. // DependsOn: TurnRate - number of radians to turn every frame. Negative for left turns. // FaceTowards - This method changes the value of FaceTowards. // Returns: void // // Notes: The rotation formula is: // x = x*cos(angle) - y*sin(angle) // y = x*sin(angle) + y*cos(angle) // // For anyone new to the rotation formula, notice it takes two formulas to // rotate something in 2D space. http://en.wikipedia.org/wiki/Rotation_(mathematics) //=================================================================================== { float x = 0f; float z = 0f; float Turn = -TurnRate * (Time.ElapsedGameTime.Milliseconds / 1000f); x = FaceTowards.X * (float)Math.Cos(Turn) - FaceTowards.Y * (float)Math.Sin(Turn); z = FaceTowards.X * (float)Math.Sin(Turn) + FaceTowards.Y * (float)Math.Cos(Turn); FaceTowards = new Vector2(x, z); FaceTowards.Normalize(); //"Should" already be normalized, but "just in case". UpdateBoundingBox(); //Update the bounding box around the character. } //=================================================================================== public void TurnRight(GameTime Time) //=================================================================================== // TurnRight() // // Purpose: Turns the character's body to the left. // Parameters: Time - GameTime object to get miliseconds since last frame. // DependsOn: TurnRate - number of radians to turn every frame. // FaceTowards - This method changes the value of FaceTowards. // Returns: void // // Notes: The rotation formula is: // x = x*cos(angle) - y*sin(angle) // y = x*sin(angle) + y*cos(angle) // // For anyone new to the rotation formula, notice it takes two formulas to // rotate something in 2D space. http://en.wikipedia.org/wiki/Rotation_(mathematics) //=================================================================================== { float x = 0f; float z = 0f; float Turn = TurnRate * (Time.ElapsedGameTime.Milliseconds / 1000f); x = FaceTowards.X * (float)Math.Cos(Turn) - FaceTowards.Y * (float)Math.Sin(Turn); z = FaceTowards.X * (float)Math.Sin(Turn) + FaceTowards.Y * (float)Math.Cos(Turn); FaceTowards = new Vector2(x, z); FaceTowards.Normalize(); //"Should" already be normalized, but "just in case". UpdateBoundingBox(); //Update the bounding box around the character. } //=================================================================================== public void LookUp(GameTime Time) //=================================================================================== // LookUp() // // Purpose: Turns the character's head upwards. // Parameters: Time - GameTime object to get miliseconds since last frame. // DependsOn: HeadTurnRate - number of radians to turn every frame. Negative for left turns. // HeadPitch - Radians that the character's head is pitched up or down. // Returns: void // // Notes: //=================================================================================== { HeadPitch += HeadTurnRate * (Time.ElapsedGameTime.Milliseconds / 1000f); if (HeadPitch > MaxHeadPitch) { HeadPitch = MaxHeadPitch; } } //=================================================================================== public void LookDown(GameTime Time) //=================================================================================== // LookDown() // // Purpose: Turns the character's head downwards. // Parameters: Time - GameTime object to get miliseconds since last frame. // DependsOn: HeadTurnRate - number of radians to turn every frame. Negative for left turns. // HeadPitch - Radians that the character's head is pitched up or down. // Returns: void // // Notes: //=================================================================================== { HeadPitch += -HeadTurnRate * (Time.ElapsedGameTime.Milliseconds / 1000f); if (HeadPitch < -MaxHeadPitch) { HeadPitch = -MaxHeadPitch; } } //=================================================================================== private Vector3 RequestStrafe(GameTime Time) //=================================================================================== // RequestStrafe() // // Purpose: Returns a vector of the distance change and direction that a strafe causes. // Parameters: Time - GameTime object to get miliseconds since last frame. // Returns: MovePerFrame - Position change that a frame of strafing causes. // // Notes: Position + MovePerFrame is where the character would move to in one frame. // Instead of just going there, we're returning a vector for MovePerFrame as a // "suggested" spot to move to. This allows the calling game to reject the // request if a wall or some object forbids the request from happening. //=================================================================================== { Vector3 MovePerFrame; float x = 0f; float z = 0f; x = FaceTowards.X * (float)Math.Cos(MathHelper.ToRadians(90f)) - FaceTowards.Y * (float)Math.Sin(MathHelper.ToRadians(90f)); z = FaceTowards.X * (float)Math.Sin(MathHelper.ToRadians(90f)) + FaceTowards.Y * (float)Math.Cos(MathHelper.ToRadians(90f)); MovePerFrame = new Vector3(x, 0f, z); MovePerFrame.Normalize(); //FaceTowards "should" have been normalized, but I'm not taking chances. MovePerFrame *= (StrafingVelocity * (Time.ElapsedGameTime.Milliseconds / 1000f)); //Velocity/FPS. return MovePerFrame; //Ask to move the walking distance per frame in Facing direction. } //=================================================================================== public void Update(GameTime Time, ref Vector3 RequestedPositionChange) //=================================================================================== // Update() // // Purpose: Handle anything that should occur every time the main game's Update is called. // Parameters: Time - GameTime object to get miliseconds since last frame. // RequestedPositionChange - Should be empty coming in. Does not matter. // DependsOn: TurnRate - number of radians to turn every frame. Negative for left turns. // FaceTowards - This method changes the value of FaceTowards. // Returns: RequestedPositionChange - Vector, when combined with Position, gives the path // that we would "like" to move the character through. // // Notes: RequestedPositionChange is a way of working with a collision detection system. By // "requesting" a change in position from the main program, the main program can // decide whether to grant the request or "how" to grant the request. This // parameter is "supposed" to be empty when it comes in and this method fills in a // value for it so that the caller knows where the character wants to be repositioned // to. Since Position is exposed as a property (IsAt), the calling program can just // simply move the character whereever it likes. So, if this call returns a requested // move that the game doesn't like (for example would cause a collision) then the // game can adjust the move to something that conforms to the rules of the game. //=================================================================================== { switch (State) { case States.Walking: RequestedPositionChange = RequestWalk(Time); break; case States.Backing: //Making the movement vector negative will point it in the opposite direction. RequestedPositionChange = -RequestWalk(Time); break; case States.StrafingRight: RequestedPositionChange = RequestStrafe(Time); break; case States.StrafingLeft: RequestedPositionChange = -RequestStrafe(Time); break; //If the character is not doing anything else, the character is standing. default: RequestedPositionChange = Vector3.Zero; //Do standing actions. break; } } //=================================================================================== } }
The Complete Source Code for Holodeck.cs:
using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Media; namespace Holodeck { /// <summary> /// This is the main type for your game /// </summary> public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager GraphicsManager; //Rename this like so. GraphicsDevice GraphicsCard; //You can basically think of this as your graphics card or screen. BasicEffect effect; Texture2D GridTexture; //Stores our texture in memory. Model HoloDeckModel; //Stores our model in memory. Matrix HoloDeckModelWorldMatrix = Matrix.Identity; //Stores the position and sizing info for our model. Model Chair; //Stores the model named Chair. Matrix ChairsWorldMatrix; //Stores Chair's position, facing and scaling. BoundingBox ChairsBounding; //Bounding box for collision with the chair. bool ShowBoundingBoxes; //Used to toggle Bounding Box drawing on and off. Press B. bool IgnoreBKey; //Used to keep the toggle from toggling unwantedly. Matrix Projection; //Think of this as the "lens system" for our camera. PlayerCharacter Character; //Character object defines the camera and represents the player. private SoundEffect FootSteps; //Stores sound effect in memory. private SoundEffectInstance Steps; //Used for more control over the effect. const float HoloDeckWidth = 40f; //Our model is 2 units cubed. This will be the number to multiply that size by. public Game1() { GraphicsManager = new GraphicsDeviceManager(this); //Since we renamed this it has to be changed here too. //Changing the backbuffer is not absolutely necessary. I have a 16:9 screen and I like //to set the backbuffer to what I want it to be rather then let XNA pick for me. //The backbuffer is where things are drawn. Once the drawing is complete and ready to //be displayed on the screen the drawing on the backbuffer is sent to the front buffer. //The front buffer is basically your screen. That's why the size needs to match the //resolution of your screen. GraphicsManager.PreferredBackBufferWidth = 1280; //Screen width horizontal. Change this to fit your screen. GraphicsManager.PreferredBackBufferHeight = 720; //Screen width vertical. Change this to fit your screen. //You can run your game in full screen or windowed mode. Sometimes you write bugs //into your code that make it hard to recover in full screen mode. (Try Alt+F4 or //Ctrl+Alt+Del to get control in those situations.) GraphicsManager.IsFullScreen = false; //Feel free to set this to true once your code works. Content.RootDirectory = "Content"; //Start the player at the center of the holodeck and facing to the back. Character = new PlayerCharacter(new Vector3(0f, 0f, 0f), new Vector2(0f, -1f), this); Components.Add(Character); } /// <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 base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() { // TODO: Add your initialization logic here GraphicsCard = GraphicsManager.GraphicsDevice; //Introduce the Graphics Manager to our graphics card. Window.Title = "The Holodeck"; //Your window's title displays in the top left corner in windowed mode. effect = new BasicEffect(GraphicsCard); //We need to load up our projection matrix with our camera's Field of View, //our screen aspect ratio, and the clipping planes. Changes this are only //updated when you change the aspect ratio, I believe. So it's pretty much //set it up once and leave it alone. Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(40f), GraphicsCard.Adapter.CurrentDisplayMode.AspectRatio, 0.1f, 10000f); //Start the player at the center of the holodeck and facing to the back. //Character = new PlayerCharacter(new Vector3(0f, 0f, 0f), new Vector2(0f, -1f)); ShowBoundingBoxes = false; base.Initialize(); } /// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { GridTexture = Content.Load<Texture2D>("GridTexture"); //Load the Grid texture into memory from disk. HoloDeckModel = Content.Load<Model>("HoloDeckMesh"); //Load our .X file model for the room into memory. //We load up a CreateScale matrix to resize our model since it's about 2 units cubed in size. //The CreateScale matrix will have the info in it to correctly size our model according to the value //we pass to it. We combine it (through multiplication) with a CreateTranslation matrix loaded //up with a position change that moves our model upwards by half the width of our cube. HoloDeckModelWorldMatrix = Matrix.CreateScale(HoloDeckWidth) * Matrix.CreateTranslation(0.0f, HoloDeckWidth, 0.0f); //Resize our model larger and move it. //Load the "Chair" model into memory and position it at x,y,z coordinates 4,0,-2. //Chair Silla by "Ketchup" from Sketchup 8 Model Warehouse //Chair is an absolutely enormous model for our scale of one unit equals one meter. //We need to shrink Chair down to a normal size. I also want to have it facing more towards //the center of the room. You can comment out the various matrices to see what happens //without them. Chair = Content.Load<Model>("Chair"); //Place the chair where we want it. ChairsWorldMatrix = Matrix.CreateScale(0.035f); //Chair is WAY too big. ChairsWorldMatrix *= Matrix.CreateRotationY(MathHelper.ToRadians(30f)); //Combine a rotation of Chair by 30 degrees. ChairsWorldMatrix *= Matrix.CreateTranslation(new Vector3(-8f, 0f, -8f)); //Combine its world matrix with a movement/translation. ChairsBounding = SetBoundingBox(new Vector3(-0.75f, 0f, -0.75f), new Vector3(0.75f, 1.4f, 0.75f), new Vector3(-8f, 0f, -8f)); //TestBox = SetBoundingBox(new Vector3(-4f, 0f, 4f), new Vector3(4f, 4f, -4f), new Vector3(0f, 0f, 0f), 0f); FootSteps = Content.Load<SoundEffect>("WalkingInRoom"); //Sound effect to play when character walks. Steps = FootSteps.CreateInstance(); //Needed to be able to pause the effect. } protected BoundingBox SetBoundingBox(Vector3 LeftLowerBack, Vector3 RightUpperFront, Vector3 Positioned) { //This method defines a BoundingBox by taking two opposite corners of the box in order //to define the box, and then moving the box to the specified position. If this position //matches the position of the object that the box represents, it can be used for collision detection. Matrix ChangeMatrix; //Holds position changes of the bounding box. ChangeMatrix = Matrix.CreateTranslation(Positioned); LeftLowerBack = Vector3.Transform(LeftLowerBack, ChangeMatrix); //Move one corner of the box. RightUpperFront = Vector3.Transform(RightUpperFront, ChangeMatrix); //Move the other corner. //Define the box. return new BoundingBox(LeftLowerBack, RightUpperFront); } protected void DrawBoundingBox(BoundingBox Box, BasicEffect Shader) { //This method is helpful when troubleshooting bounding boxes. It's difficult to tell what //the bounding boxes are doing when you can't see them. So, this method draws the boxes //in yellow. VertexPositionColor[] PointList; //Vertices. Vector3[] BoxCorners; //Holds the query results when asking the bounding box for its corners. short[] BoxIndices; //Indices. PointList = new VertexPositionColor[8]; //Vertices BoxCorners = new Vector3[8]; //Will need to capture bounding box's vertices. BoxCorners = Box.GetCorners(); //Query the bounding box for its corners. //Define the vertices of the bounding box by translating the query results from the //bounding box into actual vertices. They are not in winding order though. for (int i = 0; i < 8; i++) { PointList[i].Position = BoxCorners[i]; PointList[i].Color = Color.Yellow; //Draw bounding boxes in bright yellow. } //Order that GetCorners() spits out the vertices in. //0 LeftTopFront //1 RightTopFront //2 RightBottomFront //3 LeftBottomFront //4 LeftTopBack //5 RightTopBack //6 RightBottomBack //7 LeftBottomBack //Wind the bounding box vertices. We are just defining edges here. The box is //transparent, so winding order doesn't matter. BoxIndices = new short[24]; BoxIndices[0] = 0; BoxIndices[1] = 1; BoxIndices[2] = 1; BoxIndices[3] = 2; BoxIndices[4] = 2; BoxIndices[5] = 3; BoxIndices[6] = 3; BoxIndices[7] = 0; BoxIndices[8] = 4; BoxIndices[9] = 5; BoxIndices[10] = 5; BoxIndices[11] = 6; BoxIndices[12] = 6; BoxIndices[13] = 7; BoxIndices[14] = 7; BoxIndices[15] = 4; BoxIndices[16] = 0; BoxIndices[17] = 4; BoxIndices[18] = 1; BoxIndices[19] = 5; BoxIndices[20] = 3; BoxIndices[21] = 7; BoxIndices[22] = 2; BoxIndices[23] = 6; //Setup Shader for the Draw. Shader.World = Matrix.Identity; Shader.Projection = Projection; Shader.View = Character.View; Shader.VertexColorEnabled = true; //Must be enabled for the vertices to be colored. //There should only be one pass, but it won't really hurt to have the loop. foreach (EffectPass pass in Shader.CurrentTechnique.Passes) { pass.Apply(); GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionColor>(PrimitiveType.LineList, PointList, 0, 8, BoxIndices, 0, BoxIndices.Length / 2); } } /// <summary> /// UnloadContent will be called once per game and is the place to unload /// all content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here } /// <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 override void Update(GameTime gameTime) { KeyboardState KBState; //Check keyboard. Vector3 RequestedMovement = Vector3.Zero; //If character tries to move, this is where it wants to go. Vector3 CharacterWasAt; //Used to verify whether a collision occured. bool CollisionOccured = false; //Flag to keep track of a collision happening. // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); CharacterWasAt = Character.IsAt; //Remember where character was at before the requested move. Character.Update(gameTime, ref RequestedMovement); Character.IsAt = CollisionCheck(Character.IsAt, RequestedMovement); //Try to move the character. //Check if requested move was granted, but only if no move was requested. if ((CharacterWasAt + RequestedMovement != Character.IsAt) && RequestedMovement != Vector3.Zero) { CollisionOccured = true; } KBState = Keyboard.GetState(); if (KBState.IsKeyDown(Keys.Escape)) this.Exit(); Character.State = PlayerCharacter.States.Standing; //Control + another key pressed. if (KBState.IsKeyDown(Keys.LeftControl) || KBState.IsKeyDown(Keys.RightControl)) { if (KBState.IsKeyDown(Keys.D) || KBState.IsKeyDown(Keys.Right)) { Character.State = PlayerCharacter.States.StrafingRight; } if (KBState.IsKeyDown(Keys.A) || KBState.IsKeyDown(Keys.Left)) { Character.State = PlayerCharacter.States.StrafingLeft; } if (KBState.IsKeyDown(Keys.Up) || KBState.IsKeyDown(Keys.W)) { Character.State = PlayerCharacter.States.Walking; } if (KBState.IsKeyDown(Keys.Down) || KBState.IsKeyDown(Keys.S)) { Character.State = PlayerCharacter.States.Backing; } } else { //Control not pressed, but check if keys were pressed without Control. if (KBState.IsKeyDown(Keys.Up) || KBState.IsKeyDown(Keys.W)) { Character.State = PlayerCharacter.States.Walking; } if (KBState.IsKeyDown(Keys.Down) || KBState.IsKeyDown(Keys.S)) { Character.State = PlayerCharacter.States.Backing; } if (KBState.IsKeyDown(Keys.Left) || KBState.IsKeyDown(Keys.A)) { Character.TurnLeft(gameTime); } if (KBState.IsKeyDown(Keys.Right) || KBState.IsKeyDown(Keys.D)) { Character.TurnRight(gameTime); } if (KBState.IsKeyDown(Keys.PageUp) || KBState.IsKeyDown(Keys.E)) { Character.LookUp(gameTime); } if (KBState.IsKeyDown(Keys.PageDown) || KBState.IsKeyDown(Keys.Q)) { Character.LookDown(gameTime); } //The B key must be released before the toggle can happen again. This prevents the //toggle from happening a dozen times from one press of the button. if (KBState.IsKeyUp(Keys.B)) { IgnoreBKey = false; } //The B key toggles bounding box drawing on and off if (KBState.IsKeyDown(Keys.B)) { //Don't toggle 20 times for every time the B key is pressed. if (!IgnoreBKey) { //Toggle the bounding boxes on or off. if (ShowBoundingBoxes == true) ShowBoundingBoxes = false; else ShowBoundingBoxes = true; IgnoreBKey = true; //Don't allow another toggle until the B is released. } } } //Play the foot steps sound effect when the character moves. if (Character.State == PlayerCharacter.States.Walking || Character.State == PlayerCharacter.States.Backing || Character.State == PlayerCharacter.States.StrafingLeft || Character.State == PlayerCharacter.States.StrafingRight) { //The sound effect will start in a "Stopped" state and so //this will get it started playing for the first time. if (Steps.State == SoundState.Stopped) { Steps.Volume = 0.08f; //The holodeck is so silent that this is still a bit loud. Steps.IsLooped = true; //Keep repeating the sound until told to stop. Steps.Play(); //Start playing the foot steps sound effect. } else { if (CollisionOccured) { //Don't play foot step sounds while a collision is going on. Steps.Pause(); } else { //As long as no collision is happening... //If the effect is paused continue it. Steps.Resume(); } } } else { //If the character stops moving pause the foot steps effect. if (Steps.State == SoundState.Playing) { Steps.Pause(); } } base.Update(gameTime); } protected Vector3 CollisionCheck(Vector3 Position, Vector3 RequestedMove) { //This method is used to do some very simple collision checking. First, the walls are //made solid, by not allowing the character to move through them. A "force field" //is put in front of the walls, because otherwise the camera will get so close that //near plane clipping will occur and the wall will disappear. Who puts their eyeball //against a wall anyway. It just makes sense that you will remain some slight distance //away from the wall. //Second, we are doing some basic collision detection with the chair model. XNA only //supports Axis Aligned Bounding Boxes (AABB). This means that the bounding boxes can //NEVER be rotated. For now, we live with this constraint in order to get a basic //idea of how bounding box collision works. Vector3 GrantedMove; //Returns where the character is allowed to move to. Vector3 RequestedResult; //Combines Position + RequestedMove. float WallField = 0.4f; //Wall "Force Field" thickness. BoundingBox NewPosition; //Position being tested as to whether a move there is allowed. Vector3 TranslatedMin; //Simulates one corner of the bounding box after being moved. Vector3 TranslatedMax; //Simulates the other corner of the bounding box after being moved. Vector3 RequestedX; //JUST the X axis part of the requested movement. Vector3 RequestedZ; //JUST the Z axis part of the requested movement. //Grant the move and then revoke the grant if there is a reason. RequestedResult = Position + RequestedMove; GrantedMove = RequestedResult; //Move granted. //Prevent the character from stepping outside of the virtual reality in the X direction. if (RequestedResult.X <= -HoloDeckWidth + WallField) { GrantedMove.X = -HoloDeckWidth + WallField; } if (RequestedResult.X >= HoloDeckWidth - WallField) { GrantedMove.X = HoloDeckWidth - WallField; } //Prevent the character from stepping outside of the virtual reality in the Z direction. if (RequestedResult.Z <= -HoloDeckWidth + WallField) { GrantedMove.Z = -HoloDeckWidth + WallField; } if (RequestedResult.Z >= HoloDeckWidth - WallField) { GrantedMove.Z = HoloDeckWidth - WallField; } //Test to see if the character is colliding with the chair model. TranslatedMin = Character.Boundingbox.Min + RequestedMove; //Simulate one corner of box moving. TranslatedMax = Character.Boundingbox.Max + RequestedMove; //Simulate other corner of the box moving. NewPosition = new BoundingBox(TranslatedMin, TranslatedMax); //Simulated movement of bounding box. if (NewPosition.Intersects(ChairsBounding)) { //Character collided with the chair. GrantedMove = Position; //Tell the character to not move an inch. //But let's see if the character can "slide around" the object instead. //Try moving in the X direction. RequestedX = new Vector3(RequestedMove.X, 0f, 0f); TranslatedMin = Character.Boundingbox.Min + RequestedX; //Set position of test box. TranslatedMax = Character.Boundingbox.Max + RequestedX; //Set position of test box. NewPosition = new BoundingBox(TranslatedMin, TranslatedMax); //Construct test box. if (NewPosition.Intersects(ChairsBounding)) { //X movement is blocked. //Try moving in the Z direction. RequestedZ = new Vector3(0f, 0f, RequestedMove.Z); TranslatedMin = Character.Boundingbox.Min + RequestedZ; //Set position of test box. TranslatedMax = Character.Boundingbox.Max + RequestedZ; //Set position of test box. NewPosition = new BoundingBox(TranslatedMin, TranslatedMax); //Construct test box. if (NewPosition.Intersects(ChairsBounding)) { //All movement is blocked } else { //Z movement is not blocked. Do it. GrantedMove = Position + RequestedZ; } } else { //X movement is not blocked. Do it. GrantedMove = Position + RequestedX; } } return GrantedMove; } /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Black); //Normally you don't HAVE to change the RasterizerState but we want to change the //CullMode. The rasterizer is part of the screen drawing mechanism and we're telling //XNA how things should be drawn. The cull mode tries to cull out triangles that don't //"need" to be drawn. The default is CullCounterClockwiseFace which tells XNA not to //draw the back sides of triangles. Most of the time this makes sense; you can't "see" //the triangles that are facing away from you, so why draw them; it's just wasting CPU //time. It decides which side is the front of the triangle by the cull mode and the //direction you "wind", or define, the vertices in. This is why you should always //wind your vertices clockwise according to the direction you want them to face. //Since we're debugging, there's a good chance that we messed up and wound our //vertices in the wrong direction. So, we turn off culling and draw both sides of //all triangles by setting the cull mode to None. Once we get the program working, //we can change CullMode back to CullCounterClockwiseFace and our game will run //faster. RasterizerState RS = new RasterizerState(); //RS.CullMode = CullMode.CullCounterClockwiseFace; //Better for debugging draw problems. RS.CullMode = CullMode.None; //Game runs better RS.FillMode = FillMode.Solid; GraphicsCard.RasterizerState = RS; foreach (ModelMesh Mesh in HoloDeckModel.Meshes) { //Here we define an effect. The effect is kind of like the pen or brush that we will "draw" our //3D world with. It's basically what they call a "shader". Shaders are code that draw things on //the screen. XNA 4.0 has about 5 different effects, or shaders, built into it. BasicEffect is the one //you will use most of the time. The others are more for special purposes. //We're loading up a BasicEffect here so that we can use it. We could have loaded up one of the other //effects or written our own effect and loaded it up. Writing your own effects is done in a language //called HLSL (High Level Shader Language). You don't really need to do that starting out though; //because BasicEffect does a pretty good job and you really need to know what you're doing to write a //shader that does a better job drawing the screen than the built in effects. foreach (BasicEffect Shader in Mesh.Effects) //Define an "effect" to draw our model with. { Shader.EnableDefaultLighting(); Shader.TextureEnabled = true; //We're going to draw using a texture. Shader.Texture = GridTexture; //The model will draw without this call, but it will use the texture in the file. Shader.LightingEnabled = false; //Don't use BasicEffect's "advanced" lighting for now. Shader.World = HoloDeckModelWorldMatrix; //Tell the shader what World Matrix to use to draw the object. Shader.View = Character.View; //Tell the shader where to draw from (the camera). Shader.Projection = Projection; //Tell the shader how to draw (like the camera lens... sort of). } Mesh.Draw(); //Draw the mesh } //Draw the chair. foreach (ModelMesh Mesh in Chair.Meshes) { foreach (BasicEffect Shader in Mesh.Effects) //Define an "effect" to draw our model with. { Shader.EnableDefaultLighting(); Shader.TextureEnabled = true; //We're going to draw using a texture. Shader.World = ChairsWorldMatrix; //Tell the shader what World Matrix to use to draw the object. Shader.View = Character.View; //Tell the shader where to draw from (the camera). Shader.Projection = Projection; //Tell the shader how to draw (like the camera lens... sort of). } Mesh.Draw(); //Draw the mesh } //If Show Bounding Boxes is toggled on then draw them. if (ShowBoundingBoxes) { DrawBoundingBox(ChairsBounding, effect); DrawBoundingBox(Character.Boundingbox, effect); } base.Draw(gameTime); //Always include this. } } }
Originally, there wasn't supposed to be a Part VII for this tutorial. But as I got to preparing for the next tutorial, I realized that I wanted to introduce Game Components. The problem is that you really need to see how a game component is similar and dissimilar to a normal XNA/C# class. It's not a lot different and so it might not be clear if I start over with brand new code (which is what I intend to do for the next tutorial). So, I decided to modify the Holodeck code to turn the PlayerCharacter object into a Game Component and that's why there's a Part VII.