5.2 Asteroids
A cena de nosso Asteroids será composta 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 descartar os objetos que estão 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 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 OpenGLWindow
como fizemos nos projetos anteriores.
Classes
Para organizar melhor o projeto, separaremos os elementos de cena do jogo nas seguintes classes:
Ship
: classe que representa a nave espacial (VAO, VBO e atributos como translação, orientação e velocidade).StarLayers
: classe que gerencia as camadas de estrelas usadas para fazer o efeito de paralaxe24 de fundo.StarLayers
contém um arranjo de objetos do tipoStarLayer
, sendo que cadaStarLayer
define o VBO de pontos de uma camada de estrelas.Bullets
: classe que gerencia os tiros. A classe contém uma lista de instâncias de uma estruturaBullet
, sendo que cadaBullet
representa as propriedades de um tiro (translação, velocidade, etc). Todos os tiros compartilham um mesmo VBO definido emBullets
.Asteroids
: classe que gerencia os asteroides.Asteroids
contém uma lista de instâncias de uma estruturaAsteroid
, sendo que cadaAsteroid
define o VBO e propriedades de um asteroide.
As classes Ship
, StarLayers
, Bullets
e Asteroids
contêm suas próprias funções membro initializeGL
, paintGL
e terminateGL
que serão chamadas nas funções membro respectivas de OpenGLWindow
.
Definiremos também uma classe GameData
para permitir o compartilhamento de dados de estado do jogo entre OpenGLWindow
e as outras classes.
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
│ openglwindow.hpp
│ openglwindow.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
contém arquivos de recursos utilizados no jogo:
- O arquivo
Inconsolata-Medium.ttf
é a fonte Inconsolata utilizada na mensagem “Game Over” e “You Win.” O arquivo pode ser baixado ou copiado deabcg/abcg/assets
(ou substitua por sua fonte favorita!). - Os arquivos
stars.vert
estars.frag
contêm o código-fonte do vertex shader e fragment shader utilizados para renderizar as estrelas. - Os arquivos
objects.vert
eobjects.frag
contêm o 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, mas o projeto fica mais organizado desta nova forma.
Sempre que um projeto da ABCg é configurado pelo CMake, o diretório assets
(se existir) é copiado para build/bin/proj
, onde proj
é o nome do projeto.
Em todas as vezes que um arquivo de assets
for modificado, é necessário limpar o diretório build
para forçar a cópia de assets
para build/bin/proj
na próxima compilação. Isso pode ser feito das seguintes maneiras:
- Removendo o diretório
build
antes da compilação; - No Visual Studio Code, usando o comando “CMake: Clean Rebuild” da paleta de comandos (
Ctrl+Shift+P
) antes da compilação; - Construindo o projeto através de
build.sh
/build.bat
.
Se você precisar editar um shader várias vezes, deixe-o como uma string como fizemos nos projetos anteriores. Transforme-o em um asset apenas quando o shader estiver pronto e não for mais editado.
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
serã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 openglwindow.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 os arquivos 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 <fmt/core.h>
#include "abcg.hpp"
#include "openglwindow.hpp"
int main(int argc, char **argv) {
try {
abcg::Application app(argc, argv);
auto window{std::make_unique<OpenGLWindow>()};
window->setOpenGLSettings({.samples = 4});
window->setWindowSettings({.width = 600,
.height = 600,
.showFPS = false,
.showFullscreenButton = false,
.title = "Asteroids"});
app.run(std::move(window));
} catch (const abcg::Exception &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
: quando a aplicação está em modo de jogo, com a nave respondendo aos comandos do jogador;State::GameOver
: quando o jogador perdeu. Nesse caso a nave não é exibida e não responde aos comandos do jogador;State::Win
: quando o jogador ganhou. A nave também não é exibida nesse estado.
m_input
é uma máscara de bits de eventos de estado dos dispositivos de entrada. Por exemplo, o bit 0 corresponde aInput::Right
e está setado enquando o usuário pressiona a seta para a direita, ou a teclaD
. Esse estado é atualizado pela função membroOpenGLWindow::handleEvent
que veremos adiante.
A classe OpenGLWindow
manterá uma instância de GameData
que será compartilhada 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 410
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.
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 artística 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 410
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 410
in vec4 fragColor;
out vec4 outColor;
void main() {
float intensity = 1.0 - length(gl_PointCoord - vec2(0.5)) * 2.0;
outColor = fragColor * intensity; }
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, e tamanhos maiores que 1 fazem com que o pipeline renderize 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 openglwindow.hpp
Para a implementação das estrelas, precisamos definir em OpenGLWindow
o identificador dos shaders m_starsProgram
e a instância de StarLayers
. O código atualizado ficará como a seguir:
#ifndef OPENGLWINDOW_HPP_
#define OPENGLWINDOW_HPP_
#include <imgui.h>
#include <random>
#include "abcg.hpp"
#include "asteroids.hpp"
#include "bullets.hpp"
#include "ship.hpp"
#include "starlayers.hpp"
class OpenGLWindow : public abcg::OpenGLWindow {
protected:
void handleEvent(SDL_Event& event) override;
void initializeGL() override;
void paintGL() override;
void paintUI() override;
void resizeGL(int width, int height) override;
void terminateGL() override;
private:
GLuint m_starsProgram{};
GLuint m_objectsProgram{};
int m_viewportWidth{};
int m_viewportHeight{};
GameData m_gameData;
Ship m_ship;
StarLayers m_starLayers;
abcg::ElapsedTimer m_restartWaitTimer;
ImFont* m_font{};
std::default_random_engine m_randomEngine;
void restart();
void update();
};
#endif
Atualizando openglwindow.cpp
Precisamos atualizar também as funções membro de OpenGLWindow
:
Em
OpenGLWindow::initializeGL
, inclua o seguinte código para compilar os novos shaders:// Create program to render the stars m_starsProgram = createProgramFromFile(getAssetsPath() + "stars.vert", "stars.frag"); getAssetsPath() +
Em
OpenGLWindow::restart
, inclua a chamada ainitializeGL
deStarLayers
junto com a chamada deinitializeGL
deShip
:m_starLayers.initializeGL(m_starsProgram, 25); m_ship.initializeGL(m_objectsProgram);
Em
OpenGLWindow::update
, chame a funçãoupdate
deStarLayers
depois da chamada deupdate
deShip
, assim:m_ship.update(m_gameData, deltaTime); m_starLayers.update(m_ship, deltaTime);
Em
OpenGLWindow::paintGL
, chamepaintGL
deStarLayers
antes depaintGL
deShip
, assim:m_starLayers.paintGL(); m_ship.paintGL(m_gameData);
Por fim, modifique
OpenGLWindow::terminateGL
da seguinte forma:void OpenGLWindow::terminateGL() { m_starsProgram); abcg::glDeleteProgram(m_objectsProgram); abcg::glDeleteProgram( m_ship.terminateGL(); m_starLayers.terminateGL(); }
starlayers.hpp
A definição da classe StarLayers
ficará assim:
#ifndef STARLAYERS_HPP_
#define STARLAYERS_HPP_
#include <array>
#include <random>
#include "abcg.hpp"
#include "gamedata.hpp"
#include "ship.hpp"
class OpenGLWindow;
class StarLayers {
public:
void initializeGL(GLuint program, int quantity);
void paintGL();
void terminateGL();
void update(const Ship &ship, float deltaTime);
private:
friend OpenGLWindow;
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{glm::vec2(0)};
};
std::array<StarLayer, 5> m_starLayers;
std::default_random_engine m_randomEngine;
};
#endif
Nas linhas 28 a 35 é definida StarLayer
. A estrutura contém o VBO e VAO dos pontos que formam uma camada de estrelas, o tamanho (m_pointSize
) e quantidade (m_quantity
) de 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 37 é definido um arranjo de cinco instâncias de StarLayer
, pois renderizaremos cinco camadas de estrelas.
starlayers.cpp
O arquivo começa com a definição de StarLayers::initializeGL
:
#include "starlayers.hpp"
#include <cppitertools/itertools.hpp>
void StarLayers::initializeGL(GLuint program, int quantity) {
terminateGL();
// Start pseudo-random number generator
m_randomEngine.seed(
std::chrono::steady_clock::now().time_since_epoch().count());
m_program = program;
m_pointSizeLoc = abcg::glGetUniformLocation(m_program, "pointSize");
m_translationLoc = abcg::glGetUniformLocation(m_program, "translation");
auto &re{m_randomEngine};
std::uniform_real_distribution<float> distPos(-1.0f, 1.0f);
std::uniform_real_distribution<float> distIntensity(0.5f, 1.0f);
for (auto &&[index, layer] : iter::enumerate(m_starLayers)) {
layer.m_pointSize = 10.0f / (1.0f + index);
layer.m_quantity = quantity * (static_cast<int>(index) + 1);
layer.m_translation = glm::vec2(0);
std::vector<glm::vec3> data(0);
for ([[maybe_unused]] auto i : iter::range(0, layer.m_quantity)) {
data.emplace_back(distPos(re), distPos(re), 0);
data.push_back(glm::vec3(1) * 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);
// Get location of attributes in the program
GLint positionAttribute{abcg::glGetAttribLocation(m_program, "inPosition")};
GLint colorAttribute{abcg::glGetAttribLocation(m_program, "inColor")};
// 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 20 itera sobre cada elemento de m_starLayers
. A expressão na linha 21 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 22, a quantidade de pontos é dobrada a cada camada.
Na linha 25 é criado um arranjo data
com dados dos pontos da camada. Os dados ficarão intercalados no formato \(\{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\) de cada ponto são escolhidas de forma aleatória dentro do intervalo \([-1,1]\). A cor de cada ponto é 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 34.
Observe nas linhas 48 a 56 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 55, o deslocamento no início do VBO é sizeof(glm::vec3)
(isto é, apenas um vec3
). O cast de tipo é necessário porque o parâmetro de deslocamento é do tipo const void *
(é assim por razões históricas).
A definição de StarLayers::paintGL
ficará como a seguir:
void StarLayers::paintGL() {
abcg::glUseProgram(m_program);
abcg::glEnable(GL_BLEND);
abcg::glBlendFunc(GL_ONE, GL_ONE);
for (const auto &layer : m_starLayers) {
abcg::glBindVertexArray(layer.m_vao);
abcg::glUniform1f(m_pointSizeLoc, layer.m_pointSize);
for (const auto i : {-2, 0, 2}) {
for (const auto 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 67, 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 73 e 74 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 85, 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::terminateGL
são liberados os VBOs e VAOs de todas as instâncias de StarLayer
:
void StarLayers::terminateGL() {
for (auto &layer : m_starLayers) {
abcg::glDeleteBuffers(1, &layer.m_vbo);
abcg::glDeleteVertexArrays(1, &layer.m_vao);
}
}
Em StarLayers::update
, a translação (m_translation
) de cada camada é atualizada 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 de ship.m_velocity
. 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 103 a 106 há uma série de condicionais que testam se os pontos saíram dos limites da região visível do NDC. Se sim, são deslocados para o lado oposto.
void StarLayers::update(const Ship &ship, float deltaTime) {
for (auto &&[index, layer] : iter::enumerate(m_starLayers)) {
const auto 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;
}
}
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 openglwindow.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 openglwindow.cpp
Em
OpenGLWindow::restart
, chame oinitializeGL
dem_asteroids
junto com a chamada deinitializeGL
dos objetos anteriores:m_starLayers.initializeGL(m_starsProgram, 25); m_ship.initializeGL(m_objectsProgram); m_asteroids.initializeGL(m_objectsProgram, 3);
Em
OpenGLWindow::update
, chame oupdate
dem_asteroids
após oupdate
dem_ship
:m_ship.update(m_gameData, deltaTime); m_starLayers.update(m_ship, deltaTime); m_asteroids.update(m_ship, deltaTime);
Em
OpenGLWindow::paintGL
, chame opaintGL
dem_asteroids
logo após opaintGL
dem_starLayers
:m_starLayers.paintGL(); m_asteroids.paintGL(); m_ship.paintGL(m_gameData);
Em
OpenGLWindow::terminateGL
, chame oterminateGL
dem_asteroids
junto com oterminateGL
dos outros objetos:m_asteroids.terminateGL(); m_ship.terminateGL(); m_starLayers.terminateGL();
A ordem em que a função paintGL
de cada objeto é chamada é importante porque o objeto renderizado por último será desenhado sobre os anteriores que já foram desenhados antes 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: primeiro é desenhado o fundo (elemento mais distante) e então sobre ele são desenhados os elementos mais próximos.
asteroids.hpp
A definição da classe Asteroids
ficará assim:
#ifndef ASTEROIDS_HPP_
#define ASTEROIDS_HPP_
#include <list>
#include <random>
#include "abcg.hpp"
#include "gamedata.hpp"
#include "ship.hpp"
class OpenGLWindow;
class Asteroids {
public:
void initializeGL(GLuint program, int quantity);
void paintGL();
void terminateGL();
void update(const Ship &ship, float deltaTime);
private:
friend OpenGLWindow;
GLuint m_program{};
GLint m_colorLoc{};
GLint m_rotationLoc{};
GLint m_translationLoc{};
GLint m_scaleLoc{};
struct Asteroid {
GLuint m_vao{};
GLuint m_vbo{};
float m_angularVelocity{};
glm::vec4 m_color{1};
bool m_hit{false};
int m_polygonSides{};
float m_rotation{};
float m_scale{};
glm::vec2 m_translation{glm::vec2(0)};
glm::vec2 m_velocity{glm::vec2(0)};
};
std::list<Asteroid> m_asteroids;
std::default_random_engine m_randomEngine;
std::uniform_real_distribution<float> m_randomDist{-1.0f, 1.0f};
Asteroids::Asteroid createAsteroid(glm::vec2 translation = glm::vec2(0),
float scale = 0.25f);
};
#endif
Entre as linhas 30 a 42 é definida a estrutura Asteroid
. Cada asteroide tem seu próprio VAO e VBO. Além disso 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 44 é definida uma lista de 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 e então asteroides menores serão inseridos na lista.
A função membro createAsteroid
declarada nas linhas 49 e 50 será utilizada para criar um novo asteroide para ser inserido na lista m_asteroids
. O fator de escala (parâmetro scale
) permitirá configurar o tamanho do novo asteroide.
asteroids.cpp
O arquivo começa com a definição de Asteroids::initializeGL
:
#include "asteroids.hpp"
#include <cppitertools/itertools.hpp>
#include <glm/gtx/fast_trigonometry.hpp>
void Asteroids::initializeGL(GLuint program, int quantity) {
terminateGL();
// Start pseudo-random number generator
m_randomEngine.seed(
std::chrono::steady_clock::now().time_since_epoch().count());
m_program = 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 = createAsteroid();
// 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 das linhas 23 a 31 e o conteúdo de cada asteroide é modificado por Asteroids::createAsteroid
. No laço da linha 27 é escolhida uma posição aleatória para o asteroide, mas que esteja longe o suficiente da nave: não queremos que o jogo comece com o asteroide colidindo com a nave!
A definição de Asteroids::paintGL
ficará como a seguir:
void Asteroids::paintGL() {
abcg::glUseProgram(m_program);
for (const auto &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::terminateGL
são liberados os VBOs e VAOs dos asteroides:
void Asteroids::terminateGL() {
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 77 fazem com que as coordenadas de m_translation
permaneçam no intervalo circular de -1 a 1.
Em Asteroids::createAsteroid
é criada uma nova instância de Asteroid
:
Asteroids::Asteroid Asteroids::createAsteroid(glm::vec2 translation,
float scale) {
Asteroid asteroid;
auto &re{m_randomEngine}; // Shortcut
// Randomly choose the number of sides
std::uniform_int_distribution<int> randomSides(6, 20);
asteroid.m_polygonSides = randomSides(re);
// Choose a random color (actually, a grayscale)
std::uniform_real_distribution<float> randomIntensity(0.5f, 1.0f);
asteroid.m_color = glm::vec4(1) * randomIntensity(re);
asteroid.m_color.a = 1.0f;
asteroid.m_rotation = 0.0f;
asteroid.m_scale = scale;
asteroid.m_translation = translation;
// Choose a random angular velocity
asteroid.m_angularVelocity = m_randomDist(re);
// Choose a random direction
glm::vec2 direction{m_randomDist(re), m_randomDist(re)};
asteroid.m_velocity = glm::normalize(direction) / 7.0f;
// Create geometry
std::vector<glm::vec2> positions(0);
positions.emplace_back(0, 0);
const auto step{M_PI * 2 / asteroid.m_polygonSides};
std::uniform_real_distribution<float> randomRadius(0.8f, 1.0f);
for (const auto angle : iter::range(0.0, M_PI * 2, step)) {
const auto 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
GLint 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 101 é escolhida uma velocidade angular aleatória do intervalo \([-1, 1]\) (em radianos por segundo).
Na linha 105 é 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
Neste momento o jogo ainda não tem detecção de colisões e tiros. É isso que implementaremos agora.
Atualizando openglwindow.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 membro adicionais de OpenGLWindow
:
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 openglwindow.cpp
Em
OpenGLWindow::restart
, inclua a chamada à funçãoinitializeGL
dem_bullets
junto cominitializeGL
dos objetos anteriores, assim:m_starLayers.initializeGL(m_starsProgram, 25); m_ship.initializeGL(m_objectsProgram); m_asteroids.initializeGL(m_objectsProgram, 3); m_bullets.initializeGL(m_objectsProgram);
Em
OpenGLWindow::update
, 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);
Além disso, inclua a seguinte condicional após os
update
s para calcular as colisões e verificar a condição de vitória:if (m_gameData.m_state == State::Playing) { checkCollisions(); checkWinCondition(); }
Em
OpenGLWindow::paintGL
, chamepaintGL
dem_bullets
logo após a chamada depaintGL
dem_asteroids
:m_starLayers.paintGL(); m_asteroids.paintGL(); m_bullets.paintGL(); m_ship.paintGL(m_gameData);
Em
OpenGLWindow::terminateGL
, chameterminateGL
dem_bullets
junto comterminateGL
dos outros objetos:m_asteroids.terminateGL(); m_bullets.terminateGL(); m_ship.terminateGL(); m_starLayers.terminateGL();
Vamos agora definir OpenGLWindow::checkCollisions
como a seguir:
void OpenGLWindow::checkCollisions() {
// Check collision between ship and asteroids
for (const auto &asteroid : m_asteroids.m_asteroids) {
const auto asteroidTranslation{asteroid.m_translation};
const auto 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 (const auto i : {-2, 0, 2}) {
for (const auto j : {-2, 0, 2}) {
const auto asteroidTranslation{asteroid.m_translation +
glm::vec2(i, j)};
const auto 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 (const auto &asteroid : m_asteroids.m_asteroids) {
if (asteroid.m_hit && asteroid.m_scale > 0.10f) {
std::uniform_real_distribution<float> m_randomDist{-1.0f, 1.0f};
std::generate_n(std::back_inserter(m_asteroids.m_asteroids), 3, [&]() {
const glm::vec2 offset{m_randomDist(m_randomEngine),
m_randomDist(m_randomEngine)};
return m_asteroids.createAsteroid(
asteroid.m_translation + offset * asteroid.m_scale * 0.5f,
asteroid.m_scale * 0.5f);
});
}
}
m_asteroids.m_asteroids.remove_if(
[](const Asteroids::Asteroid &a) { return a.m_hit; });
}
}
Nas linhas 173 a 183 é feita a detecção de colisão entre a nave e cada asteroide:
// Check collision between ship and asteroids
for (auto &asteroid : m_asteroids.m_asteroids) {
auto asteroidTranslation{asteroid.m_translation};
auto 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 179, 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.
Nas linhas 185 a 203 é feita a detecção de colisão entre os tiros 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 (const auto i : {-2, 0, 2}) {
for (const auto j : {-2, 0, 2}) {
const auto asteroidTranslation{asteroid.m_translation +
glm::vec2(i, j)};
const auto 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 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 205 a 217:
// Break asteroids marked as hit
for (const auto &asteroid : m_asteroids.m_asteroids) {
if (asteroid.m_hit && asteroid.m_scale > 0.10f) {
std::uniform_real_distribution<float> m_randomDist{-1.0f, 1.0f};
std::generate_n(std::back_inserter(m_asteroids.m_asteroids), 3, [&]() {
const glm::vec2 offset{m_randomDist(m_randomEngine),
m_randomDist(m_randomEngine)};
return m_asteroids.createAsteroid(
asteroid.m_translation + offset * asteroid.m_scale * 0.5f,
asteroid.m_scale * 0.5f);
});
}
}
Nesse 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
.
Nas linhas 219 a 220, os asteroides que estavam com m_hit == true
são removidos da lista:
Isso é tudo para a detecção de colisão. Vamos agora à definição de OpenGLWindow::checkWinCondition
, que ficará como a seguir:
void OpenGLWindow::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 OpenGLWindow::update
). Enquanto isso, paintUI
exibirá o texto de vitória.
bullets.hpp
A definição da classe Bullets
ficará como a seguir:
#ifndef BULLETS_HPP_
#define BULLETS_HPP_
#include <list>
#include "abcg.hpp"
#include "gamedata.hpp"
#include "ship.hpp"
class OpenGLWindow;
class Bullets {
public:
void initializeGL(GLuint program);
void paintGL();
void terminateGL();
void update(Ship &ship, const GameData &gameData, float deltaTime);
private:
friend OpenGLWindow;
GLuint m_program{};
GLint m_colorLoc{};
GLint m_rotationLoc{};
GLint m_translationLoc{};
GLint m_scaleLoc{};
GLuint m_vao{};
GLuint m_vbo{};
struct Bullet {
bool m_dead{};
glm::vec2 m_translation{glm::vec2(0)};
glm::vec2 m_velocity{glm::vec2(0)};
};
float m_scale{0.015f};
std::list<Bullet> m_bullets;
};
#endif
Nas linhas 32 a 36 é definida a estrutura Bullet
. Observe que o VAO e VBO não está em Bullet
, mas em Bullets
, pois todos os tiros utilizarão o mesmo VBO.
Na linha 38 é definido o fator de escala de cada tiro, e na linha 40 é definida a lista de tiros atualmente na cena. O número de elementos da lista será alterado de acordo com a quantidade de tiros visíveis.
bullets.cpp
O arquivo começa com a definição de Bullets::initializeGL
:
#include "bullets.hpp"
#include <cppitertools/itertools.hpp>
#include <glm/gtx/rotate_vector.hpp>
void Bullets::initializeGL(GLuint program) {
terminateGL();
m_program = 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");
m_bullets.clear();
// Create regular polygon
const auto sides{10};
std::vector<glm::vec2> positions(0);
positions.emplace_back(0, 0);
const auto step{M_PI * 2 / sides};
for (const auto 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);
// Get location of attributes in the program
const GLint positionAttribute{
abcg::glGetAttribLocation(m_program, "inPosition")};
// 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 15. 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::paintGL
ficará como a seguir:
void Bullets::paintGL() {
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 (const auto &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 59), ângulo de rotação (linha 60) e fator de escala (linha 61). A lista de tiros é iterada no laço das linhas 63 a 68. Cada tiro é renderizado como um GL_TRIANGLE_FAN
.
Em Bullets::terminateGL
é liberado o VBO e VAO:
void Bullets::terminateGL() {
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[static_cast<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
glm::vec2 forward{glm::rotate(glm::vec2{0.0f, 1.0f}, ship.m_rotation)};
glm::vec2 right{glm::rotate(glm::vec2{1.0f, 0.0f}, ship.m_rotation)};
const auto cannonOffset{(11.0f / 15.5f) * ship.m_scale};
const auto 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([](const Bullet &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 103, 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 107 a 116 são atualizadas as coordenadas de translação de cada tiro.
Nas linhas 108 e 109, os tiros são atualizados de acordo com a velocidade da nave e a velocidade do próprio tiro.
Nas linhas 111 a 115, verifica-se 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 saiu da tela.
Na linha 119, 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.
As estrelas das camadas superiores se moverão mais rapidamente do que as estrelas das camadas inferiores, dando a sensação de profundidade do espaço.↩︎
As declarações antecipadas são utilizadas no lugar do
#include
para evitar inclusões recursivas. Por exemplo,openglwindow.hpp
incluiship.hpp
, entãoship.hpp
não pode incluiropenglwindow.hpp
.↩︎O vetor é \([0\; 1]\) pois a nave está alinhada ao eixo \(y\) positivo em sua orientação original.↩︎