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 aVector3
type (since we’re 3D now). This means you will have to also change the declarations ofGetPosition
/SetPosition
, and the constructor so the initializer list setsmPosition
toVector3::Zero
- Change the
mScale
variable to aVector3
as well, so we can allow for non-uniform scale. You’ll also need to:- Change
GetScale
to instead return aconst Vector3&
- Change the existing
SetScale
so it sets all components ofmScale
to thefloat scale
- Add a second version of
SetScale
that takes in aconst Vector3
and setsmScale
to that - In the constructor, make sure you still initialize
mScale
to1.0f
(which will set all components to1.0f
)
- Change
- Change
GetForward
to return aVector3
instead of aVector2
. Don’t negate the y-component (the result of the sin) anymore and set the z-component of the returned vector to0.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 calledmWorldTransform
- 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 Vector3
s 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 correctstatic
Matrix4
functions to create the different matrices for Scale, Rotation Z, and Translation - The parameters for the different
Matrix4
functions will bemScale
,mRotation
, ormPosition
, 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 inthis
) and save it in themRenderer
member variable - Call
mRenderer->Initialize
, passing in the width/height of the screen(WINDOW_WIDTH, WINDOW_HEIGHT)
. If this function returnsfalse
, output anSDL_Log
saying that the renderer failed to initialize, and then returnfalse
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:
Player and Static Camera
Create a new subclass of Actor
called Player
with:
-
SetScale
to2.0f
-
Create a
MeshComponent
to draw a 3D model To set the 3D mesh, there’s aSetMesh
function.Renderer
has aGetMesh
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 yourMeshComponent
, and you will need to include both Renderer.h and Game.h) -
Create a
CollisionComponent
, andSetSize
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 widthWINDOW_HEIGHT
for the view height10.0f
for the near plane distance10000.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:
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.
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
= 300VDist
= 0TargetDist
= 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 to500.0f
(to make it big) - Create a
MeshComponent
that uses the"Assets/Cube.gpmesh"
mesh - Call
SetTextureIndex
on theMeshComponent
, passing in the index from the constructor parameter (meshes can have several different textures you can pick from)
Now create these four SideBlock
s 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:
Now remove the test SideBlock
s 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.