TP4: Super Mario Bros 1-2
Entrega: 16/06/2025 às 23:59h
Introdução
No último trabalho prático (TP3), você implementou as mecânicas básicas de correr, pular, e acertar inimigos a primeira fase do Super Mario Bros (SMB). Para isso, você teve que desenvolver os sistema de câmera, colisão e animação, além de um parser de levels construídos no Tiled. Nesse TP4, você irá expandir esse projeto para incluir o menu principal e a segunda fase do jogo. Além disso, o seu jogo agora terá um Heads-Up Display (HUD), música e efeitos sonoros.
Objetivo
O objetivo desse projeto é praticar a implementação de gerenciadores de cenas, HUD e sistemas de áudio em jogos. Primeiro, você irá implementar telas de UI e seus elemetos básicos, como textos, botões e imagens. Em seguida, você irá implementar um gerenciador de cenas como uma máquina de estados finita usando if/else (ao invés de switch). Depois, você irá utilizar a estrutura de UI criada para construir o HUD do jogo. Por fim, você irá implementar um sistema para gerenciar um conjunto finito de canais de áudio da SDL_Mixer. O vídeo a seguir mostra um gameplay da versão que você irá implementar:
Código-base
Aceite o projeto tp4-super-mario-bros-1-2 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/tp4-super-mario-bros-1-2-<GITHUB_USERNAME>.git
Abra o projeto tp4-super-mario-bros-1-2 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:
UIScreen
Classe base para criação de telas de interface, como menus e HUD.
UIElement
Classe abstrata base para implementação de elementos de UI. Ela define atributos básicos de posição, tamanho e cor, que podem ser usados para desenhar elementos de interface. Essa classe deve ser estendida quando um novo elemento de UI for criado.
UIFont
Classe auxiliar para carregar e descarregar fontes em formato true type.
UIText, UIButton, UIImage
Classes que estendem UIElement para implementar três elementos de interface básicos: texto, botões e imagens.
HUD
Classe que estende UIScreen para implementar o HUD do jogo, condendo uma contagem de pontos, fase e tempo.
AudioSystem
Classe para gerenciamento de sons, permitindo tocar, pausar, resumir e parar sons armazenados em arquivos.
SpatialHashing
Classe que implementa um Hash Spacial para otimização de renderização e colisão. Você não irá trabalhar nessa classe, ela foi adicionada para possibilitar a criação de jogos maiores nos projetos finais.
Observação: o código base desse projeto foi construído a partir do anterior [TP3: Super Mario Bros 1-1], assim. As classes restantes já foram introduzidas anteriormente.
Instruções
Parte 1: Menu Principal
Na primeira parte, você irá construir o menu principal do jogo e, para isso, terá que implementar um sistema de UI simples que suporte textos, botões e imagens.
Parte 1-1: Textos
Vamos começar nossa implementação pelos elementos de texto. Os botoões e a imagens seguirão uma ideia similar.
Font.cpp
Implemente o método
Load()
para carregar um mapa de fontes true type considerando uma lista de tamanhos de fonte suportados.Implemente o méodo
Unload()
para descarregar todas as fontes que estão carregadas no mapa de fontes.Complete o método
RenderText()
para renderizar uma string em uma textura usando a fonte carregada.
UIText.cpp
Objetos
UIText
são utilizados para desenhar textos de UI. Eles estendem a class baseUIElement
com um atributostd::string mText
, que contém o texto que será mostrado, e outroSDL_Texture *mTextTexture
, que representa uma textura com esse texto e um atributoUIFont* mFont
com a fonte que será utilizada para renderizarmTextTexture
. Esses atributos serão utilizados para gerenciar e desenhar textos em telas de UI (UIScreens
).Implemente o construtor da classe chamando a função
SetText
para atualizar a textura do texto.Implemente o método
SetText(const std::string &text)
para modificar tanto a stringmText
quanto a texturamTextTexture
. Nessa etapa, você irá utilizar os métodos da classeFont.cpp
implementados.Implemente o método
Draw
para desenhar o texto em uma determina posição relativa na tela.
Game.cpp
- Implemete o método
LoadFont()
para criar objetos do tipoFont
ao jogo. As fontes carregas serão armazenadas em um dicionário, para que, quando elas forem utilizadas novamente por elementos de interface, elas sejam recuperadas diretamente da memória primária, e não da secundária.
- Implemete o método
UIScreen.cpp
Implemente o construtor da classe para adicionar o novo objeto à lista de telas do jogo e carregar a fonte que será utilizada na UI da mesma.
Implemente o destrutor para destruir os textos que foram adicionados à tela.
Implemente o método
Draw()
para desenhar os textos.Implemente o método
AddText()
para adicionar elementos de texto à tela
Ao final dessa etapa, você deveria ser capaz de criar uma tela com o construtor UIScreen
e adicionar textos a ela. Para testar o seu código, adicione o seguinte trecho ao método Game::LoadMainMenu()
:
auto mainMenu = new UIScreen(this, "../Assets/Fonts/SMB.ttf");
mainMenu->AddText("Super Mario Bros", Vector2(170.0f, 50.0f), Vector2(300.0f, 30.0f), 60);
Você deveria ver uma tela conforme a imagem a seguir:
Parte 1-2: Botões
Um botão pode ser visto como um UIText dentro de uma área retângular que pode ser pressionada via mouse ou teclado. Por isso, a classe UIButton
possui um atributo UIText mText
e outro std::function<void()> mOnClick
. O texto mText
será desenhado dentro do botão, que quando for pressionado, chamará a função apontada por mOnClick
.
UIButton.cpp
Implemente o método
Draw
para desenhar o texto do botão e o seu fundo caso ele esteja selecionado.Implemente o método
OnClick
para chamar a função do botão.
UIScreen.cpp
Complemente o código do destrutor para destruir os botões que foram adicionados à tela.
Complemente o método
Draw()
para desenhar os botões.Implemente o método
HandleKeyPress()
para navegar na lista de botões com o teclado.Implemente o método
AddButton()
para adicionar botões à tela
Ao final dessa etapa, você deveria ser capaz de adicionar botões às tela. Para testar o seu código, adicione o seguinte trecho ao método Game::LoadMainMenu()
abaixo da linha que adicionou na etapa anterior para criar um texto:
auto button1 = mainMenu->AddButton("1 Player", Vector2(mWindowWidth/2.0f - 100.0f, 200.0f), Vector2(200.0f, 40.0f),
nullptr);
auto button2 = mainMenu->AddButton("2 Players", Vector2(mWindowWidth/2.0f - 100.0f, 250.0f), Vector2(200.0f, 40.0f),
nullptr);
Assumindo que você manteve o texto criado na etapa anterior, você deveria ver uma tela conforme a imagem a seguir:
Parte 1-3: Imagens
Carregar uma imagem de UI é similar à carregar um texto, pois também envolve criar uma textura, mas dessa vez a partir de um arquivo de imagem armazenado na memória secundária, ao invés de uma fonte true type.
- UIImage.cpp
Implemente o construtor da classe para carregar uma textura que será desenhada por essa imagem.
Implemente o destrutor da classe para destruir a textura que foi carregada no construtor.
Implemente o método
Draw()
para desenhar a textura na tela.
- UIScreen.cpp
Complemente o código do destrutor para destruir as imagens que foram adicionados à tela.
Complemente o método
Draw()
para desenhar as imagens.Implemente o método
AddImage()
para adicionar imagens à tela.
Ao final dessa etapa, você deveria ser capaz de criar imagens nas telas de UI. Para testar o seu código, susbtitua o código que carrega o texto pelo que carrega a imagem:
const Vector2 titleSize = Vector2(178.0f, 88.0f) * 2.0f;
const Vector2 titlePos = Vector2(mWindowWidth/2.0f - titleSize.x/2.0f, 50.0f);
mainMenu->AddImage("../Assets/Sprites/Logo.png", titlePos, titleSize);
Ajustando a posição dos botões criados na etapa anterior para que eles fiquem abaixo da imagem, você deveria ver uma saída como a do vídeo a seguir:
Parte 2: Gerenciamento de Cenas
Na segunda parte, você irá desenvolver um gerenciador de cenas para possibilitar descagarregamento e carregamento de game objects durante o jogo. Lembre-se que gerenciadores de cenas são implementados como uma máquina de estados finita, que por simplicidade será incluída diretamente na classe Game
.
Quando a função SetGameScene
é chamada, ela irá alterar um estado mSceneManagerState
da máquina para SceneManagerState::Entering
e salva o tempo de transição. Assim, durante o update, o jogo poderá verificar esse estado e, utilizando um contador, verificar quando o tempo de transição tiver passado.
- Game.cpp
Implemente o método
SetGameScene(Game::GameScene scene, float transitionTime)
para mudar o estado da máquina de estados paraSceneManagerState::Entering
e armazenar a cena destinoGame::GameScene scene
passada como parâmetro. Essa função também possui um parâmetrotransitionTime
para controlar em quanto tempo a transição irá ocorrer. Controlar o tempo de transição é útil em momentos que você precisa esperar uma animação ocorrer antes de fazer a transição, como quando o Mario morre ou passa de fase.Implemente o método
UpdateSceneManager
para atualizar a máquina de estados do gerenciador de cenas. Note que cada estado da máquina possui um tempo de transição. Isso é útil para fazer transições menos abruptas durante o jogo. É importante destacar também que quando a máquina estiver no estadoSceneManagerState::Active
e o tempo de transição tiver decorrido, ela irá chamar o métodoGame::ChangeScene()
, que faz a troca de cenas efetivamente.No método
UpdateGame
, chame o médotoUpdateSceneManager
que você implementou no item anterior.No método
GenerateOutput
, desenhe um retângulo preto nos momentos de transição de cena, ou sejam quando o gerenciador de cenas estiver no estado ativo.No método
LoadMainMenu
, altere a criação do botãoPlayer 1
para chamar o métodoSetGameScene
quando ele for pressionado, passando a cena GameScene::Level1 como parâmetro:mainMenu->AddButton("1 Player", button1Pos, buttonSize, [this]() { SetGameScene(GameScene::Level1)});
Ao final dessa etapa, você deveria ser capaz de fazer transições de cenas com o método SetGameScene
, como mostrado abaixo:
Parte 3: HUD
Agora que você tem como criar telas de UI com a classe UIScene
, você pode utilizá-la para criar um HUD. Basta estender essa classe e adicionar textos, imagens e botões conforme necessidade. No caso do Super Mario Bros., o HUD é composto apenas de textos, a não ser o ícone das moedas, que é uma pequena imagem. Nessa seção, vamos implementar apenas o contador de pontos, o título da fase e o contador de tempo.
- HUD.cpp
Implemente o construtor da classe para criar os textos do contador de score, título da fase e contador de tempo do HUD.
Implemente o método
SetTime(int time)
para alterar o texto do contador de tempo para um certo valor inteirotime
passado como parâmetro.Implemente o método
SetLevelName(const std::string &levelName)
para alterar o nome da fase para uma certa stringlevelName
passada como parâmetro.
- Game.cpp
Complete o método
ChangeScene
para criar um HUD quando a próxima cenamNextScene
for oLevel1
ouLevel2
Complete o método
UpdateGame
para chamar a funçãoUpdateLevelTime
, que irá atualizar o contador de tempo do HUD.Implemente o método
UpdateLevelTime
para atualizar o tempo de jogo quando o jogo estiver em qualquer cena que não seja o menu principal e não estiver pausado. Essa função deve matar o Mario caso o tempo tenha acabado.
Ao final dessa etapa, você deveria ver um HUD tanto na primeira quanto na segunda fase do jogo, como mostrado no vídeo abaixo:
Parte 4: Sistema de Áudio
Agora vamos construir um sistema de áudio para tocar músicas de fundo e efeitos sonoros. Esse sistema será implementado na classe AudioSystem
e seguirá as mesmas ideias vistas na Aula 16.
- AudioSystem.cpp
Implemente o construtor da classe para inicializar a SDL_mixer e alocar o número de canais de áudio passados como parâmetro.
Implemente o destrutor da classe para desalocar os sons carregados.
Implemente o método
Update
para verificar quais sons ainda estão tocando, para liberar canais que não tenham mais sons em reprodução.Implemente o método
PlaySound(const std::string& soundName, bool looping)
para tocar um determinado som passado como parâmetro.
O parâmetrosoundName
é o nome do arquivo de áudio armazenado no diretórioAssets/Sounds
. Note não é preciso adicionar esse prefixo quando for tocar um som, pois o próprio sistema de som já faz isso para você. O parâmetrolooping
controla se o som tocará repetidamente ou não. Lembre-se que quando esse método for chamado, pode ser que todos os canais estão ocupados, então temos que aplicar uma política para parar sons atualmente em reprodução. Usaremos a mesma política definida na Aula 16.Implemente o método
StopSound(SoundHandle sound)
para parar o som definido peloSoundHandle sound
Implemente o método
PauseSound(SoundHandle sound)
para pausar o som definido peloSoundHandle sound
Implemente o método
Resume(SoundHandle sound)
para retomar o som definido peloSoundHandle sound
- Game.cpp
Instancie um
AudioSystem
no métodoInitialize
Complete o método
ChangeScene
para tocar músicas de fundo nas fases 1 e 2Complete o método
TogglePause
para tocar um efeito sonoro quando o jogo for pausado, parando a música de fundo. De forma inversa, quando jogo for retomado, toque um efeito sonoro e retome a música de fundo.
Mario.cpp
Todos os efeitos sonoros de interação do Mario com objetos do jogo, como inimigos e moedas, serão implementados na classe do Mario. Utilize o AudioSystem implementado para tocar os sons necessários (vide os TODOs desse arquivo).
Ao final dessa última etapa, você deveria ouvir as músicas de fundo da primeira e segunda fase, bem como os efeitos sonoros de ações do Mario (pular, colisão com bloco, matar goomba, etc.) e o feedback sonoro de pause, como no vídeo do início do roteiro.
Parte 5: Customização
Na quinta, e última etapa, você irá ajustar as variáveis do jogo para criar uma versão única do Super Mário Bros.
Modifique a tela do menu principal (logo, botões, background, etc…) para que ele fique o mais parecido com a do jogo original.
Adicione moedas coletáveis nas fases, incluindo um contador no HUD para contar o número de moedas coletadas. Sugestão, crie um novo actor para itens coletáveis, como moedas e power ups.
Adicione um efeito cross-fade (fade our + fade in) no gerenciador de cenas.
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.
git add .
git commit -m 'Submissão TP4'
git push
Barema
- Parte 1: Menu Principal (20%)
- Parte 2: Gerenciamento de Cenas (20%)
- Parte 3: HUD (20%)
- Parte 4: Sistema de Áudio (20%)
- Parte 5: Customização (20%)