Track, Vehicles, and Camera
In this part, we’ll set it up so you can drive your kart around the track, add a spring camera, and make sure the height (z-component) of the kart roughly corresponds to the height on the track at a specific location.
Adding the Track
We don’t need to make a new Actor subclass for the track. Instead, in Game::LoadData
, dynamically allocate a new Actor
, set the actor’s rotation to Math::Pi
, and create a MeshComponent
for this actor that uses "Assets/Track.gpmesh"
.
You should now see the track around the kart, even though the kart is not at the starting line:
Vehicle Physics
We’re going to implement the “arcade” style vehicle physics as discussed in lecture, so you’ll want to keep those slides handy.
Create a new subclass of Component
called VehicleMove
. In the VehicleMove
constructor’s initializer list, make sure that you pass in Component(owner, 50)
, which will make sure that the component is constructed with a lower than default update order. This means VehicleMove
will get updated first out of all the components.
In the override of Update
, you’re going to implement the arcade vehicle physics equations. However, before you can do this, you need to add several member variables:
- A
bool
to track whether the acceleration pedal’s been pressed (default to false) - An
enum
to track whether the vehicle is trying to turn left, turn right, or not turn at all. Default to not turning - A
Vector3
for velocity - A
float
for angular velocity - A
float
to track how long you’ve been accelerating (starts at0.0f
)
You also want member variables for these, which are tunable parameters you can change to change the car’s driving behavior:
- A minimum linear acceleration magnitude and maximum linear acceleration magnitude (default to
1000.0f
and2500.0f
) - An acceleration ramp time (default to
2.0f
) - An angular acceleration (default to
5.0f * Math::Pi
) - A linear drag coefficient (default to
0.9f
) - A linear drag coefficient when the acceleration pedal is not pressed (default to
0.975f
) - An angular drag coefficient (default to
0.9f
)
I came up with these numbers via trial and error, and I thought the car is drivable after some practice (at least on my desktop and Mac). But for some reason, the car is undrivable on my Surface Pro (I blame the keyboard). So, if you find that the karts are difficult to drive with these numbers, you may need to tweak them slightly them. Mostly, reducing the linear acceleration should make it a lot easier to drive, since you won’t be moving as quickly.
Now it’s time to implement Update
. Again, keep the lecture slides handy. The basic structure is:
- Is the pedal pressed?
- If so, update the acceleration time, calculate the correct acceleration magnitude based on the acceleration time, and then Euler integrate the velocity
- Else, just reset the acceleration time and don’t change the velocity
- Euler integrate the position
- Apply linear drag to the velocity. Remember our linear drag coefficient depends on whether the pedal is pressed
- If the vehicle is turning, Euler integrate the angular velocity (negative for turning left, positive for turning right)
- Euler integrate the angle
- Apply angular drag
Lastly, you need to add setter functions for the pedal and the turning enum, since that will be the controls that both the player and enemy kart will rely on to actually move the car.
Now change the PlayerMove
class so it inherits from VehicleMove
instead of MoveComponent
.
Next, in PlayerMove::ProcessInput
, setup the following controls:
- For the pedal: the
W
key sets the pedal to true (else the pedal should be false) - For turning:
- Pressing the
A
key should set the turn enum to left - Else pressing the
D
key should set the turn enum to right - If none of these keys are pressed (or the player tries to press both directions at the same time), then the turn enum should be none
- Pressing the
Finally, at the beginning of PlayerMove::Update
, you have to call the parent class Update
function with the following line of code:
VehicleMove::Update(deltaTime);
You should now be able to drive your kart around. Note that the kart will sometimes disappear under the track, since we aren’t accounting for the track height yet. It will look like this:
Spring Camera
Now make a new Component
subclass called CameraComponent
. For now, just override the Update
function, and move the camera code from PlayerMove::Update
into CameraComponent::Update
. Then in the Player
constructor, add a CameraComponent
to the Player
.
Make sure your camera still works as before.
Now we’ll implement the spring camera, also as discussed in slides. You’ll need these member variables:
- Horizontal distance (defaults to
60.0f
) - Target offset (defaults to
50.0f
) - Spring constant (defaults to
256.0f
) - Dampening constant (calculated via equation based on spring constant)
Vector3
s for the camera position and camera velocity
Then you should make a member function to calculate the “ideal” position and returns a Vector3
. This is using just the normal follow camera equations except instead of there being a VDist
, we’re just going to go back to hard-coding the z-component of the camera, but this the camera.z
should be 70.0f
.
Then in Update
, implement the spring camera updating logic we discussed in lecture.
Your camera should now be springy. However, you may notice that when you start the game, the camera starts very close and takes a moment to zoom out. This is because it takes a moment for the spring camera to “catch up” to the ideal position. Except for that initial catch-up, your spring camera should look like this:
To fix the catch-up problem, add a SnapToIdeal
function in CameraComponent
. This should just calculate the ideal position of the camera and the target position. It will then immediately set the view matrix to this ideal camera. Then in the Player
constructor, call SnapToIdeal
after you create the CameraComponent
. This will ensure that the camera doesn’t have the catch-up lag at the beginning.
Confirm your camera doesn’t have the catch-up lag anymore.
Height Map
Now we’ll make it so the kart’s height is based on the height of the track, using the height map technique we discussed in lecture. This is a little bit complex, so for this section we’re going give you a bit more detail than usual (especially on how to calculate the correct coordinates).
Declare a new class called HeightMap
. This class will not inherit from any other class. The only member data it needs is a vector
of vector
of int
s, since you’ll need to store a 2D grid of integer values as you did in the TileMapBGComponent
in the Zelda lab.
The constructor of HeightMap
should load in the "Assets/HeightMap/HeightMap.csv"
file into your 2D grid of values. We’ve given you the same CSVHelper.h
which you can use to split lines in the CSV.
Now in Game
, add a pointer to a HeightMap
in the member data, and then in LoadData
construct an instance of HeightMap
, saved in the pointer.
Verify your CSV file loads as you’d expect it to in the HeightMap
constructor.
Now we’re going to add some functions to height map.
IsCellOnTrack
Make a private function called IsCellOnTrack
that:
- Takes in a row and a column for the cell
- Returns
true
if that cell is on the track andfalse
if otherwise. It’s not on the track if it’s out of bounds of your 2D grid, and it’s also not on the track if the value stored for that cell is-1
(since that means there was no height assigned for that cell).
GetHeightFromCell
Make a private function GetHeightFromCell
that:
- Takes in a row and a column for the cell (you may assume that the given row/column is on the track, this is why we made it private!)
- Returns a
float
value corresponding to the height of the cell, according to this equation:-40.0f + cellValue * 5.0f
, wherecellValue
is the integral value you get from looking up the cell in the 2D grid
Constants
You should declare the following constants (which I figured out through trial and error):
-
The width/height of each cell is
CELL_SIZE = 40.55f
-
The world space x-coordinate of the (0,0) cell is
GRID_TOP = 1280.0f
-
The world space y-coordinate of the (0,0) cell is
GRID_LEFT = -1641.0f
CellToWorld
Make a public function CellToWorld
that:
- Takes in a row and a column for a cell
- If the cell is not on the track, it returns
Vector3::Zero
- Otherwise, it returns the
Vector3
(x, y, z) position corresponding to that cell
Given the constants, here’s how you get (x, y, z):
-
x = GRID_TOP – CELL_SIZE * row
-
y = GRID_LEFT + CELL_SIZE * col
-
z is the value you get from GetHeightFromCell()
Notice how in these equations the x corresponds to the row and the y corresponds to the column. This is because the rows in the height map data correlate to the x-axis in world space. In other words, the height map data is rotated by 90 degrees (which maybe in retrospect wasn’t the best idea, but it helps practice coordinate space conversions)!
Using CellToWorld
In the constructor of PlayerMove
, set the position of mOwner
to the result of CellToWorld(39, 58)
. You should be able to access your HeightMap*
via the Game
.
When the game starts, you should now see the kart at the starting position like this:
WorldToCell
Add another public function WorldToCell
that:
- Takes in a
const Vector2& pos
world space coordinate - Returns the row/col of the cell corresponding to the x, y coordinate (since you can only return one value, you can either return a
Vector2
or set row/col via reference parameters).
The equations to calculate the correct cell is a little bit complicated, because you want to find the center of the cell closest to the world position:
row = (Math::Abs(pos.x – GRID_TOP) + CELL_SIZE / 2.0f) / CELL_SIZE
col = (Math::Abs(pos.y – GRID_LEFT) + CELL_SIZE / 2.0f) / CELL_SIZE
For the purposes of WorldToCell
, you don’t need to worry whether that cell is on the grid.
IsOnTrack
Make a public function IsOnTrack
that:
- Takes in a
const Vector2& pos
world space coordinate - Returns
true
if the cell of that (x, y) coordinate is on the track, false if not
To implement this, simply call WorldToCell
to convert it to a cell, then call your IsCellOnTrack
function.
GetHeight
Make a public function GetHeight
that:
- Takes in a
const Vector2& pos
world space coordinate - Returns the height of the cell at that coordinate, or -1000 if that cell is not on the track
This involves using the functions you already made. Use IsOnTrack
to see if (x, y) is on the track, and if it is use WorldToCell
to get the row/col and then GetHeightFromCell
to get the return value.
Using GetHeight in VehicleMove
Now in VehicleMove::Update
, after you update the position using velocity, check if your (x, y) coordinate is on the track, and if so, set the z-position to the result of GetHeight
.
Now when you drive around the track, you should notice the height the car is at changes, as about halfway through this video:
Right now, this is really jerky and would probably cause motion sickness if you played it for more than a little bit.
To fix this doesn’t require anything crazy. Rather than just hard setting the z-position to the result of GetHeight()
, we’re simply going to Lerp it between the old pos.z
and the result of GetHeight
, with a Lerp
factor of 0.1f
. The result of this is that it will smoothly change the pos.z
over several frames, rather instantly changing it in a jarring fashion.
You should now have the kart height change much more smoothly, as in this video:
Once you’ve pushed this code, you’re ready to move on to part 2.