5.2 Asteroids
O cenário do jogo Asteroids será composto pelos seguintes objetos:
- Uma nave espacial, formada por
GL_TRIANGLES
; - Asteroides, formados por
GL_TRIANGLE_FAN
; - Tiros, formados por
GL_TRIANGLE_FAN
; - Estrelas de fundo, formadas por
GL_POINTS
.
Como nas aplicações feitas até agora, trabalharemos somente com gráficos 2D. As coordenadas de todos os objetos do jogo serão especificadas no chamado NDC (espaço normalizado do dispositivo). Como vimos na seção 4.3, para que as primitivas sejam renderizadas, as coordenadas em NDC devem estar dentro do volume de visão canônico, que é um cubo de \((-1, -1, -1)\) a \((1, 1, 1)\). Também vimos que coordenadas em NDC são mapeadas para o espaço da janela, de modo que o ponto \((-1,-1)\) é mapeado para o canto inferior esquerdo do viewport, e \((1,1)\) é mapeado para o canto superior direito, de acordo com o especificado em glViewport
. A figura 5.5 ilustra o posicionamento de objetos da cena recortados pela região visível do NDC.
No jogo Asteroids original, a nave se movimenta pela tela enquanto a câmera virtual permanece fixa. Quando a nave sai dos limites da tela, reaparece no lado oposto. No nosso projeto, a nave se manterá fixa no centro da tela, enquanto todo o resto se moverá ao seu redor. O espaço será finito como no Asteroids original, e terá o tamanho da região que vai de \((-1,-1)\) a \((1,1)\). Se um asteroide sair do lado esquerdo da tela, reaparecerá no lado direito (observe que isso acontece na figura 5.5).
Um truque simples para obter o efeito de replicação do espaço é renderizar a cena nove vezes, uma vez para célula de uma grade 3x3 na qual apenas a célula do meio corresponde à região de \((-1,-1)\) a \((1,1)\). Isso é ilustrado na figura 5.6:
Não é necessário replicar os objetos que não saem da tela, como a nave. No nosso caso, os tiros também não serão replicados e deixarão de existir assim que saírem da tela.
Embora esse truque de replicação de cena funcione bem para este jogo simples, em cenas mais complexas é recomendável fazer testes de proximidade para não desenhar os objetos que estão totalmente fora da área visível. Isso evita processamento desnecessário no pipeline gráfico.
Organização do projeto
Nosso jogo possui vários objetos de cena, e portanto possui potencialmente vários VBOs, VAOs e variáveis de propriedades desses objetos. Precisamos pensar bem em como organizar tudo isso. O código pode ficar bastante confuso se definirmos tudo na classe Window
como fizemos nos projetos anteriores.
Organização das classes
Para organizar melhor o projeto, separaremos os elementos de cena do jogo nas seguintes classes:
Ship
: classe que representará a nave espacial (VAO, VBO e atributos como translação, orientação e velocidade).StarLayers
: classe que gerenciará as camadas de estrelas usadas para fazer o efeito de paralaxe de fundo26.StarLayers
conterá um arranjo de objetos do tipoStarLayer
, sendo que cadaStarLayer
definirá o VBO de pontos de uma camada de estrelas.Bullets
: classe que gerencia os tiros. A classe terá uma lista de instâncias de uma estruturaBullet
, sendo que cadaBullet
representará as propriedades de um tiro (translação, velocidade, etc). Todos os tiros compartilharão um mesmo VBO definido como membro deBullets
.Asteroids
: classe que gerenciará os asteroides.Asteroids
conterá uma lista de instâncias de uma estruturaAsteroid
, sendo que cadaAsteroid
definirá o VBO e as propriedades de um asteroide.
As classes Ship
, StarLayers
, Bullets
e Asteroids
terão funções públicas create
, paint
e destroy
que serão chamadas respectivamente nas funções onCreate
, onPaint
e onDestroy
de Window
.
Definiremos também uma classe GameData
para permitir o compartilhamento dos dados de estado do jogo entre Window
e as outras classes.
Organização dos arquivos
O diretório de projeto abcg/examples/asteroids
terá a seguinte estrutura:
asteroids/
│ asteroids.cpp
│ asteroids.hpp
│ bullets.cpp
│ bullets.hpp
│ CMakeLists.txt
│ gamedata.hpp
│ main.cpp
│ window.hpp
│ window.cpp
│ ship.cpp
│ ship.hpp
│ starlayers.cpp
│ starlayers.hpp
│
└───assets/
│ Inconsolata-Medium.ttf
│ objects.frag
│ objects.vert
│ stars.frag
└ stars.vert
O subdiretório assets
terá os seguintes arquivos de recursos utilizados no jogo:
Inconsolata-Medium.ttf
: fonte Inconsolata utilizada na mensagem “Game Over” e “You Win”. É a mesma fonte que utilizados no projetotictactoe
(seção 2.4).stars.vert
estars.frag
: código-fonte do vertex shader e fragment shader utilizados para renderizar as estrelas.objects.vert
eobjects.frag
: código-fonte do vertex shader e fragment shader utilizados em todos os outros objetos: nave, asteroides e tiros.
Poderíamos continuar definindo os shaders através de strings, como fizemos até agora, mas o projeto fica mais organizado desta nova forma.
Quando o projeto é compilado para WebAssembly, o conteúdo de assets
é transformado em um arquivo .data
no diretório public
. Assim, os arquivos resultantes de um projeto chamado proj
são:
proj.data
: arquivo de recursos (assets);proj.js
: arquivo JavaScript que deve ser chamado pelo html;proj.wasm
: binário WebAssembly.
Configuração inicial
Em
abcg/examples
, crie o subdiretórioasteroids
.No arquivo
abcg/examples/CMakeLists.txt
, inclua a linhaadd_subdirectory(asteroids)
.Crie o arquivo
abcg/examples/asteroids/CMakeLists.txt
com o seguinte conteúdo:project(asteroids) add_executable(${PROJECT_NAME} main.cpp window.cpp asteroids.cpp bullets.cpp ship.cpp starlayers.cpp)enable_abcg(${PROJECT_NAME})
Crie todos os arquivos
.cpp
e.hpp
(deasteroids.cpp
atéstarlayers.cpp
). Por enquanto eles ficarão vazios.Crie o subdiretório
assets
e baixe/copie a fonte.ttf
. Crie também os arquivos.frag
e.vert
. Vamos editá-los em seguida.
main.cpp
Não há nada de realmente novo no conteúdo de main.cpp
. Apenas desativaremos o contador de FPS e o botão de tela cheia. O código ficará assim:
#include "window.hpp"
int main(int argc, char **argv) {
try {
abcg::Application app(argc, argv);
Window window;
window.setOpenGLSettings({.samples = 4});
window.setWindowSettings({
.width = 600,
.height = 600,
.showFPS = false,
.showFullscreenButton = false,
.title = "Asteroids",
});
app.run(window);
} catch (std::exception const &exception) {
fmt::print(stderr, "{}\n", exception.what());
return -1;
}
return 0;
}
gamedata.hpp
Neste arquivo definiremos uma estrutura GameData
que descreve o estado atual do jogo e o estado dos dispositivos de entrada:
#ifndef GAMEDATA_HPP_
#define GAMEDATA_HPP_
#include <bitset>
enum class Input { Right, Left, Down, Up, Fire };
enum class State { Playing, GameOver, Win };
struct GameData {
State m_state{State::Playing};
std::bitset<5> m_input; // [fire, up, down, left, right]
};
#endif
m_state
pode ser:State::Playing
: a aplicação está em modo de jogo, com a nave respondendo aos comandos do jogador;State::GameOver
: o jogador perdeu. Nesse caso a nave não é exibida e não responde aos comandos do jogador;State::Win
: o jogador ganhou. A nave também não é exibida neste estado.
m_input
é uma máscara de 5 bits setados em resposta a eventos dos dispositivos de entrada. Por exemplo, o bit 0 corresponde aInput::Right
e está setado enquanto o usuário pressiona a seta para a direita ou a teclaD
. Esse estado é atualizado pela função membroWindow::onEvent
que veremos adiante.
A classe Window
manterá um objeto GameData
que será compartilhado com outras classes (Ship
, Bullets
, Asteroids
, etc) sempre que elas precisarem ler ou modificar o estado do jogo.
objects.vert
Esse é o shader utilizado na renderização da nave, asteroides e tiros. O conteúdo será como a seguir:
#version 300 es
layout(location = 0) in vec2 inPosition;
uniform vec4 color;
uniform float rotation;
uniform float scale;
uniform vec2 translation;
out vec4 fragColor;
void main() {
float sinAngle = sin(rotation);
float cosAngle = cos(rotation);
vec2 rotated = vec2(inPosition.x * cosAngle - inPosition.y * sinAngle,
inPosition.x * sinAngle + inPosition.y * cosAngle);
vec2 newPosition = rotated * scale + translation;
gl_Position = vec4(newPosition, 0, 1);
fragColor = color;
}
Observe que os vértices só possuem um atributo inPosition
do tipo vec2
. Esse atributo corresponde à posição \((x,y)\) do vértice. A saída do vertex shader é uma cor RGBA definida pela variável uniforme color
. Isso significa que, usando esse shader, todos os vértices terão a mesma cor.
O código de main
é similar ao do vertex shader do projeto regularpolygons
, mas dessa vez a posição é modificada não apenas por um fator de escala e translação, mas também por uma rotação. As linhas 13 a 16 fazem com que a posição inPosition
seja rodada pelo ângulo rotation
(em radianos) no sentido anti-horário. O resultado é uma nova posição rotated
que é então transformada pela escala e translação (linha 18).
Em capítulos futuros, veremos a teoria das transformações geométricas e os passos necessários para se chegar à expressão das linhas 15 e 16.
Todos os objetos do jogo são desenhados em tons de cinza, mas não há nada nos shaders que impeça que utilizemos cores. O aspecto preto e branco do jogo é só uma escolha estética para lembrar o antigo Asteroids do arcade.
Estrelas
As estrelas serão desenhadas como pontos (GL_POINTS
) e usarão os shaders stars.vert
e stars.frag
definidos a seguir.
stars.vert
#version 300 es
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;
uniform vec2 translation;
uniform float pointSize;
out vec4 fragColor;
void main() {
gl_PointSize = pointSize;
gl_Position = vec4(inPosition.xy + translation, 0, 1);
= vec4(inColor, 1);
fragColor }
Os atributos de entrada são uma posição \((x,y)\) (inPosition
) e uma cor RGB (inColor
). Em main
, a cor de entrada é copiada para o atributo de saída (fragColor
) como uma cor RGBA onde A é 1. A posição do ponto é deslocada por translation
, e o tamanho do ponto é definido por pointSize
.
stars.frag
#version 300 es
precision mediump float;
in vec4 fragColor;
out vec4 outColor;
void main() {
float intensity = 1.0 - length(gl_PointCoord - vec2(0.5)) * 2.0;
= fragColor * intensity;
outColor }
O processamento principal deste shader ocorre na definição da variável intensity
. Para compreendermos o que está acontecendo, lembre-se primeiro que o tamanho de um ponto (gl_PointSize
) é dado em pixels. O ponto é na verdade um quadrado centralizado na posição de cada ponto. O fragment shader explora esse fato para exibir um gradiente radial no quadrado de modo a simular o formato circular de uma estrela. A variável embutida gl_PointCoord
contém as coordenadas do fragmento dentro do quadrado. Na configuração padrão, \((0,0)\) é o canto superior esquerdo, e \((1,1)\) é o canto inferior direito (figura 5.8).
A expressão length(gl_PointCoord - vec2(0.5))
calcula a distância euclidiana até o centro do quadrado. Na direção em \(x\) e \(y\), essa distância está no intervalo \([0,0.5]\). A distância é convertida em uma intensidade de luz armazenada em intensity
, sendo que a intensidade é máxima (1) no centro do quadrado. A cor de saída é multiplicada por essa intensidade. Se o quadrado for branco, o resultado será como o mostrado na figura 5.9).
Atualizando window.hpp
Para a implementação das estrelas, precisamos definir em Window
o identificador dos shaders m_starsProgram
e a instância de StarLayers
. O código atualizado ficará como a seguir:
#ifndef WINDOW_HPP_
#define WINDOW_HPP_
#include <random>
#include "abcgOpenGL.hpp"
#include "asteroids.hpp"
#include "bullets.hpp"
#include "ship.hpp"
#include "starlayers.hpp"
class Window : public abcg::OpenGLWindow {
protected:
void onEvent(SDL_Event const &event) override;
void onCreate() override;
void onUpdate() override;
void onPaint() override;
void onPaintUI() override;
void onResize(glm::ivec2 const &size) override;
void onDestroy() override;
private:
glm::ivec2 m_viewportSize{};
GLuint m_starsProgram{};
GLuint m_objectsProgram{};
GameData m_gameData;
Ship m_ship;
StarLayers m_starLayers;
abcg::Timer m_restartWaitTimer;
ImFont *m_font{};
std::default_random_engine m_randomEngine;
void restart();
};
#endif
Atualizando window.cpp
Precisamos atualizar também as funções membro de Window
:
Em
Window::onCreate
, inclua o seguinte código para compilar os novos shaders:// Create program to render the stars m_starsProgram = ::createOpenGLProgram({{.source = assetsPath + "stars.vert", abcg.stage = abcg::ShaderStage::Vertex}, {.source = assetsPath + "stars.frag", .stage = abcg::ShaderStage::Fragment}});
Em
Window::restart
, inclua a chamada aStarLayers::create
junto com a chamada aShip::create
feita anteriormente:m_starLayers.create(m_starsProgram, 25); m_ship.create(m_objectsProgram);
Em
Window::onUpdate
, chame a funçãoStarLayers::update
depois deShip::update
, assim:m_ship.update(m_gameData, deltaTime); m_starLayers.update(m_ship, deltaTime);
Em
Window::onPaint
, chameStarLayers::paint
antes deShip::paint
, assim:m_starLayers.paint(); m_ship.paint(m_gameData);
Por fim, modifique
Window::onDestroy
da seguinte forma:void Window::onDestroy() { ::glDeleteProgram(m_starsProgram); abcg::glDeleteProgram(m_objectsProgram); abcg m_ship.destroy(); m_starLayers.destroy(); }
starlayers.hpp
A definição da classe StarLayers
ficará assim:
#ifndef STARLAYERS_HPP_
#define STARLAYERS_HPP_
#include <array>
#include <random>
#include "abcgOpenGL.hpp"
#include "gamedata.hpp"
#include "ship.hpp"
class StarLayers {
public:
void create(GLuint program, int quantity);
void paint();
void destroy();
void update(const Ship &ship, float deltaTime);
private:
GLuint m_program{};
GLint m_pointSizeLoc{};
GLint m_translationLoc{};
struct StarLayer {
GLuint m_VAO{};
GLuint m_VBO{};
float m_pointSize{};
int m_quantity{};
glm::vec2 m_translation{};
};
std::array<StarLayer, 5> m_starLayers;
std::default_random_engine m_randomEngine;
};
#endif
Nas linhas 24 a 31 é definida a estrutura StarLayer
. Ela contém o VBO e VAO dos pontos que formam uma “camada” de estrelas. Além disso contém o tamanho (m_pointSize
) e quantidade (m_quantity
) dos pontos, e um fator de translação (m_translation
) utilizado para deslocar todos os pontos da camada (isto é, todos os vértices do VBO).
Na linha 33 é definido um arranjo de cinco objetos StarLayer
, pois renderizaremos cinco camadas sobrepostas de estrelas.
starlayers.cpp
O arquivo começa com a definição de StarLayers::create
:
#include "starlayers.hpp"
void StarLayers::create(GLuint program, int quantity) {
destroy();
// Initialize pseudorandom number generator and distributions
m_randomEngine.seed(
std::chrono::steady_clock::now().time_since_epoch().count());
std::uniform_real_distribution distPos(-1.0f, 1.0f);
std::uniform_real_distribution distIntensity(0.5f, 1.0f);
auto &re{m_randomEngine}; // Shortcut
m_program = program;
// Get location of uniforms in the program
m_pointSizeLoc = abcg::glGetUniformLocation(m_program, "pointSize");
m_translationLoc = abcg::glGetUniformLocation(m_program, "translation");
// Get location of attributes in the program
auto const positionAttribute{
abcg::glGetAttribLocation(m_program, "inPosition")};
auto const colorAttribute{abcg::glGetAttribLocation(m_program, "inColor")};
for (auto &&[index, layer] : iter::enumerate(m_starLayers)) {
// Create data for the stars of this layer
layer.m_pointSize = 10.0f / (1.0f + index);
layer.m_quantity = quantity * (gsl::narrow<int>(index) + 1);
layer.m_translation = {};
std::vector<glm::vec3> data;
for ([[maybe_unused]] auto _ : iter::range(0, layer.m_quantity)) {
data.emplace_back(distPos(re), distPos(re), 0);
data.push_back(glm::vec3(distIntensity(re)));
}
// Generate VBO
abcg::glGenBuffers(1, &layer.m_VBO);
abcg::glBindBuffer(GL_ARRAY_BUFFER, layer.m_VBO);
abcg::glBufferData(GL_ARRAY_BUFFER, data.size() * sizeof(glm::vec3),
data.data(), GL_STATIC_DRAW);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// Create VAO
abcg::glGenVertexArrays(1, &layer.m_VAO);
// Bind vertex attributes to current VAO
abcg::glBindVertexArray(layer.m_VAO);
abcg::glBindBuffer(GL_ARRAY_BUFFER, layer.m_VBO);
abcg::glEnableVertexAttribArray(positionAttribute);
abcg::glVertexAttribPointer(positionAttribute, 2, GL_FLOAT, GL_FALSE,
sizeof(glm::vec3) * 2, nullptr);
abcg::glEnableVertexAttribArray(colorAttribute);
abcg::glVertexAttribPointer(colorAttribute, 3, GL_FLOAT, GL_FALSE,
sizeof(glm::vec3) * 2,
reinterpret_cast<void *>(sizeof(glm::vec3)));
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// End of binding to current VAO
abcg::glBindVertexArray(0);
}
}
O laço da linha 24 itera sobre cada elemento de m_starLayers
. A expressão na linha 26 faz com que os pontos tenham tamanho 10
na 1ª camada, 5
na 2ª camada, 2.5
na 3ª camada, e assim sucessivamente. Na linha 27, a quantidade de pontos é dobrada a cada camada.
Na linha 30 é criado um arranjo data
com dados dos pontos da camada. Os dados ficarão intercalados no formato \[[\underline{x,y,0,r,g,b},x,y,0,r,g,b,\dots],\] onde \((x,y,0)\) é a posição do ponto, e \((r,g,b)\) é a cor do ponto. Dentro do laço, as coordenadas \(x\) e \(y\) são escolhidas de forma aleatória dentro do intervalo \([-1,1)\). A cor é um tom de cinza escolhido aleatoriamente do intervalo \([0.5,1)\).
Os dados de data
são copiados para o VBO através de glBufferData
na linha 39.
Observe nas linhas 49 a 57 como é feito o mapeamento do VBO com os atributos inPosition
(do tipo vec2
) e inColor
(do tipo vec4
) do vertex shader. O stride do VBO é sizeof(glm::vec3) * 2
(isto é, dois vec3
). Na linha 56, o deslocamento no início do VBO é sizeof(glm::vec3)
(isto é, apenas um vec3
). A conversão de tipo é necessária porque o parâmetro de deslocamento é do tipo void const *
ao invés de um inteiro (é assim por razões históricas).
A definição de StarLayers::paint
ficará como a seguir:
void StarLayers::paint() {
abcg::glUseProgram(m_program);
abcg::glEnable(GL_BLEND);
abcg::glBlendFunc(GL_ONE, GL_ONE);
for (auto const &layer : m_starLayers) {
abcg::glBindVertexArray(layer.m_VAO);
abcg::glUniform1f(m_pointSizeLoc, layer.m_pointSize);
for (auto const i : {-2, 0, 2}) {
for (auto const j : {-2, 0, 2}) {
abcg::glUniform2f(m_translationLoc, layer.m_translation.x + j,
layer.m_translation.y + i);
abcg::glDrawArrays(GL_POINTS, 0, layer.m_quantity);
}
}
abcg::glBindVertexArray(0);
}
abcg::glDisable(GL_BLEND);
abcg::glUseProgram(0);
}
Observe que os pontos são desenhados com o modo de mistura de cor habilitado. Na linha 68, a definição da função de mistura com fatores GL_ONE
faz com que as cores produzidas pelo fragment shader sejam somadas com as cores atuais do framebuffer. Isso produz um efeito cumulativo de intensidade da luz quando estrelas de camadas diferentes são renderizadas na mesma posição.
Os laços aninhados nas linhas 74 e 75 produzem índices i
e j
que são usados em layer.m_translation
para replicar o desenho das estrelas em uma grade 3x3 em torno da região visível do NDC, como vimos no início da seção.
Na linha 86, o modo de mistura de cor é desabilitado para não afetar a renderização dos outros objetos de cena que são totalmente opacos.
Em StarLayers::destroy
são liberados os VBOs e VAOs de todas as instâncias de StarLayer
:
void StarLayers::destroy() {
for (auto &layer : m_starLayers) {
abcg::glDeleteBuffers(1, &layer.m_VBO);
abcg::glDeleteVertexArrays(1, &layer.m_VAO);
}
}
Vamos agora à definição de StarLayers::update
:
void StarLayers::update(const Ship &ship, float deltaTime) {
for (auto &&[index, layer] : iter::enumerate(m_starLayers)) {
auto const layerSpeedScale{1.0f / (index + 2.0f)};
layer.m_translation -= ship.m_velocity * deltaTime * layerSpeedScale;
// Wrap-around
if (layer.m_translation.x < -1.0f)
layer.m_translation.x += 2.0f;
if (layer.m_translation.x > +1.0f)
layer.m_translation.x -= 2.0f;
if (layer.m_translation.y < -1.0f)
layer.m_translation.y += 2.0f;
if (layer.m_translation.y > +1.0f)
layer.m_translation.y -= 2.0f;
}
}
A translação (m_translation
) de cada camada é atualizada na linha 101 de acordo com a velocidade da nave. Se a nave está indo para a frente, então a camada de estrelas deve ir para trás: por isso a subtração. A velocidade é multiplicada por um fator de escala layerSpeedScale
para fazer com que a primeira camada seja mais rápida que a segunda, e assim sucessivamente para produzir o efeito de paralaxe.
Nas linhas 104 a 111 há uma série de condicionais que testam se os pontos saíram dos limites da região visível do NDC. Se saíram, são deslocados para o lado oposto.
Nesse momento, o jogo ficará como a seguir (link original):
O código pode ser baixado deste link.
Asteroides
Para incluir a implementação dos asteroides, vamos primeiramente atualizar OpenGLWindow
.
Atualizando window.hpp
Adicione a definição de m_asteroids
junto às definições dos outros objetos (m_ship
e m_starLayers
), assim:
m_asteroids;
Asteroids m_ship;
Ship m_starLayers; StarLayers
Atualizando window.cpp
Em
Window::restart
, chamecreate
dem_asteroids
junto com a chamada acreate
dos objetos anteriores:m_starLayers.create(m_starsProgram, 25); m_ship.create(m_objectsProgram); m_asteroids.create(m_objectsProgram, 3);
Em
Window::onUpdate
, chameupdate
dem_asteroids
apósupdate
dem_ship
:m_ship.update(m_gameData, deltaTime); m_starLayers.update(m_ship, deltaTime); m_asteroids.update(m_ship, deltaTime);
Em
Window::onPaint
, chamepaint
dem_asteroids
logo apóspaint
dem_starLayers
:m_starLayers.paint(); m_asteroids.paint(); m_ship.paint(m_gameData);
Em
Window::onDestroy
, chamedestroy
dem_asteroids
junto com a chamada adestroy
dos outros objetos:m_asteroids.destroy(); m_ship.destroy(); m_starLayers.destroy();
A ordem em que a função paint
de cada objeto é chamada é importante porque o objeto renderizado por último será desenhado sobre os anteriores que já estão no framebuffer.
Essa forma de renderizar os objetos na ordem do mais distante para o mais próximo é chamada de “algoritmo do pintor” pois é similar ao modo como um pintor desenha sobre uma tela: os elementos mais ao fundo são desenhados antes dos elementos mais à frente.
asteroids.hpp
A definição da classe Asteroids
ficará assim:
#ifndef ASTEROIDS_HPP_
#define ASTEROIDS_HPP_
#include <list>
#include <random>
#include "abcgOpenGL.hpp"
#include "gamedata.hpp"
#include "ship.hpp"
class Asteroids {
public:
void create(GLuint program, int quantity);
void paint();
void destroy();
void update(const Ship &ship, float deltaTime);
struct Asteroid {
GLuint m_VAO{};
GLuint m_VBO{};
float m_angularVelocity{};
glm::vec4 m_color{1};
bool m_hit{};
int m_polygonSides{};
float m_rotation{};
float m_scale{};
glm::vec2 m_translation{};
glm::vec2 m_velocity{};
};
std::list<Asteroid> m_asteroids;
Asteroid makeAsteroid(glm::vec2 translation = {}, float scale = 0.25f);
private:
GLuint m_program{};
GLint m_colorLoc{};
GLint m_rotationLoc{};
GLint m_translationLoc{};
GLint m_scaleLoc{};
std::default_random_engine m_randomEngine;
std::uniform_real_distribution<float> m_randomDist{-1.0f, 1.0f};
};
#endif
Entre as linhas 19 a 31 é definida a estrutura Asteroid
. Cada asteroide tem seu próprio VAO e VBO. Além disso, Asteroid
possui uma velocidade angular, uma cor, número de lados, ângulo de rotação, escala, translação, vetor de velocidade, e um flag m_hit
que indica se o asteroide foi acertado por um tiro.
Na linha 33 é definida uma lista de objetos Asteroid
. O número de elementos dessa lista será modificado de acordo com os asteroides que forem acertados pelos tiros. Cada vez que um asteroide for acertado, ele será retirado da lista. Entretanto, se o asteroide for grande, simularemos que ele foi quebrado em vários pedaços fazendo com asteroides menores sejam inseridos na lista.
A função makeAsteroid
declarada na linha 35 será utilizada para criar um novo asteroide para ser inserido na lista m_asteroids
. Os parâmetros translation
e scale
permitirão configurar a posição e o fator de escala do novo asteroide.
asteroids.cpp
O arquivo começa com a definição de Asteroids::create
:
#include "asteroids.hpp"
#include <glm/gtx/fast_trigonometry.hpp>
void Asteroids::create(GLuint program, int quantity) {
destroy();
m_randomEngine.seed(
std::chrono::steady_clock::now().time_since_epoch().count());
m_program = program;
// Get location of uniforms in the program
m_colorLoc = abcg::glGetUniformLocation(m_program, "color");
m_rotationLoc = abcg::glGetUniformLocation(m_program, "rotation");
m_scaleLoc = abcg::glGetUniformLocation(m_program, "scale");
m_translationLoc = abcg::glGetUniformLocation(m_program, "translation");
// Create asteroids
m_asteroids.clear();
m_asteroids.resize(quantity);
for (auto &asteroid : m_asteroids) {
asteroid = makeAsteroid();
// Make sure the asteroid won't collide with the ship
do {
asteroid.m_translation = {m_randomDist(m_randomEngine),
m_randomDist(m_randomEngine)};
} while (glm::length(asteroid.m_translation) < 0.5f);
}
}
Na linha 21, a lista de asteroides é iniciada com uma quantidade quantity
de objetos do tipo Asteroid
. Essa lista é então iterada no laço da linha 23 e o conteúdo de cada asteroide é substituído por Asteroids::makeAsteroid
. No laço das linhas 27 a 30 é escolhida uma posição aleatória para o asteroide, mas uma posição longe o suficiente da nave: não queremos que o jogo comece com o asteroide colidindo com a nave!
A definição de Asteroids::paint
ficará como a seguir:
void Asteroids::paint() {
abcg::glUseProgram(m_program);
for (auto const &asteroid : m_asteroids) {
abcg::glBindVertexArray(asteroid.m_VAO);
abcg::glUniform4fv(m_colorLoc, 1, &asteroid.m_color.r);
abcg::glUniform1f(m_scaleLoc, asteroid.m_scale);
abcg::glUniform1f(m_rotationLoc, asteroid.m_rotation);
for (auto i : {-2, 0, 2}) {
for (auto j : {-2, 0, 2}) {
abcg::glUniform2f(m_translationLoc, asteroid.m_translation.x + j,
asteroid.m_translation.y + i);
abcg::glDrawArrays(GL_TRIANGLE_FAN, 0, asteroid.m_polygonSides + 2);
}
}
abcg::glBindVertexArray(0);
}
abcg::glUseProgram(0);
}
A lista m_asteroids
é iterada e cada asteroide é renderizado 9 vezes (em uma grade 3x3), como fizemos com as estrelas.
Em Asteroids::destroy
são liberados os VBOs e VAOs dos asteroides:
void Asteroids::destroy() {
for (auto &asteroid : m_asteroids) {
abcg::glDeleteBuffers(1, &asteroid.m_VBO);
abcg::glDeleteVertexArrays(1, &asteroid.m_VAO);
}
}
Vamos agora à definição de Asteroids::update
:
void Asteroids::update(const Ship &ship, float deltaTime) {
for (auto &asteroid : m_asteroids) {
asteroid.m_translation -= ship.m_velocity * deltaTime;
asteroid.m_rotation = glm::wrapAngle(
asteroid.m_rotation + asteroid.m_angularVelocity * deltaTime);
asteroid.m_translation += asteroid.m_velocity * deltaTime;
// Wrap-around
if (asteroid.m_translation.x < -1.0f)
asteroid.m_translation.x += 2.0f;
if (asteroid.m_translation.x > +1.0f)
asteroid.m_translation.x -= 2.0f;
if (asteroid.m_translation.y < -1.0f)
asteroid.m_translation.y += 2.0f;
if (asteroid.m_translation.y > +1.0f)
asteroid.m_translation.y -= 2.0f;
}
}
Na linha 68, a translação (m_translation
) de cada asteroide é modificada pelo vetor de velocidade da nave, como fizemos com as estrelas. Na linha 69, a rotação é atualizada de acordo com a velocidade angular. Na linha 71, a translação do asteroide é modificada novamente, mas agora considerando a velocidade do próprio asteroide.
As condicionais das linhas 74 a 80 fazem com que as coordenadas de m_translation
permaneçam no intervalo circular de -1 a 1.
A definição de Asteroids::makeAsteroid
ficará como a seguir:
Asteroids::Asteroid Asteroids::makeAsteroid(glm::vec2 translation,
float scale) {
Asteroid asteroid;
auto &re{m_randomEngine}; // Shortcut
// Randomly pick the number of sides
std::uniform_int_distribution randomSides(6, 20);
asteroid.m_polygonSides = randomSides(re);
// Get a random color (actually, a grayscale)
std::uniform_real_distribution randomIntensity(0.5f, 1.0f);
asteroid.m_color = glm::vec4(randomIntensity(re));
asteroid.m_color.a = 1.0f;
asteroid.m_rotation = 0.0f;
asteroid.m_scale = scale;
asteroid.m_translation = translation;
// Get a random angular velocity
asteroid.m_angularVelocity = m_randomDist(re);
// Get a random direction
glm::vec2 const direction{m_randomDist(re), m_randomDist(re)};
asteroid.m_velocity = glm::normalize(direction) / 7.0f;
// Create geometry data
std::vector<glm::vec2> positions{{0, 0}};
auto const step{M_PI * 2 / asteroid.m_polygonSides};
std::uniform_real_distribution randomRadius(0.8f, 1.0f);
for (auto const angle : iter::range(0.0, M_PI * 2, step)) {
auto const radius{randomRadius(re)};
positions.emplace_back(radius * std::cos(angle), radius * std::sin(angle));
}
positions.push_back(positions.at(1));
// Generate VBO
abcg::glGenBuffers(1, &asteroid.m_VBO);
abcg::glBindBuffer(GL_ARRAY_BUFFER, asteroid.m_VBO);
abcg::glBufferData(GL_ARRAY_BUFFER, positions.size() * sizeof(glm::vec2),
positions.data(), GL_STATIC_DRAW);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// Get location of attributes in the program
auto const positionAttribute{
abcg::glGetAttribLocation(m_program, "inPosition")};
// Create VAO
abcg::glGenVertexArrays(1, &asteroid.m_VAO);
// Bind vertex attributes to current VAO
abcg::glBindVertexArray(asteroid.m_VAO);
abcg::glBindBuffer(GL_ARRAY_BUFFER, asteroid.m_VBO);
abcg::glEnableVertexAttribArray(positionAttribute);
abcg::glVertexAttribPointer(positionAttribute, 2, GL_FLOAT, GL_FALSE, 0,
nullptr);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// End of binding to current VAO
abcg::glBindVertexArray(0);
return asteroid;
}
Na linha 105 é escolhida uma velocidade angular aleatória no intervalo \([-1, 1)\). Essa velocidade será interpretada como sendo em radianos por segundo.
Na linha 109 é escolhido um vetor unitário aleatório para definir a velocidade do asteroide. As componentes do vetor são divididas por 7 de modo que cada asteroide inicie com uma velocidade de 1/7 unidades de espaço por segundo.
O restante do código cria a geometria do asteroide. O código é bem parecido com o que foi utilizado para criar o polígono regular no projeto regularpolygons
. A diferença é que agora usamos a equação paramétrica do círculo com raio \(r\)
\[ \begin{eqnarray} x&=&r cos(t),\\ y&=&r sin(t), \end{eqnarray} \]
e selecionamos um \(r\) aleatório do intervalo \([0.8, 1)\) para cada vértice do polígono.
Nesse momento, o jogo ficará como a seguir (link original):
O código pode ser baixado deste link.
Tiros e colisões
Até agora o jogo ainda não tem detecção de colisão com os asteroides. Faremos isso a seguir.
Atualizando window.hpp
Adicione a definição de m_bullets
junto à definição dos outros objetos:
m_asteroids;
Asteroids m_bullets;
Bullets m_ship;
Ship m_starLayers; StarLayers
Adicione também a declaração das seguintes funções adicionais de Window
:
void checkCollisions();
void checkWinCondition();
checkCollisions
será utilizada para verificar as colisões;checkWinCondition
será utilizada para verificar se o jogador ganhou (isto é, se não há mais asteroides).
Atualizando window.cpp
Em
Window::restart
, inclua a chamada à funçãocreate
dem_bullets
junto comcreate
dos objetos anteriores, assim:m_starLayers.create(m_starsProgram, 25); m_ship.create(m_objectsProgram); m_asteroids.create(m_objectsProgram, 3); m_bullets.create(m_objectsProgram);
Em
Window::onUpdate
, chameupdate
dem_bullets
em qualquer lugar após a chamada deupdate
dem_ship
. Por exemplo:m_ship.update(m_gameData, deltaTime); m_starLayers.update(m_ship, deltaTime); m_asteroids.update(m_ship, deltaTime); m_bullets.update(m_ship, m_gameData, deltaTime);
Após esses
update
s, inclua a seguinte condicional que chama as funções de detecção de colisão e verificação da condição de vitória se o jogo está no estadoState::Playing
:if (m_gameData.m_state == State::Playing) { (); checkCollisions(); checkWinCondition}
Em
Window::onPaint
, chamepaint
dem_bullets
logo após a chamada depaint
dem_asteroids
:m_starLayers.paint(); m_asteroids.paint(); m_bullets.paint(); m_ship.paint(m_gameData);
Em
Window::onDestroy
, chamedestroy
dem_bullets
junto comdestroy
dos outros objetos:m_asteroids.destroy(); m_bullets.destroy(); m_ship.destroy(); m_starLayers.destroy();
Vamos agora definir Window::checkCollisions
como a seguir:
void Window::checkCollisions() {
// Check collision between ship and asteroids
for (auto const &asteroid : m_asteroids.m_asteroids) {
auto const asteroidTranslation{asteroid.m_translation};
auto const distance{
glm::distance(m_ship.m_translation, asteroidTranslation)};
if (distance < m_ship.m_scale * 0.9f + asteroid.m_scale * 0.85f) {
m_gameData.m_state = State::GameOver;
m_restartWaitTimer.restart();
}
}
// Check collision between bullets and asteroids
for (auto &bullet : m_bullets.m_bullets) {
if (bullet.m_dead)
continue;
for (auto &asteroid : m_asteroids.m_asteroids) {
for (auto const i : {-2, 0, 2}) {
for (auto const j : {-2, 0, 2}) {
auto const asteroidTranslation{asteroid.m_translation +
glm::vec2(i, j)};
auto const distance{
glm::distance(bullet.m_translation, asteroidTranslation)};
if (distance < m_bullets.m_scale + asteroid.m_scale * 0.85f) {
asteroid.m_hit = true;
bullet.m_dead = true;
}
}
}
}
// Break asteroids marked as hit
for (auto const &asteroid : m_asteroids.m_asteroids) {
if (asteroid.m_hit && asteroid.m_scale > 0.10f) {
std::uniform_real_distribution randomDist{-1.0f, 1.0f};
std::generate_n(std::back_inserter(m_asteroids.m_asteroids), 3, [&]() {
glm::vec2 const offset{randomDist(m_randomEngine),
randomDist(m_randomEngine)};
auto const newScale{asteroid.m_scale * 0.5f};
return m_asteroids.makeAsteroid(
asteroid.m_translation + offset * newScale, newScale);
});
}
}
m_asteroids.m_asteroids.remove_if([](auto const &a) { return a.m_hit; });
}
}
Nas linhas 174 a 184 é feita a detecção de colisão entre a nave e cada asteroide:
// Check collision between ship and asteroids
for (auto const &asteroid : m_asteroids.m_asteroids) {
auto const asteroidTranslation{asteroid.m_translation};
auto const distance{
glm::distance(m_ship.m_translation, asteroidTranslation)};
if (distance < m_ship.m_scale * 0.9f + asteroid.m_scale * 0.85f) {
m_gameData.m_state = State::GameOver;
m_restartWaitTimer.restart();
}
}
A detecção de colisão é feita através da comparação da distância euclidiana (glm::distance
) entre as coordenadas de translação dos objetos. Essas coordenadas podem ser consideradas como a posição do centro dos objetos na cena (como ilustrado pelos pontos \(P_s\) e \(P_a\) na figura 5.10:
\(P_s\) e \(P_a\) também podem ser considerados como centros de círculos. O fator de escala de cada objeto corresponde ao raio do círculo (\(r_s\) e \(r_a\)). Assim, podemos detectar a colisão através de uma simples comparação da distância \(|P_s-P_a|\) com a soma dos fatores de escala. Só há colisão se a distância for menor ou igual a \(r_s+r_a\). Esse tipo de teste é bem mais simples e eficiente (embora menos preciso) do que comparar a interseção entre os triângulos que formam os objetos.
Note, na linha 180, que \(r_s\) e \(r_a\) são de fato os fatores m_scale
de cada objeto, mas multiplicados por 0.9f
(para a nave) e 0.85f
(para o asteroide). Isso é feito para diminuir um pouco o raio dos círculos e fazer com que exista uma tolerância de sobreposição antes de ocorrer a colisão. Veja, na figura 5.10, que dessa forma os objetos não ficam inscritos nos círculos. Os valores 0.9
e 0.85
foram determinados empiricamente.
O laço for
da linha 187 itera sobre os tiros. Dentro desse laço há outro (linha 191) que itera sobre os asteroides para verificar se há colisão entre cada tiro e os asteroides:
// Check collision between bullets and asteroids
for (auto &bullet : m_bullets.m_bullets) {
if (bullet.m_dead)
continue;
for (auto &asteroid : m_asteroids.m_asteroids) {
for (auto const i : {-2, 0, 2}) {
for (auto const j : {-2, 0, 2}) {
auto const asteroidTranslation{asteroid.m_translation +
glm::vec2(i, j)};
auto const distance{
glm::distance(bullet.m_translation, asteroidTranslation)};
if (distance < m_bullets.m_scale + asteroid.m_scale * 0.85f) {
asteroid.m_hit = true;
bullet.m_dead = true;
}
}
}
}
A verificação da interseção é calculada novamente através da comparação da distância entre círculos. Note que os testes de distância são feitos dentro de laços aninhados parecidos com os que foram utilizados para replicar a renderização dos asteroides na grade 3x3 em torno da região visível do viewport. De fato, o teste de colisão de um tiro com um asteroide precisa considerar essa replicação, pois um asteroide que está saindo à esquerda do viewport pode ser atingido por um tiro no lado oposto, à direita.
Se um tiro acertou um asteroide, o m_hit
do asteroide e o m_dead
do tiro tornam-se true
.
Observe agora as linhas 207 a 219:
// Break asteroids marked as hit
for (auto const &asteroid : m_asteroids.m_asteroids) {
if (asteroid.m_hit && asteroid.m_scale > 0.10f) {
std::uniform_real_distribution randomDist{-1.0f, 1.0f};
std::generate_n(std::back_inserter(m_asteroids.m_asteroids), 3, [&]() {
glm::vec2 const offset{randomDist(m_randomEngine),
randomDist(m_randomEngine)};
auto const newScale{asteroid.m_scale * 0.5f};
return m_asteroids.makeAsteroid(
asteroid.m_translation + offset * newScale, newScale);
});
}
}
Neste código, os asteroides com m_hit == true
são testados para verificar se são suficientemente grandes (m_scale > 0.10f
). Se sim, três novos asteroides menores são criados e inseridos na lista m_asteroids.m_asteroids
. std::generate
é uma função da biblioteca padrão do C++. Ela chama um número de vezes (nesse caso 3 vezes) a expressão lambda passada como último argumento. O valor de retorno da expressão lambda é inserido no fim de m_asteroids.m_asteroids
usando a função auxiliar std::back_inserter
.
Na linha 221, os asteroides que estavam com m_hit == true
são removidos da lista usando std::remove_if
:
Isso é tudo para a detecção de colisão. Vamos agora à definição de Window::checkWinCondition
, que ficará como a seguir:
void Window::checkWinCondition() {
if (m_asteroids.m_asteroids.empty()) {
m_gameData.m_state = State::Win;
m_restartWaitTimer.restart();
}
}
A vitória ocorre quando a lista de asteroides está vazia. Nesse caso, o estado do jogo é modificado para State::Win
e o temporizador m_restartWaitTimer
é reiniciado. Como resultado, o jogo será reiniciado após cinco segundos (essa verificação é feita em Window::onUpdate
). Enquanto isso, o texto de vitória será exibido em Window::onPaintUI
.
bullets.hpp
A definição da classe Bullets
ficará como a seguir:
#ifndef BULLETS_HPP_
#define BULLETS_HPP_
#include <list>
#include "abcgOpenGL.hpp"
#include "gamedata.hpp"
#include "ship.hpp"
class OpenGLWindow;
class Bullets {
public:
void create(GLuint program);
void paint();
void destroy();
void update(Ship &ship, const GameData &gameData, float deltaTime);
struct Bullet {
bool m_dead{};
glm::vec2 m_translation{};
glm::vec2 m_velocity{};
};
std::list<Bullet> m_bullets;
float m_scale{0.015f};
private:
GLuint m_program{};
GLint m_colorLoc{};
GLint m_rotationLoc{};
GLint m_translationLoc{};
GLint m_scaleLoc{};
GLuint m_VAO{};
GLuint m_VBO{};
};
#endif
Nas linhas 20 a 24 é definida a estrutura Bullet
. Observe que o VAO e VBO não está em Bullet
, mas em Bullets
, pois todos os tiros utilizam o mesmo VBO.
Na linha 26 é definida a lista de tiros atualmente na cena. O número de elementos desta lista será alterado de acordo com a quantidade de tiros visíveis.
Na linha 28 é definido o fator de escala comum a todos os tiros.
bullets.cpp
O arquivo começa com a definição de Bullets::create
:
#include "bullets.hpp"
#include <glm/gtx/rotate_vector.hpp>
void Bullets::create(GLuint program) {
destroy();
m_program = program;
// Get location of uniforms in the program
m_colorLoc = abcg::glGetUniformLocation(m_program, "color");
m_rotationLoc = abcg::glGetUniformLocation(m_program, "rotation");
m_scaleLoc = abcg::glGetUniformLocation(m_program, "scale");
m_translationLoc = abcg::glGetUniformLocation(m_program, "translation");
// Get location of attributes in the program
auto const positionAttribute{
abcg::glGetAttribLocation(m_program, "inPosition")};
m_bullets.clear();
// Create geometry data
auto const sides{10};
std::vector<glm::vec2> positions{{0, 0}};
auto const step{M_PI * 2 / sides};
for (auto const angle : iter::range(0.0, M_PI * 2, step)) {
positions.emplace_back(std::cos(angle), std::sin(angle));
}
positions.push_back(positions.at(1));
// Generate VBO of positions
abcg::glGenBuffers(1, &m_VBO);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
abcg::glBufferData(GL_ARRAY_BUFFER, positions.size() * sizeof(glm::vec2),
positions.data(), GL_STATIC_DRAW);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// Create VAO
abcg::glGenVertexArrays(1, &m_VAO);
// Bind vertex attributes to current VAO
abcg::glBindVertexArray(m_VAO);
abcg::glEnableVertexAttribArray(positionAttribute);
abcg::glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
abcg::glVertexAttribPointer(positionAttribute, 2, GL_FLOAT, GL_FALSE, 0,
nullptr);
abcg::glBindBuffer(GL_ARRAY_BUFFER, 0);
// End of binding to current VAO
abcg::glBindVertexArray(0);
}
A lista de tiros é inicializada como vazia na linha 20. O restante do código é para criar o VBO que será compartilhado por todos os tiros. O VBO contém vértices de um polígono regular de 10 lados e usa o mesmo código que utilizamos no projeto regularpolygons
.
A definição de Bullets::paint
ficará como a seguir:
void Bullets::paint() {
abcg::glUseProgram(m_program);
abcg::glBindVertexArray(m_VAO);
abcg::glUniform4f(m_colorLoc, 1, 1, 1, 1);
abcg::glUniform1f(m_rotationLoc, 0);
abcg::glUniform1f(m_scaleLoc, m_scale);
for (auto const &bullet : m_bullets) {
abcg::glUniform2f(m_translationLoc, bullet.m_translation.x,
bullet.m_translation.y);
abcg::glDrawArrays(GL_TRIANGLE_FAN, 0, 12);
}
abcg::glBindVertexArray(0);
abcg::glUseProgram(0);
}
Todos os tiros têm a mesma cor (linha 58), ângulo de rotação (linha 59) e fator de escala (linha 60). A lista de tiros é iterada no laço das linhas 62 a 67. Cada tiro é renderizado como um GL_TRIANGLE_FAN
.
Em Bullets::destroy
é liberado o VBO e VAO:
void Bullets::destroy() {
abcg::glDeleteBuffers(1, &m_VBO);
abcg::glDeleteVertexArrays(1, &m_VAO);
}
Vamos agora à definição de Bullets::update
:
void Bullets::update(Ship &ship, const GameData &gameData, float deltaTime) {
// Create a pair of bullets
if (gameData.m_input[gsl::narrow<size_t>(Input::Fire)] &&
gameData.m_state == State::Playing) {
// At least 250 ms must have passed since the last bullets
if (ship.m_bulletCoolDownTimer.elapsed() > 250.0 / 1000.0) {
ship.m_bulletCoolDownTimer.restart();
// Bullets are shot in the direction of the ship's forward vector
auto const forward{glm::rotate(glm::vec2{0.0f, 1.0f}, ship.m_rotation)};
auto const right{glm::rotate(glm::vec2{1.0f, 0.0f}, ship.m_rotation)};
auto const cannonOffset{(11.0f / 15.5f) * ship.m_scale};
auto const bulletSpeed{2.0f};
Bullet bullet{.m_dead = false,
.m_translation = ship.m_translation + right * cannonOffset,
.m_velocity = ship.m_velocity + forward * bulletSpeed};
m_bullets.push_back(bullet);
bullet.m_translation = ship.m_translation - right * cannonOffset;
m_bullets.push_back(bullet);
// Moves ship in the opposite direction
ship.m_velocity -= forward * 0.1f;
}
}
for (auto &bullet : m_bullets) {
bullet.m_translation -= ship.m_velocity * deltaTime;
bullet.m_translation += bullet.m_velocity * deltaTime;
// Kill bullet if it goes off screen
if (bullet.m_translation.x < -1.1f)
bullet.m_dead = true;
if (bullet.m_translation.x > +1.1f)
bullet.m_dead = true;
if (bullet.m_translation.y < -1.1f)
bullet.m_dead = true;
if (bullet.m_translation.y > +1.1f)
bullet.m_dead = true;
}
// Remove dead bullets
m_bullets.remove_if([](auto const &p) { return p.m_dead; });
}
Um par de tiros é criado a cada disparo. O temporizador m_bulletCoolDownTimer
é utilizado para fazer com que os disparos ocorram em intervalos de no mínimo 250 milissegundos.
Observe, na linha 102, que subtraímos da velocidade da nave o vetor de direção dos tiros. Isso produz um efeito de recuo da nave. Quanto mais tiros são disparados, mais a nave será deslocada para trás.
Nas linhas 106 a 119 são atualizadas as coordenadas de translação de cada tiro.
Nas linhas 107 e 108, os tiros são atualizados de acordo com a velocidade da nave e a velocidade do próprio tiro.
Nas linhas 110 a 118, verificamos se o tiro saiu da tela. Se sim, o flag m_dead
torna-se true
. A comparação é feita com -1.1f
/+1.1f
no lugar de -1.0f
/+1.0f
para ter certeza que todo o polígono do tiro (e não só seu centro) saiu da tela.
Na linha 122, todos os tiros com m_dead == true
são removidos da lista.
Isso é tudo! Eis o jogo completo (link original):
O código do projeto completo pode ser baixado deste link.