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

Theme: auto

Part 3: Places and Finishing Touches

We’ll now make it so the game tracks how many laps you’ve made, what place you’re in, and then display that to the player. We’ll also add some music and sound effects.

Laps

Look at "Assets/HeightMap/Checkpoints.csv". This file contains different checkpoints around the track. To complete a lap, the player needs to visit all checkpoints in the correct order.

Instead of having just a single point for the checkpoint, each checkpoint is defined by min/max CellX and min/max CellY. Remember that CellX corresponds to the row in our case, and CellY corresponds to the column.

The first checkpoint in the file is:

Checkpoint,35,35,57,61

This means that the minimum cell for the checkpoint is (35, 57) and the maximum cell for the checkpoint is (35, 61). If you reach any of these specific cells, or any of the cells in between like (35, 58), (35, 59), (35, 60), then you’ve successfully reached that checkpoint.

Keep in mind the checkpoints are integers, but your WorldToCell uses floats. So in the first example, if your cell were (35.1, 58) and you left the numbers as floats, 35.1 is not within the bounds of the checkpoint. To fix this, you have to make sure to convert your cell x/y values to integers.

Now in VehicleMove, add a std::vector to the member data that you’ll use to store the four different values for each checkpoint. In the constructor, load in the Checkpoints.csv file into this member data.

You also need two integers representing the current lap you’re on (initialize it to 0) and the index of the last checkpoint you hit (initialize to -1). The reason we initialize it this way is so that when you hit the initial checkpoint (which is right under the arch at the beginning), it will recognize you’re on lap 1 and checkpoint 0.

At the end of VehicleMove::Update, you need to figure out if you made it to the “next” checkpoint. If you did, you should increment the last checkpoint index. Don’t forget to account for the checkpoint indices wrapping around. If you’ve completed a lap (meaning you just hit checkpoint 0), you should increment the lap count.

Put a breakpoint in your checkpoint detection code. You should notice that right when the enemy passes under the arch, your code should recognize it’s now at checkpoint 0, lap 1. Then right before the enemy takes the first turn, it should be checkpoint 1, lap 1. Let the enemy advance to all the checkpoints and verify that after it passes under the arch again, your code sets it to checkpoint 0, lap 2.

Displaying Laps

Since we haven’t really talked about implementing a UI yet (we will later in the semester), we’ve given you a mostly-implemented PlayerUI that will display relevant things like when you’re on a lap, whether you’re winning or losing, etc.

PlayerUI is just like any other component, so create a PlayerUI in the Player constructor.

Now when you run, you should notice that in the top right corner it shows “2nd” in blue.

We want the UI to display the lap number each time the player starts a new lap. To do this, add a virtual function to VehicleMove in the header:

virtual void OnLapChange(int newLap) { }

Call this function in VehicleMove::Update when you increment the lap, passing in the new lap number.

Then in PlayerMove, we can implement an override of OnLapChange. For now, PlayerMove::OnLapChange should simply forward the lap number to the PlayerUI’s OnLapChange function.

Now as soon as you drive through the arch you should see the UI say “Lap 1/2”. Verify that after you complete a lap it says, “Final Lap”. (Note, because of the way the checkpoints work, if you drive way off the track it won’t detect you hitting a specific checkpoint, so you’d have to backtrack in that case.) Lap numbers

Place Tracking

For the UI to correctly show whether the player is currently in first or second place, you need to implement the PlayerUI::IsPlayerInFirst. Right now, it just always returns false, so the UI thinks you’re in second place.

It’s pretty easy to figure out whether the player is in first or second if the player’s lap number is different from the enemy’s, or if the player’s checkpoint number is different than the enemy’s.

But what happens if the player and enemy are on the same lap and same checkpoint? In this case, the only way to figure out who’s further is to determine both vehicle’s distance to the next checkpoint and assume that whoever’s closer is ahead. This won’t be perfect, but it should mostly work. To help with this last part, I’d recommend adding a function to VehicleMove that tells you the vehicle’s distance to the next checkpoint.

Verify that when you get ahead of the enemy kart, it shows you in first place! (If you’re having a hard time beating the enemy, maybe slow down your enemy’s kart). In 1st place!

Start Timer

Rather than having the race instantly start as soon as you load up the game, we want to give the player several seconds to get ready for the race at hand (and we’ll also play cool music/sound effects!)

First, add a float member to game initialized to 8.5f. Then in Game::LoadData, set both the Player and Enemy to ActorState::Paused initially.

Then, update this timer at the end of Game::Update like normal. When the timer initially reaches <= 0.0f, you should set both the Enemy and Player to ActorState::Active. Make sure this code only executes the first time the timer is <= 0.0f, or it will mess stuff up later.

Then in PlayerUI.h, change the default value of mGoDisplayTimer to 2.0f.

Now when you start up the game, you should sit on the starting line with “Get Ready” for 8.5 seconds before it’ll say “Go!!” and then you can start driving.

If you notice that the camera is in the wrong spot initially when the game starts, make sure to remove the code that sets the camera matrix in LoadData, and confirm that your CameraComponent sets the camera matrix in SnapToIdeal().

End of Race

Now in PlayerMove::OnLapChange, if you hit lap #3 rather than calling OnLapChange on the UI, you just need to decide whether the player won or the enemy won. (You won if your lap number is higher than the enemy’s, since that means you completed your last lap before the enemy did.)

If the player won, call SetRaceState(PlayerUI::Won) on the PlayerUI, otherwise do the same but with PlayerUI::Lost. In either case, you want to set both the player and the enemy to ActorState::Paused so they stop driving.

Music/Sound

Play the sounds as follows:

  • "RaceStart.wav" is just played at the end of Game::LoadData (not looping).
  • "Music.ogg" starts when the race begins (as in when your start timer hits 0). Save the SoundHandle in a member variable in Game

Additions to AudioSystem

For the final lap logic, we want to add support for sounds to fade out instead of stopping immediately, and conversely fading in rather than starting at full volume. To do this, requires changing the declaration of both PlaySound and StopSound.

Change the PlaySound declaration to add an optional fadeTimeMS parameter which defaults to 0:

SoundHandle PlaySound(const std::string& soundName, bool looping = false, int fadeTimeMS = 0);

Similarly, change the declaration of StopSound to also add an optional fadeTimeMS parameter which defaults to 0:

void StopSound(SoundHandle sound, int fadeTimeMS = 0);

You will likewise need to change the implementations in AudioSystem.cpp to add these new parameters.

For PlaySound, the only change you need to make is that where you previously called Mix_PlayChannel, you need to first check if fadeTimeMS > 0. If there is a fade time specified, then instead of calling Mix_PlayChannel, call Mix_FadeInChannel (otherwise, you should still call Mix_PlayChannel as before. You pass in all the same parameters are you would to Mix_PlayChannel, but then also pass in the fadeTimeMS as a final parameter.

For StopSound, if fadeTimeMS > 0, rather than calling Mix_HaltChannel you should call Mix_FadeOutChannel. Importantly, if you fade out, DO NOT reset the mChannels index or erase the SoundHandle from the handle map. Instead, you will rely on the fact that when the volume hits 0, Update will detect the sound is no longer playing and will then remove the SoundHandle at that point.

Using Fade In/Out for Final Lap and Victory

When the final lap begins (meaning the player’s lap changes to 2), you want to:

  1. Call StopSound on the music’s SoundHandle, passing in 250 for fadeTimeMS
  2. Play "FinalLap.wav" (not looping and no fade time)
  3. Play "MusicFast.ogg" (looping with a fade time of 4000). Save this in a SoundHandle member variable (you can just reuse the variable you used for the original music).

At the end of the race, call StopSound on the music handle with a fade out time of 250ms. Then, play (no looping or fade):

  • "Won.wav" if the player won
  • "Lost.wav" if the player lost

Your game should now look like the final video:

Once you’ve pushed your code, you should review the grading specifications to confirm you’ve satisfied them.