5. Score and lives
Where to keep score and lives
Section titled “Where to keep 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:
#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:
#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.
The enemy is worth points
Section titled “The enemy is worth points”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:
#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:
#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.)
The ship loses lives
Section titled “The ship loses lives”In Player.hpp, the ship gets the onRender event, the function hurt and an invincibility counter. This is the final version of the file:
#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:
#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.
The scoreboard
Section titled “The scoreboard”-
Create the object
Hud, without a sprite, and drag it into the roomMain. -
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;}; -
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 textsdrawText(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);
onRenderGuidraws on top of everything, in screen coordinates, ideal for scoreboards and menus.drawText(text, x, y)writes a text. The color is optional:colors::yellowmakes the lives yellow.std::format("PONTOS {:05}", ...)builds the text.{:05}shows the number with 5 digits, padded with zeros:00300.
Test it now
Section titled “Test it now”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).
What you learned
Section titled “What you learned”- Data that several objects use lives in a shared state, like
state(). onRenderdraws the object, anddrawSelf()draws the normal sprite.onRenderGuidraws on top of everything, anddrawTextwrites on screen. See Text on screen.isDestroyed()tells whether the instance has already been destroyed on this step.
Next part: 6. Sound →
