A lot of the fun in games is trying to increase your score. Part of good game design is to make the game challenging to play, but not so challenging that the player cannot score or improve.
Most players also get better at a game as they play, so if the game difficulty does not increase, the player will eventually get bored because the player will no longer be challenged.
We will start by simply displaying the score on the screen so that the player can see how well they are doing. Then we will discuss techniques that are used to continually increase the difficulty of the game, thus steadily increasing the challenge.
We already learned how to display text on the screen when we were creating the credits screen. Now, we will use the same techniques to display the score.
If you recall, we already have a mechanism to keep track of the score. Every sprite has a value property. For pickups, we assign a positive value so that the player gains points for each pickup. For enemies, we assign a negative value so that the player loses points whenever they collide with an enemy. We store the current score in the value property of the player.
Add the following code to RoboRacer2D.cpp
to create the DrawScore
function:
void DrawScore() { char score[50]; sprintf_s(score, 50, "Score: %i", player->GetValue()); DrawText(score, 350.0f, 25.0f, 0.0f, 0.0f, 1.0f); }
This code works just like the DrawCredits
function that we created earlier. First, we create a character string that holds the current score and a caption, then we use DrawText
to render the text.
We also need to wire this into the main game. Modify the GS_Running
case of the m_gameState
switch in the Render
function with the highlighted line:
case GameState::GS_Running: case GameState::GS_Paused: { background->Render(); robot_left->Render(); robot_right->Render(); robot_left_strip->Render(); robot_right_strip->Render(); pauseButton->Render(); resumeButton->Render(); pickup->Render(); enemy->Render(); DrawScore(); } break;
The score will display both when the game is running and when the game is paused.
In order to add progression to the game, we need to have certain thresholds established. For our game, we will set three thresholds:
For each level that the player successfully completes, we will make things a little more difficult. There are many ways that we could increase the difficulty of each level:
To keep things simple, we will only do one of these. We will increase the spawn time threshold for pickups by .25 seconds for each level. With pickups spawning less often, the player will eventually receive too few pickups, and the game will end.
Let's set up the code for level progression. We will start by defining a timer to keep track of how much time has passed. Add the following declarations to RoboRacer2D.cpp
:
float levelTimer; float levelMaxTime; float pickupSpawnAdjustment; int pickupsReceived; int pickupsThreshold; int enemiesHit;
We will initialize the variables in the StartGame
function:
levelTimer = 0.0f; levelMaxTime = 30.0f; pickupSpawnAdjustment = 0.25f; pickupsReceived = 0; pickupsThreshold = 5; enemiesHit =0;
We are setting up a timer that will run for 120 seconds, or two minutes. At the end of two minutes the level will end and the spawn time for pickups will be incremented by .25 seconds. We will also check to see whether the player has received five pickups. If not, the game will be over.
To handle the logic for the level progression, let's add a new function called NextLevel
by adding the following code:
void NextLevel() { if (pickupsReceived < pickupsThreshold) { m_gameState = GameState::GS_GameOver; } else { pickupSpawnThreshold += pickupSpawnAdjustment; levelTimer = 0.0f; m_gameState = GameState::GS_NextLevel; } }
As stated previously, we check to see whether the number of pickups that the robot has is less than the pickup threshold. If so, we change the game state to GS_GameOver
. Otherwise, we reset the level timer, reset the pickups received counter, increment the pickup spawn timer, and set the game state back to GS_Running
.
We still need to add some code to update the level timer and check to see whether the level is over. Add the following code to the GS_Running
case in the Update
function:
levelTimer += p_deltaTime; if (levelTimer > levelMaxTime) { NextLevel(); }
This code updates the level timer. If the timer exceeds our threshold, then call NextLevel
to see what happens next.
Finally, we need to add two lines of code to CheckCollisions
to count the number of pickups received by the player. Add the following highlighted line of code to CheckCollisions
:
if (player->IntersectsCircle(pickup)) { pickup->IsVisible(false); pickup->IsActive(false); player->SetValue(player->GetValue() + pickup->GetValue()); pickupSpawnTimer = 0.0f; pickupsReceived++; } if (player->IntersectsRect(enemy)) { enemy->IsVisible(false); enemy->IsActive(false); player->SetValue(player->GetValue() + enemy->GetValue()); enemySpawnTimer = 0.0f; enemiesHit++; }
It would be nice for the player to be able to see how they did between each level. Let's add a function to display the player stats:
void DrawStats() { char pickupsStat[50]; char enemiesStat[50]; char score[50]; sprintf_s(pickupsStat, 50, "Enemies Hit: %i", enemiesHit); sprintf_s(enemiesStat, 50, "Pickups: %i", pickupsReceived); sprintf_s(score, 50, "Score: %i", player->GetValue()); DrawText(enemiesStat, 350.0f, 270.0f, 0.0f, 0.0f, 1.0f); DrawText(pickupsStat, 350.0f, 320.0f, 0.0f, 0.0f, 1.0f); DrawText(score, 350.0f, 370.0f, 0.0f, 0.0f, 1.0f); }
Now that we have the logic in place to detect the end of the level, it is time to implement our next level screen. By now, the process should be second nature, so let's try an abbreviated approach:
Sprite* nextLevelScreen;
LoadTextures
:nextLevelScreen = new Sprite(1); nextLevelScreen->SetFrameSize(800.0f, 600.0f); nextLevelScreen->SetNumberOfFrames(1); nextLevelScreen->AddTexture("resources/level.png", false); nextLevelScreen->IsActive(true); nextLevelScreen->IsVisible(true);
GS_NextLevel
case in the Update
function:case GameState::GS_NextLevel: { nextLevelScreen->Update(p_deltaTime); continueButton->IsActive(true); continueButton->Update(p_deltaTime); inputManager->Update(p_deltaTime); ProcessInput(p_deltaTime); break; }
GS_NextLevel
case in the Render
function to look like the following code::case GameState::GS_NextLevel: { nextLevelScreen->Render(); DrawStats(); continueButton->Render(); } break;
Now, we need to add a button that allows the player to continue the game. Again, you have done this so many times, so we will use a shorthand approach:
Sprite* continueButton;
LoadTextures
:continueButton = new Sprite(1); continueButton->SetFrameSize(75.0f, 38.0f); continueButton->SetNumberOfFrames(1); continueButton->SetPosition(390.0f, 400.0f); continueButton->AddTexture("resources/continueButton.png"); continueButton->IsVisible(true); continueButton->IsActive(false); inputManager->AddUiElement(continueButton);
Update
:case GameState::GS_NextLevel: { nextLevelScreen->Update(p_deltaTime); continueButton->IsActive(true); continueButton->Update(p_deltaTime); inputManager->Update(p_deltaTime); ProcessInput(p_deltaTime); } break;
Render
: case GameState::GS_NextLevel:
{
nextLevelScreen->Render();
DrawStats();
continueButton->Render();
}
break;
ProcessInput
:if (continueButton->IsClicked()) { continueButton->IsClicked(false); continueButton->IsActive(false); m_gameState = GameState::GS_Running; pickupsReceived = 0; enemiesHit = 0; }
Clicking the continue button simply changes the game state back to GS_Running
. The level calculations have already occurred when NextLevel
was called.
3.149.25.60