Bem vindo à sétima parte do nosso mini curso Criando um Game Completo, onde criamos uma versão do clássico Space Invaders compatível com Windows e Linux, utilizando aceleração de hardware para gráficos 2D, suporte a joysticks e uma tabela de scores online! Tudo isto compilando em Delphi e Lazarus.
Neste post iremos criar o menu inicial do game, definir a lógica de transição entre as telas e realizar os ajustes necessários na tela de gameplay para que o jogador possa voltar à tela inicial.
Menu Inicial
Nosso jogo precisa de uma tela inicial que permita que o jogador navegue por suas opções, inicie a jogatina e, no caso de um build para PC, encerre o software. Esta é a primeira tela com a qual o jogador se depara cada vez que o jogo é iniciado portanto, além de fornecer um mecanismo básico de navegação, temos a primeira oportunidade de definir o feeling do jogo.
Do que se trata este game? Qual a temática? Quem o produziu? Quando foi lançado? Todas estas perguntas podem ser respondidas se sua tela tiver um design adequado e, embora nosso foco aqui seja mais programação que game design, sabemos que nem sempre há um designer disponível para nos ajudar então, antes de partir para o código, vamos analisar um pouco os elementos desta tela:
Independentemente da natureza do game, estes elementos sempre estarão presentes em uma composição visual de qualidade:
- Logotipo
- Título
- Opções de navegação
- mesmo que seja tão simples como um "pressione x para começar"
- Imagem de contexto sendo, geralmente, uma destas opções
- personagem principal ou inimigo
- cena do game
- Plano de fundo animado
- nuvens se movendo, estrelas brilhando, algum efeito de parallax, etc..
Depois de incluir estes elementos em seu layout, utilizando um bom editor de imagens como o photoshop, o próximo passo é exportar as imagens individualmente e recriar a composição via código. Se você utilizar fontes diferentes, será necessário criá-las no TFontManager e adicioná-las na pasta de assets para que o motor as entenda.
Opções de Navegação
O menu inicial é uma cena, portanto, uma unit chamanda
scnMainMenu
deverá ser criada no diretório de cenas do projeto e a classe TMainMenuScene
precisa ser criada e registrada nas cenas do jogo. Para tanto, o método TSpaceInvaders.CreateScenes
será modificado.procedure TSpaceInvadersGame.CreateScenes; var gamePlay : TGamePlayScene; menu : TMainMenuScene; begin gamePlay := TGamePlayScene.Create(fPlayer); gamePlay.Name:= 'gamePlay'; Scenes.Add(gamePlay); menu := TMainMenuScene.Create; menu.Name:= 'mainMenu'; Scenes.Add(menu); {$IFDEF FPC} gamePlay.OnQuit := @doOnSceneQuit; menu.OnQuit := @doOnSceneQuit; {$ELSE} gamePlay.OnQuit := doOnSceneQuit; menu.OnQuit := doOnSceneQuit; {$ENDIF} Scenes.Current := menu; // define a cena inicial do game end;
Agora a cena do menu agora está sendo exibida, mas, por enquanto ainda é só uma tela preta. Vamos começar a preenchê-la desenhando as opções do menu, mas antes para mantar as coisas simples, uma classe para encapsular o comportamento deste menu pode ser criada. Veja.
TMenuOption = (moNewGame, moHighScore, moExit); { TMenu } TMenu = class private const YOFFSET = 30; //distância vertical entre as linhas public x, y: integer; // ponto de origem do menu selected : TMenuOption; constructor Create; procedure Draw; procedure SelectNext(const amount: integer); end; { implementation } constructor TMenu.Create; begin x := 540; y := 450; selected := moNewGame; //inicia com New Game selecionado end; procedure TMenu.Draw; var engine : TEngine; // as opções não selecionadas ficam um pouco esmaecidas (alpha = 44) function getAlpha(item : TMenuOption) :UInt8; begin if self.selected = item then result := 255 else result := 40; end; begin engine := TEngine.GetInstance; engine.Text.Draw('new game', x, y, engine.Fonts.MainMenu, getAlpha(moNewGame)); engine.Text.Draw('high score', x, y + YOFFSET, engine.Fonts.MainMenu, getAlpha(moHighScore)); engine.Text.Draw('exit', x, y + 2 * YOFFSET, engine.Fonts.MainMenu, getAlpha(moExit)); end; procedure TMenu.SelectNext(const amount: integer); begin TEngine.GetInstance.Sounds.Play(sndMenu); selected:= TMenuOption(Ord(selected) + amount); if Ord(selected) < 0 then selected:= TMenuOption(0); if selected > High(TMenuOption) then selected := TMenuOption(High(TMenuOption)); end;
Agora vamos declarar
fMenu
como uma variável privada da cena e ajustar as opções de acordo com o input do usuário (seta para cima e seta para baixo mudam a opção e enter
confirma a escolha, executando a ação equivalente). Para isto usaremos o método TMainMenuScene.doOnKeyUp
.procedure TMainMenuScene.doOnKeyUp(key: TSDL_KeyCode); begin inherited doOnKeyUp(key); if fInputEnabled then case key of SDLK_UP : SelectNext(-1); SDLK_DOWN : SelectNext(+1); SDLK_RETURN: begin case fMenu.selected of moNewGame: begin //evita que a opção seja selecionada mais de uma vez fInputEnabled := false; //toca um som para confiar a escolha TEngine.GetInstance.Sounds.Play(sndNewGame); //inicia o processo de fadout da cena fFader.FadeOut(0, FADE_OUT); //executa gotoNewGame em FADE_OUT milisegundos ExecuteDelayed(FADE_OUT, {$IFDEF FPC}@{$ENDIF}gotoNewGame); end; moHighScore: begin //not implemented yet TEngine.GetInstance.Sounds.Play(sndPlayerHit); end; moExit: begin doQuit(qtQuitGame, 0); end; end; end; end; end;
Agora basta invocar
fMenu.Draw
em TMainMenuScene.doOnRender
e o menu estará funcionando.Outros Elementos
procedure TMainMenuScene.doBeforeQuit; begin inherited; TEngine.GetInstance.Sounds.StopMusic( fMenuMusic ); end; procedure TMainMenuScene.doBeforeStart; begin inherited; TEngine.GetInstance.Sounds.PlayMusic( fMenuMusic, 1 ); fFader.FadeIn(0, FADE_IN); fState := stFadingIn; end; procedure TMainMenuScene.doFreeSounds; begin TEngine.GetInstance.Sounds.FreeMusic(fMenuMusic); end; procedure TMainMenuScene.doLoadSounds; begin fMenuMusic := TEngine.GetInstance.Sounds.LoadMusic(MENU_MUSIC); end; procedure TMainMenuScene.doLoadTextures; var engine : TEngine; begin engine := TEngine.GetInstance; engine.Textures.Clear; TEXTURE_LOGO := engine.Textures.Load('aeonsoft-small.png'); TEXTURE_PAWN := engine.Textures.Load('paw-small.png'); TEXTURE_GEAR := engine.Textures.Load('gear-small.png'); TEXTURE_MOON := engine.Textures.Load('moon.png'); end; constructor TMainMenuScene.Create; begin inherited; fMenu := TMenu.Create; fAlpha:= 0; fStars := TStarField.Create(400);; fFader := TFader.Create; fInputEnabled := true; end; destructor TMainMenuScene.Destroy; begin fMenu.Free; fStars.Free; fFader.Free; inherited Destroy; end; procedure TMainMenuScene.doOnRender(renderer: PSDL_Renderer); const DIVIDER_Y = 388; var src, dest : TSDL_Rect; engine: TEngine; begin engine := TEngine.GetInstance; renderer := engine.Renderer; fStars.Draw; fMenu.Draw; src.x := 0; src.y := 0; //draw moon src.w := engine.Textures[TEXTURE_MOON].W; src.h := engine.Textures[TEXTURE_MOON].h; dest.x := 500; dest.y := 76; dest.w := src.w; dest.h := src.h; SDL_SetTextureBlendMode(engine.Textures[TEXTURE_LOGO].Data, SDL_BLENDMODE_BLEND); SDL_SetTextureAlphaMod(engine.Textures[TEXTURE_MOON].Data, $FF); SDL_RenderCopy(renderer, engine.Textures[TEXTURE_MOON].Data, @src, @dest); //divider line SDL_SetRenderDrawColor(renderer, $FF, $FF, $FF, $FF); SDL_RenderDrawLine(renderer, 0, DIVIDER_Y, engine.Window.w, DIVIDER_Y); engine.Text.Draw('open', 280, 307, engine.Fonts.GUILarge, $FF); engine.Text.Draw('SPACE-INVADERS', 280, 347, engine.Fonts.GUILarge, $FF); engine.Text.Draw('Aeonsoft 2017 - An open source tribute to Taito''s classic', 280, 395, engine.Fonts.DebugNormal, 80); SDL_SetTextureBlendMode(engine.Textures[TEXTURE_LOGO].Data, SDL_BLENDMODE_BLEND); SDL_SetTextureAlphaMod(engine.Textures[TEXTURE_LOGO].Data, $FF); //gear src.w := engine.Textures[TEXTURE_GEAR].W; src.h := engine.Textures[TEXTURE_GEAR].H; dest.x := 164; dest.y := 294; dest.h := 102; dest.w := 90; SDL_RenderCopyEx(renderer, engine.Textures[TEXTURE_GEAR].Data, @src, @dest, fAngle, nil, SDL_FLIP_NONE); //pawn src.w := engine.Textures[TEXTURE_PAWN].W; src.h := engine.Textures[TEXTURE_PAWN].H; dest.x := dest.x+25; dest.y := dest.y+31; dest.h := 38; dest.w := 40; SDL_RenderCopy(renderer, engine.Textures[TEXTURE_PAWN].Data, @src, @dest); //logo src.w := engine.Textures[TEXTURE_LOGO].W; src.h := engine.Textures[TEXTURE_LOGO].H; dest.x := 225; dest.y := 368; dest.h := 40; dest.w := 40; SDL_RenderCopy(renderer, engine.Textures[TEXTURE_LOGO].Data, @src, @dest); case fState of stFadingIn, stFadingOut : begin expandToWindow(@dest); if fState = stFadingIn then SDL_SetRenderDrawColor(renderer, 0, 0, 0, $FF- fFader.Value) else SDL_SetRenderDrawColor(renderer, 0, 0, 0, fFader.Value); SDL_SetRenderDrawBlendMode(engine.Renderer, SDL_BLENDMODE_BLEND); SDL_RenderFillRect(renderer, @dest); end; end; end; procedure TMainMenuScene.doOnUpdate(const deltaTime: real); begin inherited doOnUpdate(deltaTime); fAngle := fAngle + 25 * deltaTime; fStars.Update( deltaTime ); fFader.Update( deltaTime ); end; procedure TMainMenuScene.expandToWindow(r: PSDL_Rect); begin r^.x :=0; r^.y :=0; r^.w := TEngine.GetInstance.Window.w; r^.h := TEngine.GetInstance.Window.h; end; procedure TMainMenuScene.gotoNewGame; begin doQuit(qtQuitCurrentScene, 0); fInputEnabled := true; end;
A unit
scnMainMenu
ficou bem simples e fácil de compreender. Nenhum objeto novo foi introduzido e ao criar uma nova cena com cerca de 300 linhas, começamos a perceber o benefício de ter criado a engine junto com o game.Aplicando a mesmas idéias aos menus da tela de gameplay, chegamos á primeira versão jogável do game. Confira como está o visual do game até aqui:
Com isto terminamos o primeiro tópico da parte 7.
No próximo post, vamos configurar fazer os ajustes para compilar o game para Linux e Windows a partir do mesmo projeto e, a partir daí, começaremos a criar a tela e a infra para uma tabela de pontos online, armazenada em um banco de dados nas nuvens.
Até lá!