Project+2+(single+page)

toc

In this document, we will learn to make a 3 dimensional simulation in Microsoft's XNA Game Studio. It also discusses how to add sound effects to a simulation.

=Basics=

To begin, we will go over the basics to creating a 3 dimensional interface. For this document, we will be using the tutorial models and textures that come with XNA Game Studio. You can create your own models and textures, but that will not be covered here.

Create a new Project
To start, we need to create a new project in Visual Studio. On the new Program screen, we need to select the menu for XNA Game Studio 3.1, and then the option for Windows Game 3.1. If you wanted to create a game for the Xbox 360 or Zune, you would choose the appropriate option for that system instead.



Don't forget to name your program, and ensure the path is correct. When you are ready, press Ok. This will generate the basic framework necessary for your game. Now we need to do a few things to setup the program to use 3 dimensional models. To do this, we need to add a couple folders under the Content node in the Solution Explorer in Visual Studio. Right click on the Content node and select Add > New Folder. Do this twice. Name one folder “Models”, and the other “Textures”.

Now right click on the Models folder and select Add > Existing Item. This will bring up an explorer window. Navigate to the location of your model. Since we are using the tutorial model, ours will be called p1_wedge.fbx. After you have done this, your Solution Explorer should look like this:



Now we need to add a texture. This is the same, except we right click on the Textures folder, and select Add > Existing Item. Our texture will be called wedge_p1_diff_v1.tga. Once this is in your Textures folder, we are ready to start working with the code.

Load Models
In order to load a model, we first need to add a model object to the code. This is simply done by declaring it using the Model type, as shown below.

code //Set model to be drawn Model myModel;

code

This gives us a Model object to work with. We also need a float for the aspect ratio. This will tell the computer how to show a three dimensional object on the two dimensional screen.

code //determines how to 3d to 2d projections float aspectRatio;

code

Now we need to find the LoadContent method. This is the method that is used to load all of our model and sprite objects into our game. We need to add two lines to the method to load our model. These are shown below:

code myModel = Content.Load("Models\\p1_wedge"); aspectRatio = graphics.GraphicsDevice.Viewport.AspectRatio;

code This code sets the Content Pipeline to load your model when the LoadContent method is called. Notice that the name of the model no longer has the file extension attached to it. This is the default formatting for model names, however you can change asset names if you wish. The second line of code here sets our aspectRatio. The actual value of this will depend on the screen that is being used. This way, the image will be to scale with the screen, meaning it will get larger as you put it on a larger screen. We now have our model loading into the system. It is time to make the program draw it.

Display Model
To make a model display, we need to modify the draw method, as well as add a few variables to the code. These variables are what tells the program where to place the model when it is run. We also have an extra float variable, modelRotation. We will be using this in a little while to make the model rotate. For now, don't worry about it.

code //Set the position of the model in world space, and set the rotation. Vector3 modelPosition = Vector3.Zero; float modelRotation = 0.0f;

//Set the position of the camera in world space, for our view matrix. Vector3 cameraPosition = new Vector3(0.0f, 50.0f, 5000.0f);

protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

//Copy any parent transforms. Matrix[] transforms = new Matrix[myModel.Bones.Count]; myModel.CopyAbsoluteBoneTransformsTo(transforms);

//Draw the model. A model can have multiple meshes, so loop. foreach (ModelMesh mesh in myModel.Meshes) {   //This is where the mesh orientation is set, as well //as our camera and projection. foreach (BasicEffect effect in mesh.Effects) {     effect.EnableDefaultLighting; effect.World = transforms[mesh.ParentBone.Index] * Matrix.CreateRotationY(modelRotation)

Matrix.CreateTranslation(modelPosition); effect.View = Matrix.CreateLookAt(cameraPosition,     Vector3.Zero, Vector3.Up); effect.Projection = Matrix.CreatePerspectiveFieldOfView(     MathHelper.ToRadians(45.0f), aspectRatio, 1.0f, 10000.0f); }   //Draw the mesh, using the effects set above. mesh.Draw; } base.Draw(gameTime); }

code

At this point, you can compile and run the simulation. It will open a window with the model. It will be rather dull however, as the model is just going to sit in the middle of the screen and do nothing. Here is what it should look like:



Now lets make it rotate to add a bit of visual interest. To do this, we need to change the update method. To do this, we only need to add a single line of code to Update, which will leave it looking as follows:

code protected override void Update(GameTime gameTime) { //Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit;

modelRotation += (float)gameTime.ElapsedGameTime.TotalMilliseconds * MathHelper.ToRadians(0.1f);

base.Update(gameTime); }

code

All we added to this method was the line:

code modelRotation += (float)gameTime.ElapsedGameTime.TotalMilliseconds * MathHelper.ToRadians(0.1f);

code

This will make the object rotate. The other code in the method that is there automatically is what allows the program to exit, and what makes it update the screen. Now compile and run the program again. This time, the ship should rotate counter-clockwise.



=Manipulating the Models=

Now that we have our ship appearing on the screen, it is time to make it move about. Before doing any of this, you may want to remove the rotation code that we wrote in the last section. Otherwise your ship will spin out of control while you move it about.

Create Movement Variables
Before we can start moving about, we need a way to track where our model is and its orientation. Luckily, if you have been following along, we already have a variable for the position and rotation of the model, so now we will just need a vector for the model's velocity:

code Vector3 modelVelocity = Vector3.Zero; code

Now we need to edit the Update method to handle the movement of our object. To do this, we will have the code first check for input, which we will write in the next section. Then we will have the ship move and lose some momentum. This way it will not continue to move in the same direction indefinitely. Here is the new update method:

code protected override void Update(GameTime gameTime) { //Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit;

//Get some input. UpdateInput;

//Add velocity to the current position. modelPosition += modelVelocity;

//Bleed off velocity over time. modelVelocity *= 0.95f;

base.Update(gameTime); }

code

Don't worry about that call to UpdateInput for now. We will be taking care of that shortly. Notice that we add the velocity to the position. That is what causes the ship to move in the direction of the velocity. We then remove five percent of the velocity to simulate loss of momentum as the model moves. Finally, we call Update recursively to continue running the game.

Getting User Input
Now lets add our keyboard commands. Lets start with the easiest change. Currently, the game will exit if the back button on an Xbox 360 Gamepad is pressed. However, we are running on a computer, not a Xbox 360. So lets make the escape key close the program as well. To do this, find the line in the Update method:

code if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit;

code

Now add the code to make the escape key work as well:

code if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||                                                     Keyboard.GetState.IsKeyDown(Keys.Escape)) this.Exit;

code

This will make the code check both the Gamepad and the keyboard to see if the program should exit. Since we kept the Gamepad instructions for this method, we will also code all our movement to be controllable with the Xbox 360 Gamepad as well as our keyboard. This will add a little bit of complication to our UpdateInput method, but it will allow our game to be more usable later on. Here is the entire UpdateInput method. Don't worry, we will discuss each part in a moment.

code protected void UpdateInput { //Get the game pad state. GamePadState currentState = GamePad.GetState(PlayerIndex.One); KeyboardState currentKeyState = Keyboard.GetState;

//Rotate the model, and scale it down if (currentKeyState.IsKeyDown(Keys.A)) modelRotation += 0.10f; else if (currentKeyState.IsKeyDown(Keys.D)) modelRotation -= 0.10f; else modelRotation -= currentState.ThumbSticks.Left.X * 0.10f;

//Create some velocity. Vector3 modelVelocityAdd = Vector3.Zero;

//Find out what direction we should be thrusting, using rotation. modelVelocityAdd.X = -(float)Math.Sin(modelRotation); modelVelocityAdd.Z = -(float)Math.Cos(modelRotation);

//Now scale our direction. if (currentKeyState.IsKeyDown(Keys.W)) modelVelocityAdd *= 1; else modelVelocityAdd *= currentState.Triggers.Right;

//Finally, add this vector to our velocity. modelVelocity += modelVelocityAdd;

GamePad.SetVibration(PlayerIndex.One, currentState.Triggers.Right,                                       currentState.Triggers.Right);

//In case you get lost, press A to warp back to the center. if (currentState.Buttons.A == ButtonState.Pressed || currentKeyState.IsKeyDown(Keys.Enter)) {   modelPosition = Vector3.Zero; modelVelocity = Vector3.Zero; modelRotation = 0.0f; } }

code

Ok, lets look at this piece by piece. First off, we have the code to get the current state of our input devices.

code GamePadState currentState = GamePad.GetState(PlayerIndex.One); KeyboardState currentKeyState = Keyboard.GetState; code

Next, we rotate our model. We first need to check the keyboard inputs A and D. A will cause the model to rotate counterclockwise and D will cause the model to rotate clockwise. If neither of those is being pressed, the system will check the state of the Gamepad's Left Thumbstick. This can be rotated either direction, and the code here will allow for both. Using the Gamepad greatly simplifies the rotation code from four lines on the keyboard to one.

code if (currentKeyState.IsKeyDown(Keys.A)) modelRotation += 0.10f; else if (currentKeyState.IsKeyDown(Keys.D)) modelRotation -= 0.10f; else modelRotation -= currentState.ThumbSticks.Left.X * 0.10f;

code

Now, we will look at how we handle the velocity of our ship. We first create a new vector to hold how much our velocity should be changing depending on it's orientation and whether it is thrusting. Next, we figure what direction the model should thrust if you tell it to. This is done using trigonometric properties and the rotation angle we obtained earlier. Next, we determine if we are infact thrusting. This is done using the W key on the keyboard, or the right trigger on the gamepad. Finally, we will add our vector to the current velocity to give us our updated velocity. Finally, we enable the vibration of the gamepad if the trigger is being pulled. On the keyboard, there is no vibration system, so we don't have any equivalent code for this.

code Vector3 modelVelocityAdd = Vector3.Zero;

//Find out what direction we should be thrusting, using rotation. modelVelocityAdd.X = -(float)Math.Sin(modelRotation); modelVelocityAdd.Z = -(float)Math.Cos(modelRotation);

//Now scale our direction. if (currentKeyState.IsKeyDown(Keys.W)) modelVelocityAdd *= 1; else modelVelocityAdd *= currentState.Triggers.Right;

//Finally, add this vector to our velocity. modelVelocity += modelVelocityAdd;

GamePad.SetVibration(PlayerIndex.One, currentState.Triggers.Right, currentState.Triggers.Right); code

One last thing we will look at is resetting the ship. It is quite possible to fly off the screen and get lost. If this happens, we want to be able to press a single button and bring the ship back. In our code, we have the A button on the gamepad or the enter key on the keyboard do this. When either of these buttons is pressed, the ship returns to its initial position and rotation.

code if (currentState.Buttons.A == ButtonState.Pressed || currentKeyState.IsKeyDown(Keys.Enter)) { modelPosition = Vector3.Zero; modelVelocity = Vector3.Zero; modelRotation = 0.0f; }

code

You can compile and run this code. You should be able to steer your ship around the screen. You may notice that we do not have code to move our ship up and down. This code was not necessary to show how to code the movement of the ship, and thus we did not include it. If you want to change the elevation of the ship, you will simply need to add code to alter the Y component of your velocity just like we have done with the X and Z components. The Rotation may prove a bit more complex, but should still be possible using trigonometric identities. You can also change the cameraPosition vector that we set a while back to alter where the camera is in relation to the ship.

= = =Adding Sounds=

Now we are going to shift to a subject that is applicable in both 2 dimensional and 3 dimensional games. A key factor in many games are sound effects that add a layer of depth to the game. This sound can be effects when a button is pressed, or even background music.

Load Wave Files
In the XNA Game Studio, audio is stored in wave files. This means we will need some of these files before we can add any sound to our game. For this document, we are using the prepackaged sound files that were included with the XNA Game Studio. To load the wave files into the content pipeline like we did with the models, first we need to create a folder for them. Right click on the Content node in the Solution Explorer and select Add > New Folder and name the new folder Audio. Then right click on the new Audio folder, and create a new folder within it called Waves. Now just right click on the Waves folder, and select Add > Existing item, then navigate to the wave files you want. For our demonstration here, we will have two wave files, engine_2.wav and hyperspace_activate.wav. When your done, the solution explorer should look like this:



With the waves now showing in the content node of the solution explorer, it's time to add them to the content pipeline. First, we need some variables for our sound effects.

code //Set the sound effects to use SoundEffect soundEngine; SoundEffectInstance soundEngineInstance; SoundEffect soundHyperspaceActivation;

code

These variables then need to be assigned in the LoadContent method.

code protected override void LoadContent { //Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice);

myModel = Content.Load("Models\\p1_wedge");

//Load Sound Effects soundEngine = Content.Load("Audio\\Waves\\engine_2"); soundEngineInstance = soundEngine.CreateInstance; soundHyperspaceActivation = Content.Load("Audio\\Waves\\hyperspace_activate");

aspectRatio = graphics.GraphicsDevice.Viewport.AspectRatio; } code

We have added three new lines to this code. The first loads the Engine_2.wav file to the content pipeline, and assigns that to soundEngine. The second line then creates an instance of this soundEffect, and assigns that to the soundEngineInstance variable. Finally, we load the hyperspace_activate.wav file the same way we did the engine_2.wav. This file will not need a soundEffectInstance variable. We will explain why after showing you how to use each.

Use the Audio API to play sounds
Now that we have our wave files in the content pipeline, we can use them in our code. First, lets add the engine sounds whenever we are moving our ship forward. To do this, we use the following code, which is added to the UpdateInput method we created earlier.

code //Play engine sound only when the engine is on. if (currentState.Triggers.Right > 0 || currentKeyState.IsKeyDown(Keys.W)) {

if (soundEngineInstance.State == SoundState.Stopped) {   soundEngineInstance.Volume = 0.75f; soundEngineInstance.IsLooped = true; soundEngineInstance.Play; } else soundEngineInstance.Resume; } else if (currentState.Triggers.Right == 0 || !currentKeyState.IsKeyDown(Keys.W)) { if (soundEngineInstance.State == SoundState.Playing) soundEngineInstance.Pause; } code

This will cause the engine sound to play whenever the W key is being pressed on the keyboard, or if the right trigger is pulled, and cause it to stop when the button or trigger are released. This code should be placed right before the block of code that allows the ship to return to its default location. Let us now look at that code, and add the hyperspace sound effect to that section.

code //In case you get lost, warp back to the center. if (currentState.Buttons.A == ButtonState.Pressed || currentKeyState.IsKeyDown(Keys.Enter)) { modelPosition = Vector3.Zero; modelVelocity = Vector3.Zero; modelRotation = 0.0f; soundHyperspaceActivation.Play; } code

For this we simply use the play method of the sound effect after returning. This will play this sound file every time the ship is sent back to the default location by pressing the A button on the Xbox 360 Gamepad or the Enter key on the keyboard. Go ahead and compile and run your code to make sure it works. Now that we have seen how both sound files are being used, we can explain why the hyperspace sound effect did not need a soundEffectInstance assigned to it. With the soundEffect class, you can only play a sound, but cannot stop or pause it after it has begun. These functions are implemented in the soundEffectInstance class. The hyperspace sound does not need to stop when it is triggered, the entire sound clip plays every time. Therefore, this added functionality was not needed. However, the engine sound file does pause whenever the ship is not thrusting forward. Therefore, it did need an instance.