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

Theme: auto

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: Kart and Track

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 at 0.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 and 2500.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:

  1. Is the pedal pressed?
    1. If so, update the acceleration time, calculate the correct acceleration magnitude based on the acceleration time, and then Euler integrate the velocity
    2. Else, just reset the acceleration time and don’t change the velocity
  2. Euler integrate the position
  3. Apply linear drag to the velocity. Remember our linear drag coefficient depends on whether the pedal is pressed
  4. If the vehicle is turning, Euler integrate the angular velocity (negative for turning left, positive for turning right)
  5. Euler integrate the angle
  6. 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

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)
  • Vector3s 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 ints, 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 and false 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, where cellValue 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: Kart at starting position

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.