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"
- personagem principal ou inimigo
- cena do game
- nuvens se movendo, estrelas brilhando, algum efeito de parallax, etc..
Seu navegador não suporta áudio HTML5.
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) 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á!
Links
- Código fonte - GitHub
- Código fonte - Download direto
- Executável - Win32
This post first appeared on Computação Gráfica E Jogos Em Pascal, please read the originial post: here