We are now ready to define our user interface, which will consist of 2D screens, text, and buttons. These will all work exactly as they did in RoboRacer2D. Look at the tip in the Getting ready for a splash section earlier in this chapter for a reminder of how to include prebuilt 2D resources in your project.
The 2D text system is built by first creating a font framework, then creating functions to display text on the screen. Open RoboRacer2D.cpp
and copy the following functions. Then paste them into SpaceRacer3D.cpp
:
BuildFont
KillFont
DrawText
We are going to add some new variables to handle the data that we want to display. Add the following lines of code to the global variables section of SpaceRacer3D.cpp
:
int score; int speed; int missionTime; int asteroidsHit; int maximumSpeed;
These variables will hold the stats and scoring used by the game:
score
: This is the current game scorespeed
: This is the current speed of the shipmissionTime
: This is the number of seconds that have elapsed since starting the missionasteroidsHit
: This is the number of asteroids hit by the playermaximumSpeed
: This is the maximum speed obtained by the player
Score
, speed
, and missionTime
will all be displayed on the heads-up-display (HUD) while the player is piloting the ship. Score
, asteroidsHit
, missionTime
, and maximumSpeed
will be displayed as stats at the end of the game.
Let's go to StartGame
and initialize these variables:
score = 0; speed = 1.0f; maximumSpeed = 0; asteroidsHit = 0; missionTime = 0;
Now, let's create the functions to render these items on the screen. Add the following two functions to the game somewhere above the Render2D
function:
void DrawUi() { float startY = screenHeight - 50.0f; float x1 = 50.0f; float x2 = screenWidth / 2.0f - 50.0f; float x3 = screenWidth - 250.0f; char scoreText[50]; char speedText[50]; char missionTimeText[50]; sprintf_s(scoreText, 50, "Score: %i", score); sprintf_s(speedText, 50, "Speed: %i", speed); sprintf_s(missionTimeText, 50, "Time: %f", missionTime / 100.0f); DrawText(scoreText, x1, startY, 0.0f, 1.0f, 0.0f); DrawText(speedText, x2, startY, 0.0f, 1.0f, 0.0f); DrawText(missionTimeText, x3, startY, 0.0f, 1.0f, 0.0f); } void DrawStats() { float startX = screenWidth - screenWidth / 2.5f; float startY = 275.0f; float spaceY = 30.0f; char asteroidsHitText[50]; char maximumSpeedText[50]; char scoreText[50]; char missionTimeText[50]; sprintf_s(asteroidsHitText, 50, "Asteroids Hit: %i", asteroidsHit); sprintf_s(maximumSpeedText, 50, "Maximum Speed: %i", maximumSpeed); sprintf_s(scoreText, 50, "Score: %i", score); sprintf_s(missionTimeText, 50, "Time: %f", missionTime / 100.0f); DrawText(asteroidsHitText, startX, startY, 0.0f, 1.0f, 0.0f); DrawText(maximumSpeedText, startX, startY + spaceY, 0.0f, 1.0f, 0.0f); DrawText(scoreText, startX, startY + spaceY * 2.0f, 0.0f, 1.0f, 0.0f); DrawText(missionTimeText, startX, startY + spaceY * 3.0f, 0.0f, 1.0f, 0.0f); } void DrawCredits() { float startX = screenWidth - screenWidth / 2.5f; float startY = 300.0f; float spaceY = 30.0f; DrawText("Robert Madsen", startX, startY, 0.0f, 1.0f, 0.0f); DrawText("Author", startX, startY + spaceY, 0.0f, 1.0f, 0.0f); }
These functions work exactly like their corresponding functions in RoboRacer2D. First, we use sprintf_s
to create a character string with the text that we want to display. Next, we use glRasterPos2f
to set the render position in 2D. Then, we use glCallLists
to actually render the font. In the DrawCredits
function, we use the DrawText
helper function to render the text.
Change CheckCollisions
to look like the code below:
void CheckCollisions() { bool collision = false; for (int i = 0; i < asteroids.size(); i++) { Model* item = asteroids[i]; collision = ship->CollidedWith(item); if (collision) { item->IsCollideable(false); score++; asteroidsHit++; } } }
This code updates the score and asteroid stats.
Now, it's time to load all of our textures. Add the following function to the game:
const bool LoadTextures() { menuScreen = new Sprite(1); menuScreen->SetFrameSize(screenWidth, screenHeight); menuScreen->SetNumberOfFrames(1); menuScreen->AddTexture("resources/mainmenu.png", false); menuScreen->IsActive(true); menuScreen->IsVisible(true); menuScreen->SetPosition(0.0f, 0.0f); playButton = new Sprite(1); playButton->SetFrameSize(75.0f, 38.0f); playButton->SetNumberOfFrames(1); playButton->SetPosition(690.0f, 300.0f); playButton->AddTexture("resources/playButton.png"); playButton->IsVisible(true); playButton->IsActive(false); inputManager->AddUiElement(playButton); creditsButton = new Sprite(1); creditsButton->SetFrameSize(75.0f, 38.0f); creditsButton->SetNumberOfFrames(1); creditsButton->SetPosition(690.0f, 350.0f); creditsButton->AddTexture("resources/creditsButton.png"); creditsButton->IsVisible(true); creditsButton->IsActive(false); inputManager->AddUiElement(creditsButton); exitButton = new Sprite(1); exitButton->SetFrameSize(75.0f, 38.0f); exitButton->SetNumberOfFrames(1); exitButton->SetPosition(690.0f, 500.0f); exitButton->AddTexture("resources/exitButton.png"); exitButton->IsVisible(true); exitButton->IsActive(false); inputManager->AddUiElement(exitButton); creditsScreen = new Sprite(1); creditsScreen->SetFrameSize(screenWidth, screenHeight); creditsScreen->SetNumberOfFrames(1); creditsScreen->AddTexture("resources/credits.png", false); creditsScreen->IsActive(true); creditsScreen->IsVisible(true); menuButton = new Sprite(1); menuButton->SetFrameSize(75.0f, 38.0f); menuButton->SetNumberOfFrames(1); menuButton->SetPosition(690.0f, 400.0f); menuButton->AddTexture("resources/menuButton.png"); menuButton->IsVisible(true); menuButton->IsActive(false); inputManager->AddUiElement(menuButton); gameOverScreen = new Sprite(1); gameOverScreen->SetFrameSize(screenWidth, screenHeight); gameOverScreen->SetNumberOfFrames(1); gameOverScreen->AddTexture("resources/gameover.png", false); gameOverScreen->IsActive(true); gameOverScreen->IsVisible(true); replayButton = new Sprite(1); replayButton->SetFrameSize(75.0f, 38.0f); replayButton->SetNumberOfFrames(1); replayButton->SetPosition(690.0f, 400.0f); replayButton->AddTexture("resources/replayButton.png"); replayButton->IsVisible(true); replayButton->IsActive(false); inputManager->AddUiElement(replayButton); return true; }
There is nothing new here! We are simply loading all of our 2D assets into the game as sprites. Here are a few reminders as to how this works:
Now that we have finally loaded all of our 2D assets, we are ready to finish the Render2D
function:
void Render2D() { Enable2D(); switch (gameState) { case GameState::GS_Loading: { splashScreen->Render(); } break; case GameState::GS_Menu: { menuScreen->Render(); playButton->Render(); creditsButton->Render(); exitButton->Render(); } break; case GameState::GS_Credits: { creditsScreen->Render(); menuButton->Render(); DrawCredits(); } break; case GameState::GS_Running: { DrawUi(); } break; case GameState::GS_Splash: { splashScreen->Render(); } break; case GameState::GS_GameOver: { gameOverScreen->Render(); DrawStats(); menuButton->Render(); } break; } Disable2D(); }
Again, there is nothing here that you haven't seen already. We are simply implementing the full state engine.
We can also implement the full ProcessInput
function now that we have buttons to click. Add the following lines to the switch
statement:
case Input::Command::CM_UI: { if (playButton->IsClicked()) { playButton->IsClicked(false); exitButton->IsActive(false); playButton->IsActive(false); creditsButton->IsActive(false); gameState = GameState::GS_Running; } if (creditsButton->IsClicked()) { creditsButton->IsClicked(false); exitButton->IsActive(false); playButton->IsActive(false); creditsButton->IsActive(false); gameState = GameState::GS_Credits; } if (menuButton->IsClicked()) { menuButton->IsClicked(false); exitButton->IsActive(true); playButton->IsActive(true); menuButton->IsActive(false); switch (gameState) { case GameState::GS_Credits: { gameState = GameState::GS_Menu; } break; case GameState::GS_GameOver: { StartGame(); } break; } } if (exitButton->IsClicked()) { playButton->IsClicked(false); exitButton->IsActive(false); playButton->IsActive(false); creditsButton->IsActive(false); PostQuitMessage(0); } } break; }
Yep, we've seen all this before. If you recall, the Input
class assigns a command enum to each button that can be clicked. This code simply processes the command, if there was any, and sets the state based on which button was just clicked.
We now implement the full Update
function to handle our new state machine:
void Update(const float p_deltaTime) { switch (gameState) { case GameState::GS_Splash: case GameState::GS_Loading: { splashScreen->Update(p_deltaTime); splashDisplayTimer += p_deltaTime; if (splashDisplayTimer > splashDisplayThreshold) { gameState = GameState::GS_Menu; } } break; case GameState::GS_Menu: { menuScreen->Update(p_deltaTime); playButton->IsActive(true); creditsButton->IsActive(true); exitButton->IsActive(true); playButton->Update(p_deltaTime); creditsButton->Update(p_deltaTime); exitButton->Update(p_deltaTime); inputManager->Update(p_deltaTime); ProcessInput(p_deltaTime); } break; case GameState::GS_Credits: { creditsScreen->Update(p_deltaTime); menuButton->IsActive(true); menuButton->Update(p_deltaTime); inputManager->Update(p_deltaTime); ProcessInput(p_deltaTime); } break; case GameState::GS_Running: { inputManager->Update(p_deltaTime); ProcessInput(p_deltaTime); ship->Update(p_deltaTime); ship->SetVelocity(ship->GetVelocity() + ship->GetVelocity()*p_deltaTime/10.0f); speed = ship->GetVelocity() * 1000; if (maximumSpeed < speed) { maximumSpeed = speed; } missionTime = missionTime + p_deltaTime * 100.0f; CheckCollisions(); if (ship->GetPosition().z > 10.0f) { gameState = GS_GameOver; menuButton->IsActive(true); gameOverScreen->IsActive(true); } } break; case GameState::GS_GameOver: { gameOverScreen->Update(p_deltaTime); replayButton->IsActive(true); replayButton->Update(p_deltaTime); exitButton->IsActive(true); exitButton->Update(p_deltaTime); inputManager->Update(p_deltaTime); ProcessInput(p_deltaTime); } break; } }
Finally, we need to modify the game loop so that it supports all of our new features. Move to the GameLoop
function and modify it so that it looks like the following code:
void GameLoop(const float p_deltatTime) { if (gameState == GameState::GS_Splash) { BuildFont(); LoadTextures(); gameState = GameState::GS_Loading; } Update(p_deltatTime); Render(); }
As always, the game loop calls the Update
and Render
functions. We add a special case to handle the splash screen. If we are in the GS_Splash
game state, we then load the rest of the resources for the game and change the game state to GS_Loading
.
Note that several of the functions referenced previously haven't been created yet! We will add support for sound, fonts, and textures as we continue.
18.216.166.101