Creating the user interface

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.

Defining the text system

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 score
  • speed: This is the current speed of the ship
  • missionTime: This is the number of seconds that have elapsed since starting the mission
  • asteroidsHit: This is the number of asteroids hit by the player
  • maximumSpeed: 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.

Defining textures

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:

  • Each sprite is loaded from a PNG file, specifying the number of frames. As none of these sprites are animated they all have one frame.
  • We position each sprite with a 2D coordinate.
  • We set the properties—visible means that it can be seen, and active means that it can be clicked on.
  • If the object is intended to be a button, we add it to the UI system.

Wiring in render, update, and the game loop

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.166.101