Skip to content
Download

7. Game over

  1. Right-click the rooms folder and choose Nova room… (new room). Name it GameOver.

  2. Create the object GameOverScreen, without a sprite, and drag it into the room GameOver.

  3. The room Main is still the first one of the game. In the Recursos (Resources) panel, it shows up as Main (inicial). You change that under Projeto › Configurações do projeto… › Room inicial (Project › Project settings › start room).

Under Projeto › Configurações do projeto…, in Ações de entrada (input actions), create two more actions, the same way you created atirar:

Action Key Gamepad button
confirmar (confirm) Enter Pad:Start
sair (quit) Esc (choose Escape in the Tecla list) Pad:Back

Esc cancels Apertar a tecla… (press the key), which is why it is chosen from the list.

In game.hpp, add two helper functions: centeredX and newGame. This is the final version of the file:

src/game.hpp
#pragma once
#include <string_view>
// State that lives across rooms: score and lives.
struct GameState {
int score = 0;
int lives = 3;
};
GameState& state();
// X that centers a text on screen (the drawText font is 8 pixels per letter).
float centeredX(std::string_view text, float screenWidth);
// Resets the state and opens the room Main.
void newGame();

And game.cpp gets the two functions:

src/game.cpp
#include "game.hpp"
#include <brasa/brasa.hpp>
GameState& state() {
static GameState current;
return current;
}
float centeredX(std::string_view text, float screenWidth) {
return (screenWidth - static_cast<float>(text.size()) * 8.0f) / 2.0f;
}
void newGame() {
state() = GameState{};
brasa::goToScene("Main");
}
  • centeredX computes the x that centers a text. Each letter of drawText is 8 pixels wide, so the text takes letters × 8, and the rest of the screen is split between the two sides.
  • newGame resets the state (GameState{} goes back to 0 points and 3 lives) and opens the room Main. Because newGame is not inside an object, it uses brasa::goToScene, the “outside” version of goTo.

In Player.cpp, when the lives run out, the ship goes to the room GameOver before going away. This is the final Player.cpp:

objects/Player.cpp
#include "Player.hpp"
#include "src/game.hpp"
#include <algorithm>
using namespace brasa;
void Player::onSpawn() {
animSpeed = 0.2f; // the engine flame alternates between the 2 frames
// Touching an Enemy costs one life (and the enemy goes away too).
onCollide<Enemy>([this](Enemy& enemy) {
if (hurt()) enemy.destroy();
});
}
void Player::onUpdate() {
// The actions are in the project settings (keyboard and gamepad).
// axis returns from -1 to 1: -1 = the first action, 1 = the second.
float dx = axis("esquerda", "direita");
float dy = axis("cima", "baixo");
if (dx != 0.0f && dy != 0.0f) { // on the diagonal, it moves at the same speed
dx *= 0.7071f;
dy *= 0.7071f;
}
// clamp keeps the ship inside the screen (the ship is 16 px, so the margin is 8).
const float maxX = static_cast<float>(roomWidth()) - 8.0f;
const float maxY = static_cast<float>(roomHeight()) - 8.0f;
x = std::clamp(x + dx * velocidade, 8.0f, maxX);
y = std::clamp(y + dy * velocidade, 8.0f, maxY);
// held(action) is true while the key or the button is pressed.
if (held("atirar") && canShoot_) {
spawn<Bullet>(x, y - 8.0f, "Tiros"); // the shot is born in the Tiros layer, behind the ship
Audio::play("tiro", {.volume = 0.4f});
canShoot_ = false;
after(intervaloTiro, [this] { canShoot_ = true; }); // only shoots again a few steps from now
}
if (invincible_ > 0) --invincible_;
}
void Player::onRender() {
// Blinks while invincible: skips the drawing on some steps.
if (invincible_ > 0 && (invincible_ / 4) % 2 == 0) return;
drawSelf();
}
bool Player::hurt() {
if (invincible_ > 0 || isDestroyed()) return false;
--state().lives;
Audio::play("dano");
if (state().lives <= 0) {
goTo("GameOver");
destroy();
} else {
invincible_ = 90; // 1.5 s at 60 steps per second
}
return true;
}
BRASA_ENTITY(Player);

goTo("GameOver") switches rooms: everything in the current room goes away and the room GameOver starts, with GameOverScreen in it.

Open GameOverScreen.hpp, delete the examples and write:

objects/GameOverScreen.hpp
#pragma once
#include <brasa/brasa.hpp>
// Game over screen: shows the score and waits for "confirmar" (play again) or "sair" (quit).
class GameOverScreen : public brasa::Entity {
public:
BRASA_VARS(GameOverScreen)
void onUpdate() override;
void onRenderGui() override;
};

And GameOverScreen.cpp:

objects/GameOverScreen.cpp
#include "GameOverScreen.hpp"
#include "src/game.hpp"
#include <format>
#include <string>
#include <string_view>
using namespace brasa;
void GameOverScreen::onUpdate() {
// pressed(action) is true only on the step the key or the button was pressed.
if (pressed("confirmar")) newGame();
if (pressed("sair")) quit();
}
void GameOverScreen::onRenderGui() {
const float screenW = static_cast<float>(roomWidth());
constexpr std::string_view kTitle = "FIM DE JOGO";
constexpr std::string_view kRetry = "ENTER JOGAR DE NOVO";
constexpr std::string_view kQuit = "ESC SAIR";
const std::string score = std::format("PONTOS {}", state().score);
drawText(kTitle, centeredX(kTitle, screenW), 60.0f, colors::red);
drawText(score, centeredX(score, screenW), 85.0f);
drawText(kRetry, centeredX(kRetry, screenW), 120.0f);
drawText(kQuit, centeredX(kQuit, screenW), 135.0f, Color{150, 150, 150, 255});
}
BRASA_ENTITY(GameOverScreen);
  • pressed("confirmar") is true only on the step the action was pressed, unlike held, which is true while it is held. For menus, pressed is the right one: one tap, one action.
  • quit() closes the game.
  • In onRenderGui, four texts centered with centeredX. Color{150, 150, 150, 255} is a gray: red, green, blue and opacity, from 0 to 255.

Press Play and play until you lose the three lives. The game over screen shows your score. Enter starts over from zero and Esc closes.

Done: your first game is complete! 🎉

  • A game can have several rooms, and goTo("Name") switches from one to another. See Switch rooms.
  • pressed is true only on the step the action was pressed, and held while it is held. See Input.
  • quit() closes the game.
  • Helper functions, such as centeredX and newGame, can live in game.cpp and serve every object.
  • Change the numbers in the Inspector and see what happens: the ship’s velocidade, the intervaloTiro, the pontos of each enemy.
  • Check the small box of a variable on an instance (under Valores próprios, own values) to give it a value of its own.
  • Make a new enemy that zigzags (hint: change hspeed with every).
  • Make the camera follow the ship in a bigger room: see Camera following the player.
  • Check the guides in How to and the Reference.