Skip to main content Link Search Menu Expand Document (external link)

Theme: auto

3D Setup and Level

First, we must make some changes to our existing code to get it to support 3D properly. Then we can add in 3D objects to our game.

Preparing for 3D

First, make these changes in Actor:

  • Change mPosition to a Vector3 type (since we’re 3D now). This means you will have to also change the declarations of GetPosition/SetPosition, and the constructor so the initializer list sets mPosition to Vector3::Zero
  • Change the mScale variable to a Vector3 as well, so we can allow for non-uniform scale. You’ll also need to:
    • Change GetScale to instead return a const Vector3&
    • Change the existing SetScale so it sets all components of mScale to the float scale
    • Add a second version of SetScale that takes in a const Vector3 and sets mScale to that
    • In the constructor, make sure you still initialize mScale to 1.0f (which will set all components to 1.0f)
  • Change GetForward to return a Vector3 instead of a Vector2. Don’t negate the y-component (the result of the sin) anymore and set the z-component of the returned vector to 0.0f

Now MoveComponent will not compile. To fix this, change any references to Vector2 to instead use Vector3.

If your MoveComponent implementation separately updated the x and y components for the position update, you should also fix it so you use vector operations. Otherwise, the z-component will never update.

Then you back in Actor:

  • Add a Matrix4 member variable called mWorldTransform
  • Add the following public member function:
const Matrix4& GetWorldTransform() const { return mWorldTransform; }

We include a basic working 3D CollisionComponent except it does not have a working GetMinOverlap implementation. This AABB works like the one you wrote, except there are three dimensions now (width, height, and depth), and GetMin()/GetMax() return Vector3s now. You won’t need to implement the 3D GetMinOverlap function until Lab 9.

Finally, in HUD.cpp, you need to comment out the line that’s includes Player.h as you don’t have that file yet. Then, in HUD.h, change line 22 to:

SoundHandle mBarrelRollSnd;

Your code should now compile! However, running it will not even bring up a window.

Next, you need to add a void CalcWorldTransform() function to the public section of Actor (it needs to be public because we will in some instances, need to call it outside of Actor). Implement this function in Actor.cpp:

  • You should use Matrix4::CreateScale and the other correct static Matrix4 functions to create the different matrices for Scale, Rotation Z, and Translation
  • The parameters for the different Matrix4 functions will be mScale, mRotation, or mPosition, depending on the matrix
  • Remember the equation to create the final world transform matrix is: World = Scale * Rotation * Translation
  • Save the result matrix in mWorldTransform

Then, at the end of Actor::Update, call CalcWorldTransform. This should happen every frame regardless of whether or not the Actor is active.

Your code should still compile, though you still won’t see anything if you run.

In Game, there is a new pointer member variable called mRenderer which will point to the simple 3D renderer we give you for the 3D labs. This class also handles setting up the SDL window, but we’re not using it yet which is why there is no window yet!

In Game.h, add the following public member variables:

const float WINDOW_WIDTH = 1024.0f;
const float WINDOW_HEIGHT = 768.0f;

In Game::Initialize, where there’s the TODO comment:

  • Dynamically allocate a Renderer object (pass in this) and save it in the mRenderer member variable
  • Call mRenderer->Initialize, passing in the width/height of the screen (WINDOW_WIDTH, WINDOW_HEIGHT). If this function returns false, output an SDL_Log saying that the renderer failed to initialize, and then return false

If the 1024x768 window size ends up too big for your computer, you can use smaller numbers as long as you maintain the same 4:3 aspect ratio.

In Game::GenerateOutput, add a call to mRenderer->Draw()

In Game::Shutdown, where there’s a TODO:

  • Call mRenderer->Shutdown()
  • Delete mRenderer

Now when you run, you should see a window with a black background:

Blank window

Player and Static Camera

Create a new subclass of Actor called Player with:

  • SetScale to 2.0f

  • Create a MeshComponent to draw a 3D model To set the 3D mesh, there’s a SetMesh function. Renderer has a GetMesh function that loads the specified mesh. So, to use the "Assets/Arwing.gpmesh" mesh as you want to do here, you’d say:

    mc->SetMesh(mGame->GetRenderer()->GetMesh("Assets/Arwing.gpmesh"));
    

    (Assuming mc is a pointer to your MeshComponent, and you will need to include both Renderer.h and Game.h)

  • Create a CollisionComponent, and SetSize to (40.0f, 25.0f, 15.0f) and a getter to get its pointer

Now in Game::LoadData, create a Player, save it in a corresponding Player* member variable, and add a getter.

Still in LoadData, you will need to initialize the projection and view matrices. For the numbers given here, you can just hardcode them as you’ll only use them once (so you don’t need to make constants for all of them.

First create a local Matrix4 for the projection matrix, using Matrix4::CreatePerspectiveFOV, with the following parameters:

  • 1.22f to specify a field of view of 1.22 radians (about 70°)
  • WINDOW_WIDTH for the view width
  • WINDOW_HEIGHT for the view height
  • 10.0f for the near plane distance
  • 10000.0f for the far plane distance (that’s ten thousand, not one thousand)

Once you create the projection matrix, call mRenderer->SetProjectionMatrix to tell the renderer to use it.

Next, create a local Matrix4 for the view matrix, using Matrix4::CreateLookAt:

  • The camera “eye” position is (-300, 0, 0)
  • The target position the camera is looking at is (20, 0, 0)
  • The up direction is Vector3::UnitZ

If you’re confused by these numbers, remember that the 3D coordinate system we’re using is:

  • +x is forward
  • +y is to the right
  • +z is up

So, this eye position means we want the camera slightly behind the origin (which is where the ship will be, for now).

Once you create the view matrix, call mRenderer->SetViewMatrix to tell the renderer to use it.

You should now see the player’s ship from behind, like this: Camera setup

Moving the Player Forward

Create a new subclass of MoveComponent called PlayerMove, and create one for the Player.

In PlayerMove, add a Vector3 member variable for the velocity. Set it up so the player automatically moves forward (along the x axis) at a rate of 400 units/second. This movement should happen regardless of any key input.

Now you should see the ship get progressively further and further away. Far away

Moving the Player Left/Right/Up/Down

For movement, instead of using the arrow keys, we will use the W, A, S, and D keys as many 3D PC games do.

Make it so that if you hold down W, you move up (+z) at a rate of 300 units/second and if you hold down S, you move down at a rate of 300 units/second. If you hold both W and S, you shouldn’t move up or down.

Similarly, make it so that if you hold D you move to the right (+y) at a rate of 300 units/second, and if you hold A you move to the left at a rate of 300 units/second. If you hold both A and D, you shouldn’t move left or right.

Verify that the ship still flys away but you can now move it up, down, left, and/or right using WASD.

You also want to the y/z position of the ship to be within these inclusive ranges (you could use Math::Clamp):

  • y is [-180, 180]
  • z is [-225, 225]

You should notice that if you hold down one of the WASD keys, the ship will eventually stop moving in that direction.

Basic Follow Camera

To make the camera’s position relative to the ship always the same, we will use the basic follow camera equations as discussed in lecture with the following distance values:

  • HDist = 300
  • VDist = 0
  • TargetDist = 20

Because the VDist is 0, that means you can entirely ignore that component of the follow camera equation.

In addition to this, we actually don’t want the camera’s eye z value to ever change for this game, as it gives a cooler effect. So, after calculating the eye position using the equations, you should just force the eye.z to be 0.0f.

At the very end of PlayerMove::Update, calculate the eye and target positions. Then, call Matrix4::CreateLookAt, passing in these two as parameters well as Vector3::UnitZ for the up vector. Once you create the matrix, call the SetViewMatrix on the renderer.

Your camera should no longer fly off away, though you won’t be able to tell it’s moving because it no longer has a frame of reference. However, if you use W or S you should see the ship rotate a bit as in this video:

Tunnel Blocks

Now we’ll add big blocks on the left, right, up, and down sides of the tunnel to give us a missing frame of reference. Create a new subclass of Actor called SideBlock:

  • For the constructor, take in an additional size_t parameter for the texture index
  • Set the SideBlock’s scale to 500.0f (to make it big)
  • Create a MeshComponent that uses the "Assets/Cube.gpmesh" mesh
  • Call SetTextureIndex on the MeshComponent, passing in the index from the constructor parameter (meshes can have several different textures you can pick from)

Now create these four SideBlocks for testing purposes in Game::LoadData:

  • One at (0, 500, 0) with a rotation of Math::Pi and with texture index 0
  • One at (0, -500, 0) with texture index 0
  • One at (0, 0, -500) with texture index 5
  • One at (0, 0, 500) with texture index 6

You should initially see these four blocks around the ship, but they will very quickly move out of view: Tunnel block test

Now remove the test SideBlocks you created in Game::LoadData.

Since the game could go on for a while, we want to create side blocks as the player moves through the level. Furthermore, once the side blocks are out of view, we can destroy them so that we don’t eventually run out of memory. So, instead of just creating a fixed number of side blocks at the start, we must create more blocks as the player advances through the level.

The y and z position of these side blocks is always fixed based on whether it’s the block to the left, right, up, or down. However, the x positions will increment by 500. For example, the first set of four blocks is at x = 0, the second set at x = 500, the third set at x = 1000, and so on.

What you want to do is on every frame, make sure you’ve spawned SideBlocks up to 4000 units in the x direction in front of the player. For example, since the player starts at x = 0, at the start of the game you need to make sure that you have side blocks at 0, 500, 1000, 1500, 2000, 2500, 3000, and 3500 (use a loop). After the player moves forward even 1 unit, you’ll need to spawn the blocks at x=4000. Then when the player reaches an x >= 500, you’d need to spawn the blocks at x=4500, and so on.

We recommend you do this dynamic spawning of the blocks in PlayerMove, as that’s where you update the player position.

For now, always use texture index 0 for the left/right blocks, texture index 5 for the down block, and texture index 6 for the up block. You will also always need to rotate the right block by Math::Pi in order to view the texture in the correct orientation.

You should now see an endless tunnel of side blocks which looks like this:

With the code as is, eventually the game would run out of memory because we aren’t deleting any side blocks. Since the ship always moves forward, we can destroy side blocks that are more than 2000 units behind the player. You can check for this case in SideBlock::OnUpdate and set it to ActorState::Destroy as needed.

Use a breakpoint to verify that your side blocks get destroyed when you expect them to. Your game should appear the same as it before.

It’s a little boring to have all the blocks be the same texture. Instead, change it so the blocks spawn with the following texture index patterns:

  • For the left/right sides, use the texture index pattern 0, 1, 2, 0 (this does mean two 0s will appear next to each other)
  • For the top side, use the texture index pattern: 6, 7
  • For the bottom side, you can just always use texture index 5

You should now see variety in the tunnel and it’ll look like this:

Once you’ve pushed this code, you’re ready to move on to part 2.