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.)
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).
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 ofGame::LoadData
(not looping)."Music.ogg"
starts when the race begins (as in when your start timer hits 0). Save theSoundHandle
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:
- Call
StopSound
on the music’sSoundHandle
, passing in250
forfadeTimeMS
- Play
"FinalLap.wav"
(not looping and no fade time) - Play
"MusicFast.ogg"
(looping with a fade time of 4000). Save this in aSoundHandle
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.