TP2: Asteroids
Entrega: 01/10/2025 às 23:59h
Introdução
Assim como o Pong, o Asteroids, lançado pela Atari em 1979, também foi um dos jogos mais populares da era do Arcade. O Asteroids é um jogo de tiro com uma temática espacial onde o jogador controla uma nave com o objetivo de destruir todos os asteroides no mapa sem colidir com nenhum deles. Quando o jogador destrói todos os asteroides, um número maior do que o anterior é criado no mapa, aumentando a dificuldade do jogo. Se o jogador colidir com um asteroide, ele perde uma vida. O jogo termina quando o jogador perder suas três vidas. O vídeo abaixo mostra um gameplay do jogo original:
Objetivo
Nesse projeto, você irá desenvolver as mecânicas princiais de movimentação, colisão e tiro do Asteroids em C++ e OpenGL/SDL. Nessa versão, o jogador poderá mover-se e atirar com a nave como no jogo original, destruindo os asteroides caso um laser os acerte. O jogo será reiniciado quando a nave colidir com um asteroide. Inicialmente, você irá criar os um modelo de objetos híbrido, com hierarquia de classes e componentes. Em seguida, você irá implemantar os componentes RigidBodyComponent e CircleColliderComponent para movimentar e detectar as colisões dos objetos do jogo. Por fim, você irá utilizar esses componentes para implementar uma nave que atira raios laser e gerar um dado número de asteroides com geometrias aleatórias. O video a seguir mostra um gameplay da versão que você irá implementar:
Código-base
Aceite o projeto tp2-asteroids no GitHub classroom [nesse link]
Clone o seu novo repositório no seu computador:
# Substitua <GITHUB_USERNAME> pelo seu usuário do GitHub git clone https://github.com/ufmg-dcc192/tp2-asteroids-<GITHUB_USERNAME>.gitAbra o projeto tp2-asteroids na CLion e, antes de começar a sua implementação, verique com cuidado as definições de métodos e atributos de cada classe:
Component
Classe base para todos os componentes do jogo, contendo métodos para processamento de eventos de entrada e atualização.
DrawComponent
Componente de desenho de objetos com retângulos coloridos.
RigidBodyComponent
Componente de movimentação de objetos rígidos.
CircleColliderComponent
Componente de detecção de colisão baseado em comparações entre círculos.
ParticleSystemComponent
Componente para emissão de partículas, que pode ser usado para atirar os lasers.
Ship
Classe que estende
Actorpara representar a nave.Asteroid
Classe que estende
Actorpara representar um asteroide.Laser
Classe que estende
Actorpara representar uma partícula de laser, que é atirada pela nave quando o jogador pressiona a tecla Espaço.
Observação: o código base desse projeto foi construído a partir do código do projeto anterior [TP1: Pong], portanto muitas das classes já foram introduzidas anteriormente.
Instruções
Parte 1: Modelo de Objetos
Na primeira parte, você irá implementar uma estrutura de objetos com hierarquia de classes e componentes, o que possibilitará a criação e gerenciamento de objeto de uma maneira mais flexível.
Actor.cpp
Implemente o construtor
Actorpara adicionar ao jogo o ator que está sendo criadoImplemente o destrutor
~Actorpara retirar o ator da lista de atores do jogo. Em seguida, percorra o vetor de componentes, deletando-os um a um, e finalize esvaziando esse vetor para evitar vazamentos de memória.Complete o método
Updatepara, quando o ator estiver ativo, chamar o update dos seus componentesComplete o método
ProcessInputpara, quando o ator estiver ativo, chamar o processador de entrada dos seus componentesImplemente o método
AddComponentpara registrar novos componentes em um ator. Primeiro, adicione o componente ao final do vetor de componentes. Em seguida, ordene esse vetor para garantir que os componentes fiquem organizados conforme seuupdateOrder, de modo que sejam atualizados na ordem correta durante a execução do jogo.
Game.cpp
Implemente a função
AddActorpara adicionar novos atores ao jogo. Caso mUpdatingActors esteja ativo, insira o ator em mPendingActors; caso contrário, adicione-o diretamente em mActorsImplemente a função
RemoveActorpara remover um ator tanto de mPendingActors quanto de mActors. Para evitar cópias desnecessárias, use std::iter_swap para trocar o elemento encontrado com o último da lista e depois remova-o com pop_back().Implemente a função
UpdateActorsativando a flagmUpdatingActorsantes de atualizar cada ator e desativando-a ao final do loop, garantindo que novos atores criados sejam armazenados emmPendingActors. Depois, mova todos os atores pendentes para mActors e usemPendingActors.clear()para esvaziar a lista. Por último, crie um vetor temporáriodeadActorscom os atores em estadoDestroye, em seguida, percorra esse vetor deletando cada ator para liberar a memória corretamente.Implemente a função
AddDrawablepara adicionar um novoDrawComponentà lista de desenháveis. Em seguida, ordene a lista usandostd::sort, garantindo que os componentes fiquem organizados pelo valor deGetDrawOrder().Implemente a função
RemoveDrawablepara localizar oDrawComponentna lista de desenháveis usandostd::finde removê-lo comerase.Complete a parte final de
ProcessInputpercorrendo todos os atores emmActorse chamandoProcessInput(state)para cada um, usando o estado de teclado obtido emSDL_GetKeyboardState.Implemente a função
GenerateOutputpara desenhar todos os elementos da cena. Entre oRenderer::Cleare oRenderer::Present, percorra a listamDrawableschamandoDrawpara cada componente, e casomIsDebuggingesteja ativo, percorra também os componentes do ator dono chamandoDebugDrawem cada um. Dessa forma, os elementos são renderizados normalmente e, em modo de depuração, são exibidas informações extras sobre seus componentes.No inicío do método
Shutdown, percorra a lista de atores do jogo, deletando todos eles
DrawComponent.cpp
Implemente o construtor
DrawComponent, adicionando o componente que está sendo criado como desenhável no jogoGame::AddDrawable. Em seguida, crie os arrays de vértices e índices a partir do vetor de pontos fornecidovertices. Por fim, inicializemDrawArraycom os dados construídos.Implemente o destrutor
~DrawComponentpara remover o componente da lista de desenháveis do jogo. Libere a memória demDrawArraye atribuanullptrpara evitar ponteiros soltos.Implemente o método
Drawpara desenhar o componente chamando o métodoDrawdo Renderer. Use a matriz de modelo do ator dono, os dados de vérticesmDrawArraye a cor branca como parâmetros.
Ao final dessa primeira etapa, teste a sua implementação criando um actor com um DrawComponent na função Game::Initialize(). Por exemplo, experimente criar um actor representado por um triângulo com o código abaixo:
std::vector<Vector2> vertices;
vertices.emplace_back(Vector2(0.0f, -50.0f));
vertices.emplace_back(Vector2(-50.0f, 50.0f));
vertices.emplace_back(Vector2(50.0f, 50.0f));
Actor *testActor = new Actor(this);
testActor->SetPosition(Vector2(WINDOW_WIDTH / 2.0f, WINDOW_HEIGHT / 2.0f));
new DrawComponent(testActor, vertices);
O seu jogo deveria mostrar a seguinte saída:

Parte 2: Movimentação de Objetos Rígidos
Na segunda parte, você irá implementar os componentes RigidBodyComponent e CircleColliderComponent para movimentar e detectar as colisões dos objetos do jogo.
RigidBodyComponent.cpp
Implemente a função
ApplyForcepara atualizar a aceleração do corpo rígido somando a força recebida dividida pela massa (force / mMass).Implemente a função
Updatepara integrar a física do corpo rígido: atualize a velocidade a partir da aceleração (limitando-a ao máximo), use essa velocidade para mudar a posição aplicando screen wrap, e depois zere a aceleração. Corrija velocidades muito pequenas para zero e, por fim, atualize a rotação do ator somando a velocidade angular vezes o delta de tempo, de forma análoga ao cálculo da translação, mas aplicado ao ângulo.Implemente a função
ScreenWrappara garantir que o ator reapareça do lado oposto da tela quando sair dos limites. Caso a coordenadaxseja menor que 0, defina comoWINDOW_WIDTH, e se for maior queWINDOW_WIDTH, defina como 0 (o mesmo vale parayem relação aWINDOW_HEIGHT). Isso cria o efeito de teletransporte contínuo típico do jogo Asteroids.
CircleColliderComponent.cpp
Implemente o construtor
CircleColliderComponentgerando os vértices que formam um círculo aproximado com 10 pontos igualmente espaçados, usando seno e cosseno para calcular as coordenadas. Depois, crie os índices que conectam os vértices em pares sequenciais e inicializemDrawArraycom esses dados para desenhar a malha do círculo. Esses vértices serão utilizados para visualizar a geometria de colisão desse componente, para fins de depuração.Implemente o destrutor
~CircleColliderComponentpara liberar a memória usada emmDrawArray. Após odelete, atribuanullptrao ponteiro para evitar acessos inválidos.Implemente a função
Intersectpara verificar se dois círculos colidem, como visto em sala de aula.Implemente o método
DebugDrawpara desenhar graficamente o colisor circular, facilitando a depuração. Para isso, chame o métodoDrawdorenderer, passando a matriz de modelo do ator, omDrawArraye a cor verde.
Ao final dessa segunda etapa, teste a sua implementação adicionando componentes RigidBodyComponent e CircleColliderComponent ao actor de criado ao final da etapa anterior. Aplique um força qualquer a esse actor, como no exemplo abaixo:
RigidBodyComponent *rb = new RigidBodyComponent(testActor, 1.0f);
CircleColliderComponent *cc = new CircleColliderComponent(testActor, 20.0f);
rb->ApplyForce(Vector2(0.0f, 1000.0f));
O seu jogo deveria mostrar uma saída como esta:
Parte 3: Objetos do Asteroids
Na terceira parte, você irá utilizar os novos componentes para implementar uma nave que atira raios laser e gerar um dado número de asteroides com geometrias aleatórias.
Ship.cpp
Implemente o construtor de
Shippara construir um triângulo de vértices para representar a nave e use-o para criar oDrawComponent, além de adicionar umRigidBodyComponente umCircleColliderComponentpara física e colisão.Implemente a função
OnProcessInputpara processar as teclas de controle da nave. Use W para aplicar uma força na direção para frente, A e D para ajustar a velocidade angular negativa ou positiva, respectivamente.Implemente a função
OnUpdatepara atualizar o estado da nave a cada quadro. Primeiro, diminua o tempo de recarga do laser (mLaserCooldown) pelodeltaTime. Em seguida, percorra todos os asteroides do jogo e, se o colisor circular da nave intersectar o colisor de algum asteroide, finalize o jogo chamandoQuit().
Teste o seu código instanciando um objeto do tipo Ship no método Game::InitializeActors. Você deveria ser capaz de mover a nave com as teclas W, A e D, como no vídeo abaixo:
Asteroid.cpp
Implemente a função
GenerateVerticespara criar os vértices que formam a silhueta irregular de um asteroide. Para isso, distribuanumVerticespontos igualmente espaçados ao longo da circunferência (incrementando o ângulo a cada iteração) e, em cada ponto, calcule um comprimento aleatório. Use esse comprimento para gerar as coordenadas(x, y)e adicione-as ao vetor de vértices, que será retornado ao final.Implemente a função
CalculateAverageVerticesLengthpara calcular o raio médio de um polígono.Implemente a função
GenerateRandomStartingForcepara criar uma força inicial aleatória que movimenta o asteroide.Implemente o construtor de
Asteroidgerando os vértices de um polígono irregular comnumVerticese raioradius, calcule o comprimento médio desses vértices para usar como raio de colisão e crie os componentesDrawComponent,RigidBodyComponenteCircleColliderComponent. Por fim, aplique uma força inicial aleatória ao corpo rígido para movimentar o asteroide e registre-o no jogo comAddAsteroid(this).Implemente o destrutor
~Asteroidpara remover esse arteroide da lista de asteroides do jogo. Isso garante que, ao ser destruído, ele não continue registrado no sistema de atualização e colisão.
Teste o seu código instanciando múltiplos (ex. 5) objetos do tipo Asteroid no método Game::InitializeActors. O jogo deveria ser fechado quando a nave colidir com um dos asteroids, como no vídeo abaixo:
ParticleSystemComponent.cpp
Implemente o construtor de
Particlecriando os componentesDrawComponent,RigidBodyComponenteCircleColliderComponentpara que a partícula tenha desenho, física e colisão. Por fim, configure o estado inicial da partícula como pausado, torne-a invisível e marquemIsDeadcomo verdadeiro até que seja ativada pelo sistema de partículas.Implemente o método
Particle::Killpara desativar a partícula quando sua vida terminar. MarquemIsDeadcomo verdadeiro, pause o ator e torne oDrawComponentinvisível. Por fim, reinicie sua velocidade para zero noRigidBodyComponent, garantindo que não continue em movimento após ser “morta”.Implemente o método
Particle::Awakepara reativar a partícula quando for emitida. Defina o tempo de vida (mLifeTime) recebido, marquemIsDeadcomo falso e mude o estado para ativo, tornando oDrawComponentvisível. Por fim, ajuste a posição e a rotação da partícula conforme os parâmetros recebidos.Implemente o método
Particle::OnUpdatepara controlar o ciclo de vida da partícula. Primeiro, reduza o tempo restante (mLifeTime) e, se ele chegar a zero ou menos, chameKill()para desativar a partícula. Caso contrário, percorra todos os asteroides do jogo e, se houver colisão do colisor circular da partícula com o de algum asteroide, finalize a partícula chamandoKill()e marque o asteroide como destruído (SetState(ActorState::Destroy)).Implemente o construtor
ParticleSystemComponentchamando o construtor da classe baseComponent. Em seguida, crie um pool de partículas: para cada índice atépoolSize, instancie uma novaParticlecom os vértices fornecidos e adicione-a ao vetormParticles. Isso garante que o sistema tenha um conjunto fixo de partículas disponíveis para emissão durante o jogo.Implemente o método
ParticleSystemComponent::EmitParticlepara ativar uma partícula inativa do pool. Para isso, percorra o vetormParticlese, ao encontrar a primeira morta, acorde-a (Awake) na posição do ator dono somada aooffsetPosition, usando a rotação atual e o tempo de vida fornecido. Em seguida, aplique uma força na direção do ator multiplicada pela velocidade, e encerre o loop para emitir apenas uma partícula por chamada.
Para testar o seu sistema de partículas, modifique o método Ship::OnProcessInput para verificar se a tecla espaço for pressionada. Caso esteja, verique o tempo de recarga, se ele estiver zerado, dispare um projétil pelo sistema de partículas e reinicie o mLaserCooldown. Essa etapa conclui as implementações básicas do jogo, que deveria ter um resultado como o mostrado no vídeo do início do roteiro.
Parte 4: Customização
Na quarta, e última etapa, você irá ajustar as variáveis do jogo para criar uma versão única do Asteroids.
Ajuste os parâmetros do jogo da forma que achar mais interessante: tamanhos, cores, aceleração, velocidade escalar, velocidade máxima, velocidade mínima, massa. Garante que o jogador terá um tempo razoável para escapar dos asteroides no início do jogo.
Implemente a a funcionalidade de gerar três novos asteroides menores e mais rápidos quando um grande é destruído.
Modifique o gráfico da nave para que ela parece mais uma letra ‘A’, do que um triângulo e adicione um propulsor na parte traseira, que pisca quando você acelera a nave (veja o vídeo do jogo original).
Implemente a animação de destruição dos asteroides. Quando um asteroide for destuído, crie um objeto com um sistema de partículas onde cada partícula é um ponto que é atirado para uma direção aleatória e vive por poucos segundos.
Submissão
Para submeter o seu trabalho, basta fazer o commit e o push das suas alterações no repositório que foi criado para você no GitHub classroom.
Observação: você pode realizar quantos commits quiser. Isso é inclusive recomendado para dividir o problema em partes menores. Além disso, as mensagens nos commits não precisam ser necessariamente “Submissão TP2”. Você pode também criar branches se quiser, mas apenas o último commit da branch main será avaliado.
git add .
git commit -m 'Submissão TP2'
git push
Barema
- Parte 1: Desenhos Vetoriais (10%)
- Parte 2: Movimentação de Objetos Rígidos (20%)
- Parte 3: Objetos do Asteroids (20%)
- Parte 4: Customização (50%)