5. Pontos e vidas
Onde guardar pontos e vidas
Seção intitulada “Onde guardar 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:
#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:
#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.
O inimigo vale pontos
Seção intitulada “O inimigo vale pontos”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:
#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:
#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.)
A nave perde vidas
Seção intitulada “A nave perde vidas”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:
#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:
#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.
O placar
Seção intitulada “O placar”-
Crie o objeto
Hud, sem sprite, e arraste-o para a roomMain. -
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;}; -
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 textosdrawText(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);
onRenderGuidesenha por cima de tudo, em coordenadas da tela, ideal para placar e menus.drawText(texto, x, y)escreve um texto. A cor é opcional:colors::yellowdeixa as vidas amarelas.std::format("PONTOS {:05}", ...)monta o texto. O{:05}mostra o número com 5 dígitos, completando com zeros:00300.
Teste agora
Seção intitulada “Teste agora”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).
O que você aprendeu
Seção intitulada “O que você aprendeu”- Dados que vários objetos usam ficam num estado comum, como o
state(). onRenderdesenha o objeto, edrawSelf()desenha o sprite normal.onRenderGuidesenha por cima de tudo, edrawTextescreve na tela. Veja Texto na tela.isDestroyed()diz se a instância já foi destruída neste passo.
Próxima parte: 6. Som →
