Pular para o conteúdo
Baixar

5. Pontos e vidas

Os pontos e as vidas não pertencem a um objeto só: o inimigo soma pontos, a nave perde vidas e o placar mostra os dois. Por isso eles ficam num lugar comum, o estado do jogo, em dois arquivos de código soltos.

Clique com o botão direito na pasta src e escolha Novo script…. Digite game.hpp e confirme. Escreva:

src/game.hpp
#pragma once
// Estado que vive entre as rooms: pontos e vidas.
struct GameState {
int score = 0;
int lives = 3;
};
GameState& state();

Faça o mesmo para criar o game.cpp, na mesma pasta:

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

De qualquer objeto que tenha #include "src/game.hpp", state().score e state().lives dão acesso aos pontos e às vidas.

Quantos pontos vale um inimigo? É um bom valor para ajustar no editor. No Inspetor do Enemy, crie a variável pontos, do tipo inteiro, com o valor 100.

No Enemy.hpp, acrescente a função kill. O arquivo fica assim:

objects/Enemy.hpp
#pragma once
#include <brasa/brasa.hpp>
// Inimigo que cai do topo da tela. pontos é uma variável do objeto (Inspetor).
class Enemy : public brasa::Entity {
public:
BRASA_VARS(Enemy)
void onSpawn() override;
void onUpdate() override;
// Soma pontos, toca o som e se destrói.
void kill();
};

No Enemy.cpp, o tiro agora chama kill(), que soma os pontos:

objects/Enemy.cpp
#include "Enemy.hpp"
#include "src/game.hpp"
using namespace brasa;
void Enemy::onSpawn() {
animSpeed = 0.1f;
// random(a, b) sorteia um número entre a e b: cada inimigo cai numa velocidade diferente.
vspeed = random(0.8f, 1.6f);
// Quando um Bullet encosta em mim, o tiro some e eu morro.
// Os outros objetos (Bullet, Player...) já são conhecidos aqui, sem #include.
onCollide<Bullet>([this](Bullet& bullet) {
bullet.destroy();
kill();
});
}
void Enemy::onUpdate() {
// roomHeight() é a altura da tela; passou dela (com folga), o inimigo escapou.
if (y > static_cast<float>(roomHeight()) + 16.0f) destroy();
}
void Enemy::kill() {
if (isDestroyed()) return; // evita pontuar duas vezes no mesmo step
state().score += pontos;
destroy();
}
BRASA_ENTITY(Enemy);

O isDestroyed() evita um caso chato: se dois tiros acertarem o mesmo inimigo no mesmo passo, ele só conta pontos uma vez. (O som de que o comentário do .hpp fala vem na próxima parte.)

No Player.hpp, a nave ganha o evento onRender, a função hurt e um contador de invencibilidade. Esta é a versão final do arquivo:

objects/Player.hpp
#pragma once
#include <brasa/brasa.hpp>
// A nave. velocidade e intervaloTiro são variáveis do objeto: mude no Inspetor, sem mexer no código.
class Player : public brasa::Entity {
public:
BRASA_VARS(Player)
void onSpawn() override;
void onUpdate() override;
void onRender() override;
// Tira uma vida. Devolve false se estava invencível (o golpe não contou).
bool hurt();
private:
bool canShoot_ = true; // vira false por intervaloTiro steps depois de cada tiro
int invincible_ = 0; // steps de invencibilidade que restam
};

No Player.cpp, a nave passa a reagir aos inimigos e a piscar enquanto está invencível:

objects/Player.cpp
#include "Player.hpp"
#include "src/game.hpp"
#include <algorithm>
using namespace brasa;
void Player::onSpawn() {
animSpeed = 0.2f; // a chama do motor alterna entre os 2 frames
// Encostar num Enemy custa uma vida (e o inimigo some junto).
onCollide<Enemy>([this](Enemy& enemy) {
if (hurt()) enemy.destroy();
});
}
void Player::onUpdate() {
// As ações ficam em Configurações do projeto (teclado e controle).
// axis devolve de -1 a 1: -1 = a primeira ação, 1 = a segunda.
float dx = axis("esquerda", "direita");
float dy = axis("cima", "baixo");
if (dx != 0.0f && dy != 0.0f) { // na diagonal, anda com a mesma velocidade
dx *= 0.7071f;
dy *= 0.7071f;
}
// clamp mantém a nave dentro da tela (a nave tem 16 px, então a margem é 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(ação) é verdadeiro enquanto a tecla ou o botão estão pressionados.
if (held("atirar") && canShoot_) {
spawn<Bullet>(x, y - 8.0f, "Tiros"); // o tiro nasce na camada Tiros, atrás da nave
canShoot_ = false;
after(intervaloTiro, [this] { canShoot_ = true; }); // só atira de novo daqui a alguns steps
}
if (invincible_ > 0) --invincible_;
}
void Player::onRender() {
// Pisca enquanto está invencível: pula o desenho em alguns 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 a 60 steps por segundo
}
return true;
}
BRASA_ENTITY(Player);

As novidades:

  • No onSpawn: quando a nave encosta num inimigo, hurt() tenta tirar uma vida. Se o golpe contou, o inimigo é destruído.
  • hurt(): se a nave está invencível, nada acontece. Senão, perde uma vida. Sem vidas, a nave é destruída (na parte 7 isso vai abrir a tela de fim de jogo). Com vidas, fica invencível por 90 passos, um segundo e meio.
  • No fim do onUpdate: a invencibilidade diminui um passo de cada vez.
  • onRender: é o evento que desenha o objeto. Normalmente a Brasa desenha o sprite sozinha. Aqui, enquanto a nave está invencível, ela pula o desenho em alguns passos, o que faz a nave piscar. Nos outros passos, drawSelf() desenha o sprite normalmente.
  1. Crie o objeto Hud, sem sprite, e arraste-o para a room Main.

  2. Abra o Hud.hpp, apague os exemplos e escreva:

    objects/Hud.hpp
    #pragma once
    #include <brasa/brasa.hpp>
    // Placar: pontos e vidas no canto da tela.
    class Hud : public brasa::Entity {
    public:
    BRASA_VARS(Hud)
    void onRenderGui() override;
    };
  3. Abra o Hud.cpp, apague os exemplos e escreva:

    objects/Hud.cpp
    #include "Hud.hpp"
    #include "src/game.hpp"
    #include <format>
    using namespace brasa;
    void Hud::onRenderGui() {
    // drawText só desenha ASCII: nada de acentos nos textos
    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 desenha por cima de tudo, em coordenadas da tela, ideal para placar e menus.
  • drawText(texto, x, y) escreve um texto. A cor é opcional: colors::yellow deixa as vidas amarelas.
  • std::format("PONTOS {:05}", ...) monta o texto. O {:05} mostra o número com 5 dígitos, completando com zeros: 00300.

Aperte Play. Cada inimigo destruído soma 100 pontos no placar. Deixe um inimigo bater na nave: as vidas caem e a nave pisca. Sem vidas, a nave some (por enquanto o jogo continua sem ela).

  • Dados que vários objetos usam ficam num estado comum, como o state().
  • onRender desenha o objeto, e drawSelf() desenha o sprite normal.
  • onRenderGui desenha por cima de tudo, e drawText escreve na tela. Veja Texto na tela.
  • isDestroyed() diz se a instância já foi destruída neste passo.

Próxima parte: 6. Som →