Skip to content
Download

5. Score and lives

The score and the lives don’t belong to a single object: the enemy adds points, the ship loses lives and the scoreboard shows both. So they live in a shared place, the game state, in two loose code files.

Right-click the src folder and choose Novo script… (new script). Type game.hpp and confirm. Write:

src/game.hpp
#pragma once
// State that lives across rooms: score and lives.
struct GameState {
int score = 0;
int lives = 3;
};
GameState& state();

Do the same to create game.cpp, in the same folder:

src/game.cpp
#include "game.hpp"
GameState& state() {
static GameState current;
return current;
}

From any object that has #include "src/game.hpp", state().score and state().lives give access to the score and the lives.

How many points is an enemy worth? It is a good value to tune in the editor. In the Inspetor (Inspector) of Enemy, create the variable pontos (points), of type inteiro (integer), with the value 100.

In Enemy.hpp, add the function kill. The file ends up like this:

objects/Enemy.hpp
#pragma once
#include <brasa/brasa.hpp>
// Enemy that falls from the top of the screen. pontos is a variable of the object (Inspector).
class Enemy : public brasa::Entity {
public:
BRASA_VARS(Enemy)
void onSpawn() override;
void onUpdate() override;
// Adds points, plays the sound and destroys itself.
void kill();
};

In Enemy.cpp, the shot now calls kill(), which adds the points:

objects/Enemy.cpp
#include "Enemy.hpp"
#include "src/game.hpp"
using namespace brasa;
void Enemy::onSpawn() {
animSpeed = 0.1f;
// random(a, b) picks a number between a and b: each enemy falls at a different speed.
vspeed = random(0.8f, 1.6f);
// When a Bullet touches me, the shot goes away and I die.
// The other objects (Bullet, Player...) are already known here, with no #include.
onCollide<Bullet>([this](Bullet& bullet) {
bullet.destroy();
kill();
});
}
void Enemy::onUpdate() {
// roomHeight() is the height of the screen; past it (with some slack), the enemy got away.
if (y > static_cast<float>(roomHeight()) + 16.0f) destroy();
}
void Enemy::kill() {
if (isDestroyed()) return; // avoids scoring twice on the same step
state().score += pontos;
destroy();
}
BRASA_ENTITY(Enemy);

isDestroyed() avoids an annoying case: if two shots hit the same enemy on the same step, it only scores once. (The sound the comment in the .hpp mentions comes in the next part.)

In Player.hpp, the ship gets the onRender event, the function hurt and an invincibility counter. This is the final version of the file:

objects/Player.hpp
#pragma once
#include <brasa/brasa.hpp>
// The ship. velocidade and intervaloTiro are variables of the object: change them in the Inspector, without touching the code.
class Player : public brasa::Entity {
public:
BRASA_VARS(Player)
void onSpawn() override;
void onUpdate() override;
void onRender() override;
// Takes one life. Returns false if it was invincible (the hit did not count).
bool hurt();
private:
bool canShoot_ = true; // turns false for intervaloTiro steps after each shot
int invincible_ = 0; // steps of invincibility left
};

In Player.cpp, the ship now reacts to enemies and blinks while it is invincible:

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
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;
if (state().lives <= 0) {
destroy();
} else {
invincible_ = 90; // 1.5 s at 60 steps per second
}
return true;
}
BRASA_ENTITY(Player);

What is new:

  • In onSpawn: when the ship touches an enemy, hurt() tries to take one life. If the hit counted, the enemy is destroyed.
  • hurt(): if the ship is invincible, nothing happens. Otherwise, it loses a life. With no lives left, the ship is destroyed (in part 7 this will open the game over screen). With lives left, it is invincible for 90 steps, a second and a half.
  • At the end of onUpdate: the invincibility goes down one step at a time.
  • onRender: it is the event that draws the object. Normally Brasa draws the sprite by itself. Here, while the ship is invincible, it skips the drawing on some steps, which makes the ship blink. On the other steps, drawSelf() draws the sprite normally.
  1. Create the object Hud, without a sprite, and drag it into the room Main.

  2. Open Hud.hpp, delete the examples and write:

    objects/Hud.hpp
    #pragma once
    #include <brasa/brasa.hpp>
    // Scoreboard: score and lives in the corner of the screen.
    class Hud : public brasa::Entity {
    public:
    BRASA_VARS(Hud)
    void onRenderGui() override;
    };
  3. Open Hud.cpp, delete the examples and write:

    objects/Hud.cpp
    #include "Hud.hpp"
    #include "src/game.hpp"
    #include <format>
    using namespace brasa;
    void Hud::onRenderGui() {
    // drawText only draws ASCII: no accents in the texts
    drawText(std::format("PONTOS {:05}", state().score), 4.0f, 4.0f);
    drawText(std::format("VIDAS {}", state().lives), 4.0f, 14.0f, colors::yellow);
    }
    BRASA_ENTITY(Hud);
  • onRenderGui draws on top of everything, in screen coordinates, ideal for scoreboards and menus.
  • drawText(text, x, y) writes a text. The color is optional: colors::yellow makes the lives yellow.
  • std::format("PONTOS {:05}", ...) builds the text. {:05} shows the number with 5 digits, padded with zeros: 00300.

Press Play. Each destroyed enemy adds 100 points to the scoreboard. Let an enemy hit the ship: the lives go down and the ship blinks. With no lives left, the ship goes away (for now the game goes on without it).

  • Data that several objects use lives in a shared state, like state().
  • onRender draws the object, and drawSelf() draws the normal sprite.
  • onRenderGui draws on top of everything, and drawText writes on screen. See Text on screen.
  • isDestroyed() tells whether the instance has already been destroyed on this step.

Next part: 6. Sound →