News
DirectX
Links
Contact Me

In Association with Amazon.com
In Association with Amazon.ca

$5 via PayPal


Moving in a 3D World



Moving in a 3D World

In this lesson, we will create a small room and allow the player to move around it using a First-Person perspective. To keep things simple, collision detection is not implemented, so you can walk through walls.

The geometry for our room is loaded from disk in 3 pieces: floor, walls, and ceiling. The file format is very simple. It's basically a texture name and a list of vertices. Nothing fancy there. Since the focus of this lesson is on moving around in a 3D world, I'm not going into any detail on the geometry loading. It was just a simple way to get our world read in. The rendering of our geometry is done by the dhSimpleMesh class which has been covered in previous tutorials.

Keyboard Handling

First, let's go over the keyboard handling. Windows messaging provides all we need for this tutorial, so that's what we're using. All we really need is to know whether a given key is up or down. Given that this lesson can run in windowed mode, we also want to make sure we're only reading keys sent to our window. An earlier version of this tutorial queried the keyboard state directly, so when in windowed mode you could end up moving around while typing in another app.

Here are the global definitions for our keyboard handling setup:

//These index into our key array for movement, 'cuz Laura wouldn't let me use
//direct Keyboard querying
enum{
   KEY_UP=0,
   KEY_DOWN,
   KEY_LEFT,
   KEY_RIGHT,
   KEY_SPACE,
   KEY_COUNT
};
//Are the keys up or down?
bool g_key_state[KEY_COUNT];

Our array, g_key_state, is an array of booleans which simply tracks which keys are up or down. Our enumeration relates a key name to an index into the array. And because of the magic of enumerations, KEY_COUNT is equal to the number of keys in our array. If we want to add another key (KEY_CTRL to fire, for example) we can add it to the enumeration before KEY_COUNT and our allocations and key handling will adjust to fit automatically.

To properly track our key states, we need to initialize them before we use them. That's what our init_keys function does. It simply loops through setting all of our keys to false (up, or not pressed).

// Procedure:init_keys
// Whazzit:Initializes all of our keys to false (up)
void init_keys(void){
int count;

   for(count=0;count < KEY_COUNT;count++){
      g_key_state[count]=false;
   }

}

Now that we have them declared and initialized we need the tracking code. When a key is pressed, the appropriate key state in our array is set to true, and when it's released it's set to false. This is done in our window message handler. We've gone over the message handler many times before, so I'll just show the bit that processes KEYUP messages.

case WM_KEYUP:
   switch(virt_key){
      case VK_UP:
         g_key_state[KEY_UP]=false;
         break;
      case VK_DOWN:
         g_key_state[KEY_DOWN]=false;
         break;
      case VK_LEFT:
         g_key_state[KEY_LEFT]=false;
         break;
      case VK_RIGHT:
         g_key_state[KEY_RIGHT]=false;
         break;
      case VK_SPACE:
         g_key_state[KEY_SPACE]=false;
         break;

   }

Now that we've built it all, we need an example of how to use it. The following bit of code tests to see if the player is holding down the right or left arrow keys. The code to handle the turn has been removed for clarity. We'll come back to it later.

if(g_key_state[KEY_RIGHT]){
   //Turn right
}else if(g_key_state[KEY_LEFT]){
   //Turn left
}

As you can see, if the user holds down both the left and right arrows, he'll turn right because that's the key that's tested first. You could do more complicated processing to make him not turn at all, or to try to detect which key was pressed more recently, but that would be overkill for our simple example.

The really important code is in the Update procedure. This is where all the First-Person-style camera work is done, so that's what we'll cover next.


First-Person Camera

It was handsome at the auction, oh but when we got it home, it grew into something we could no longer contain
Where's our First-Person camera, by now he could be anywhere and after all that training.
- The Tragically Hip, First Person Camera

We know from the previous tutorials that we can treat our View Matrix like a camera. This isn't the most efficient solution because every time you change the View Matrix D3D has to make a number of internal adjustments. But it is a quick and easy way to get things going, so that's what we'll do. Optimizations aren't the goal of this tutorial, concepts are. And these same concepts apply to the more optimized methods as well.

When we create our View Matrix we have 3 vectors:Eye (position), LookAt (facing), Up. In a standard FPS your Up vector won't often change. However if you allow complete 3D movement (Descent) or allow leaning (System Shock 2) then it would come in to play. For this lesson we won't be doing any of that, so our Up vector will never change from its initial setting (0,1,0).

The basic issues in handling a First-Person camera are facing and position. Facing is allowing the user to spin around and look at the world around him, while position is calculating the direction and velocity of the camera to find out where we are in 3D space. Our position calculations require that we have our facing sorted out, so we'll do that first.

Facing

Let's have a look at our player (The relevant bits are highlighted):

struct player_struct{

   D3DXVECTOR3 position;   //Current position
   D3DXVECTOR3 up;         //Up vector
   float angle;            //Rotation on the Y-axis (in radians)

} g_player;

As the comments say, angle is our rotation on the Y-axis. Note:In this lesson we only allow the player to look right or left, looking up and down is not supported. That's why we can represent our facing with just a single rotation angle.

There are a lot of different ways you can organize your data. I could have easily chosen to use a facing vector. I could have chosen to use a completely different representation like a quaternion. Using the angle was a choice driven by convenience. It worked well for me.

You may have noticed that I specified that the angle is in radians. Radians are the units used for all of the trig functions (some of which we'll be using shortly) so it just makes sense to store them in the way they'll be used. Some people are hesitant to move away from degrees because degrees are familiar. But they really aren't difficult to use.

Most of the time you deal with angles, you'll be dealing with them relatively anyway. When the player turns right, we'll just increment the angle a wee bit, the fact that it's in radians isn't really an issue. For those rare times when you need absolutes, they're easy to remember. 180 degrees is PI radians. PI is roughly equal to 3.14159 and is represented by π, which is the Greek symbol for "ottoman". 360 degrees is twice as big as 180, and so it's 2π. That's about all there is to that.

Now let's look back at our code to see how we handle the player hitting a turn key.

if(g_key_state[KEY_RIGHT]){
   g_player.angle+=turn_rate;
   if(g_player.angle > pi2){
      g_player.angle-=pi2;
   }
   changed_camera=true;
}else if(g_key_state[KEY_LEFT]){
   g_player.angle-=turn_rate;
   if(g_player.angle < 0){
      g_player.angle+=pi2;
   }
   changed_camera=true;
}

If the player hits the right or left arrows keys we increment/decrement the angle. We also make sure the angle stays within the range 0..2π. If the angle became very large in magnitude (postive or negative), error could be introduced and our calculations would be off.

We set our changed_camera variable to true so we know to rebuild our view matrix. As I mentioned above, this is an expensive operation so we only do it when needed.

Storing our facing as an angle was very convenient here. Doing the same thing with a vector would have been more complex.

Now that we've calculated our new facing we need to update our look-at point. The function we use to build our view matrix (D3DXMatrixLookAtLH) takes vectors as parameters, not rotation angles. That means we have to convert our angle to a vector that it wants, which is a look-at point. Here's that little bit of magic:

look_at.x=sinf(g_player.angle)+g_player.position.x;
look_at.y=g_player.position.y;
look_at.z=cosf(g_player.angle)+g_player.position.z;

Since the y coordinate of our look-at point is the same as our players height, no calculation is required. The x and z coordinated require a little bit of trig. The Sine of the angle gives us our x, and we add the player position to make sure the look_at is ahead of our player. Similarly, the z coordinate comes from the Cosine of our angle. And that's all there is to handling our facing.

Movement

Updating our position isn't much harder than updating our facing. In fact it's very similar. To calculate our look-at point on the x-axis, we found the Sine of the player angle and added it to our position on the a-axis. Similarly, to find our velocity on the x-axis, we multiply the Sine of the angle by our base velocity. This gives us our velocity along the x-axis. If we're moving forward, we add the result to our position. If we're moving backwards, we subtract it. We do the same for the z-axis and we're done.

if(g_key_state[KEY_UP]){
   g_player.position.x+=(velocity) * (sinf(g_player.angle));
   g_player.position.z+=(velocity) * (cosf(g_player.angle));
   changed_camera=true;
}else if(g_key_state[KEY_DOWN]){
   g_player.position.x-=(velocity) * (sinf(g_player.angle));
   g_player.position.z-=(velocity) * (cosf(g_player.angle));
   changed_camera=true;
}

Building Our View

As I mentioned earlier, the up vector never changes in this lesson, so our player's up vector is static. We just pass it in without update. The position vector is calculated every time we move forward or backward. Our look-at point updates whenever we turn. Since we've calculated our new vectors in the above sections, all we have to do is rebuild the view matrix and apply it.

D3DXMatrixLookAtLH(&view_matrix,
                   &g_player.position,
                   &look_at,
                   &g_player.up);

g_engine.SetTransform(D3DTS_VIEW,&view_matrix);

And that is all there is to that. You are now the proud owner of a first-person camera.


Miscellaneous Bits

In the above code when the player turns we add 'turn_rate' to the player's angle. When the player moves forwards we add velocity (modified by facing angle). But we haven't discussed where those values come from.

Time Scale

We process the keys and do our updates every frame. Since different PCs will run at different frame rates that can cause problems. We looked at frame-independant timing in the Dancing Square tutorial, but I'll go over it briefly again. The basic idea is to take the amount of time that has passed since the last frame and use that to scale how quickly things move. This bit of code calculates our time scale.

current_time=timeGetTime();

time_scale=(current_time-g_last_frame_time)/1000.0f;

g_last_frame_time=current_time;

We get the current time from timeGetTime (which returns it's value in milliseconds). Subtracting our last frame time from it gives us the number of milliseconds since our last frame. We then divide this amount by 1000, which gives us the number of seconds since the last frame. If our frame rates are decent this gives us a very small (significantly less than 1.0) number.

Choosing Units of Measurement

Now we can think of our velocities in terms of radians per second (for turning) and meters per second (for movement). It's handy to be able to relate your units of measurement to familiar units. It's also important to choose your measurements before you begin. I didn't, which is why you may have noticed that our player is 1 meter tall. Oops.

I could adjust the view matrix, but then the players head would be brushing the ceiling. I could also edit all the geometry in a wild fit of revisionism. But I have chosen to let my mistake stand as an example for all of you. Besides, I'm lazy and that would take a lot of work.

Velocity

Now we are ready to look at how we use the time scale values.

velocity=4.0f * time_scale;
turn_rate=1.0f * time_scale;

We've set our character's velocity to be 4 meter/second and his turning rate to be 1 radian/second. Multiplying these values by our time scale gives us our velocities over the current time slice. Using these values, our game will play the same on just about any PC. If the frame rate goes too low, then the character will teleport around, and it will be unplayable, but there's little you can do about that. You could try running in a smaller display mode, or turning off graphical features (note: this demo doesn't have any, unless you count walls as a feature).

One Last Tidbit

The controls are simple. Arrow keys turn and move. Hitting the space bar will teleport you back to the center. Hitting the 'F' key toggles the fillmode between solid and wireframe. It's a very handy thing to be able to do.



This tutorial is the first I've done in the new style. I hope you like it and I encourage feedback. Constructive critcism will make the tutorials praiseworthy, and praise will feed my ego. If my ego gets large enough, I plan to claim it as a dependant. I don't know what Revenue Canada will think of that.

Back