Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Criando um Game Completo - Parte 7


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:
  1. Logotipo
  2. Título
  3. 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..
    Se achar conveniente, uma mensagem curta e discreta de copyright e ano de lançamento também podem ser adicionadas, como o fizemos aqui. Além destes elementos, um pequeno jingle ou riff pode ser usado para dar um toque final ao seu menu. Assumindo que você esteja lendo em um navegador moderno, poderá ouvir o som que usaremos logo abaixo.


    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


    Para concluir a cena, precisamos desenhar os textos e imagens restantes além de tocar o riff e desenhar as estrelas ao fundo. Para nossa sorte, a maior parte do trabalho pesado já foi feito e podemos reaproveitar muita coisa. As estrelas, por exemplo: podemos utilizar a mesma classe que desenvolvemos na parte 6 desta série. O texture manager também está pronto, então basta carregar as imagens do disco e desenhá-las na posição correta. O mesmo vale para o gerenciador de som. Então tudo o que nos resta fazer é observar as coordenadas de cada objeto no photoshop e escrever o código equivalente.

    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

    Share the post

    Criando um Game Completo - Parte 7

    ×

    Subscribe to Computação Gráfica E Jogos Em Pascal

    Get updates delivered right to your inbox!

    Thank you for your subscription

    ×