Compartilhar via


Gerenciamento de fluxo de jogo

Observação

Este tópico faz parte da série de tutoriais Criar um jogo simples da Plataforma Universal do Windows (UWP) com DirectX. O tópico nesse link define o contexto da série.

O jogo agora tem uma janela, registrou alguns manipuladores de eventos e carregou ativos de forma assíncrona. Este tópico explica o uso de estados de jogo, como gerenciar estados-chave específicos do jogo e como criar um loop de atualização para o mecanismo de jogo. Em seguida, aprenderemos sobre o fluxo da interface do usuário e, por fim, entenderemos mais sobre os manipuladores de eventos necessários para um jogo UWP.

Estados de jogo usados para gerenciar o fluxo do jogo

Usamos os estados do jogo para gerenciar o fluxo do jogo.

Quando o jogo de exemplo Simple3DGameDX é executado pela primeira vez em uma máquina, ele está em um estado em que nenhum jogo foi iniciado. Nas vezes subsequentes em que o jogo é executado, ele pode estar em qualquer um desses estados.

  • Nenhum jogo foi iniciado ou o jogo está entre os níveis (a pontuação mais alta é zero).
  • O loop do jogo está em execução e está no meio de um nível.
  • O loop do jogo não está em execução devido à conclusão de um jogo (a pontuação mais alta tem um valor diferente de zero).

Seu jogo pode ter quantos estados forem necessários. Mas lembre-se de que ele pode ser encerrado a qualquer momento. E quando ele for retomado, o usuário espera que ele seja retomado no estado em que estava quando foi encerrado.

Gerenciamento de estado do jogo

Portanto, durante a inicialização do jogo, você precisará dar suporte à inicialização a frio do jogo, bem como retomar o jogo após interrompê-lo durante o voo. O exemplo Simple3DGameDX sempre salva seu estado de jogo para dar a impressão de que ele nunca parou.

Em resposta a um evento de suspensão, o jogo é suspenso, mas os recursos do jogo ainda estão na memória. Da mesma forma, o evento de retomada é manipulado para garantir que o jogo de exemplo seja retomado no estado em que estava quando foi suspenso ou encerrado. Dependendo do estado, diferentes opções são apresentadas ao jogador.

  • Se o jogo for retomado no nível médio, ele aparecerá pausado e a sobreposição oferecerá a opção de continuar.
  • Se o jogo for retomado em um estado em que o jogo foi concluído, ele exibirá as pontuações mais altas e uma opção para jogar um novo jogo.
  • Por fim, se o jogo for retomado antes do início de um nível, a sobreposição apresentará uma opção de início ao usuário.

O jogo de exemplo não distingue se o jogo está iniciando a frio, iniciando pela primeira vez sem um evento de suspensão ou retomando de um estado suspenso. Esse é o design adequado para qualquer aplicativo UWP.

Neste exemplo, a inicialização dos estados do jogo ocorre em GameMain::InitializeGameState (uma estrutura de tópicos desse método é mostrada na próxima seção).

Aqui está um fluxograma para ajudá-lo a visualizar o fluxo. Ele abrange a inicialização e o loop de atualização.

  • A inicialização começa no nó Iniciar quando você verifica o estado atual do jogo. Para obter o código do jogo, consulte GameMain::InitializeGameState na próxima seção.
  • Para obter mais informações sobre o loop de atualização, acesse Atualizar mecanismo de jogo. Para o código do jogo, vá para GameMain::Update.

a máquina de estado principal para o nosso jogo

O método GameMain::InitializeGameState

GameMain::InitializeGameState é chamado indiretamente por meio do construtor da classe GameMain , que é o resultado de criar uma instância GameMain dentro de App::Load.

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) : ...
{
    m_deviceResources->RegisterDeviceNotify(this);
    ...
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    ...
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();
    ...
}

void GameMain::InitializeGameState()
{
    // Set up the initial state machine for handling Game playing state.
    if (m_game->GameActive() && m_game->LevelActive())
    {
        // The last time the game terminated it was in the middle
        // of a level.
        // We are waiting for the user to continue the game.
        ...
    }
    else if (!m_game->GameActive() && (m_game->HighScore().totalHits > 0))
    {
        // The last time the game terminated the game had been completed.
        // Show the high score.
        // We are waiting for the user to acknowledge the high score and start a new game.
        // The level resources for the first level will be loaded later.
        ...
    }
    else
    {
        // This is either the first time the game has run or
        // the last time the game terminated the level was completed.
        // We are waiting for the user to begin the next level.
        ...
    }
    m_uiControl->ShowGameInfoOverlay();
}

Atualizar motor de jogo

O método App::Run chama GameMain::Run. Dentro de GameMain::Run é uma máquina de estado básica para lidar com todas as principais ações que um usuário pode executar. O nível mais alto dessa máquina de estado lida com o carregamento de um jogo, a reprodução de um nível específico ou a continuação de um nível após o jogo ter sido pausado (pelo sistema ou pelo usuário).

No jogo de exemplo, há 3 estados principais (representados pela enumeração UpdateEngineState ) em que o jogo pode estar.

  1. UpdateEngineState::WaitingForResources. O loop do jogo está em ciclo de vida, incapaz de fazer a transição até que os recursos (especificamente recursos gráficos) estejam disponíveis. Quando as tarefas assíncronas de carregamento de recursos são concluídas, atualizamos o estado para UpdateEngineState::ResourcesLoaded. Isso geralmente acontece entre os níveis quando o nível está carregando novos recursos do disco, de um servidor de jogo ou de um back-end de nuvem. No jogo de exemplo, simulamos esse comportamento, pois o exemplo não precisa de nenhum recurso adicional por nível naquele momento.
  2. UpdateEngineState::WaitingForPress. O loop do jogo está em ciclo de vida, aguardando uma entrada específica do usuário. Essa entrada é uma ação do jogador para carregar um jogo, iniciar um nível ou continuar um nível. O código de exemplo refere-se a esses subestados por meio da enumeração PressResultState .
  3. UpdateEngineState::D ynamics. O loop do jogo está sendo executado com o usuário jogando. Enquanto o usuário está jogando, o jogo verifica 3 condições para as quais ele pode fazer a transição:
  • GameState::TimeExpired. Expiração do prazo para um nível.
  • GameState::LevelComplete. Conclusão de um nível pelo jogador.
  • GameState::GameComplete. Conclusão de todos os níveis pelo jogador.

Um jogo é simplesmente uma máquina de estado contendo várias máquinas de estado menores. Cada estado específico deve ser definido por critérios muito específicos. As transições de um estado para outro devem ser baseadas na entrada discreta do usuário ou nas ações do sistema (como carregamento de recursos gráficos).

Ao planejar seu jogo, considere desenhar todo o fluxo do jogo para garantir que você tenha abordado todas as ações possíveis que o usuário ou o sistema podem tomar. Um jogo pode ser muito complicado, portanto, uma máquina de estado é uma ferramenta poderosa para ajudá-lo a visualizar essa complexidade e torná-la mais gerenciável.

Vamos dar uma olhada no código do loop de atualização.

O método GameMain::Update

Esta é a estrutura da máquina de estado usada para atualizar o mecanismo do jogo.

void GameMain::Update()
{
    // The controller object has its own update loop.
    m_controller->Update(); 

    switch (m_updateState)
    {
    case UpdateEngineState::WaitingForResources:
        ...
        break;

    case UpdateEngineState::ResourcesLoaded:
        ...
        break;

    case UpdateEngineState::WaitingForPress:
        if (m_controller->IsPressComplete())
        {
            ...
        }
        break;

    case UpdateEngineState::Dynamics:
        if (m_controller->IsPauseRequested())
        {
            ...
        }
        else
        {
            // When the player is playing, work is done by Simple3DGame::RunGame.
            GameState runState = m_game->RunGame();
            switch (runState)
            {
            case GameState::TimeExpired:
                ...
                break;

            case GameState::LevelComplete:
                ...
                break;

            case GameState::GameComplete:
                ...
                break;
            }
        }

        if (m_updateState == UpdateEngineState::WaitingForPress)
        {
            // Transitioning state, so enable waiting for the press event.
            m_controller->WaitForPress(
                m_renderer->GameInfoOverlayUpperLeft(),
                m_renderer->GameInfoOverlayLowerRight());
        }
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            // Transitioning state, so shut down the input controller
            // until resources are loaded.
            m_controller->Active(false);
        }
        break;
    }
}

Atualizar a interface do usuário

Precisamos manter o jogador informado sobre o estado do sistema e permitir que o estado do jogo mude dependendo das ações do jogador e das regras que definem o jogo. Muitos jogos, incluindo este jogo de exemplo, geralmente usam elementos de interface do usuário para apresentar essas informações ao jogador. A interface do usuário contém representações do estado do jogo e outras informações específicas do jogo, como pontuação, munição ou o número de chances restantes. A interface do usuário também é chamada de sobreposição porque é renderizada separadamente do pipeline de gráficos principal e colocada na parte superior da projeção 3D.

Algumas informações da interface do usuário também são apresentadas como um heads-up display (HUD) para permitir que o usuário veja essas informações sem tirar os olhos totalmente da área principal do jogo. No jogo de exemplo, criamos essa sobreposição usando as APIs Direct2D. Como alternativa, podemos criar essa sobreposição usando XAML, que discutimos em Estendendo o jogo de exemplo.

Há dois componentes para a interface do usuário.

  • O HUD que contém a pontuação e informações sobre o estado atual do jogo.
  • O bitmap de pausa, que é um retângulo preto com texto sobreposto durante o estado pausado/suspenso do jogo. Esta é a sobreposição do jogo. Discutiremos isso mais detalhadamente em Adicionando uma interface do usuário.

Sem surpresa, a sobreposição também tem uma máquina de estado. A sobreposição pode exibir uma mensagem de início de nível ou fim de jogo. É essencialmente uma tela na qual podemos gerar qualquer informação sobre o estado do jogo que queremos exibir ao jogador enquanto o jogo está pausado ou suspenso.

A sobreposição renderizada pode ser uma dessas seis telas, dependendo do estado do jogo.

  1. Tela de progresso de carregamento de recursos no início do jogo.
  2. Tela de estatísticas de jogo.
  3. Tela de mensagem inicial de nível.
  4. Tela de fim de jogo quando todos os níveis são concluídos sem que o tempo se esgote.
  5. Tela de fim de jogo quando o tempo acabar.
  6. Tela do menu de pausa.

Separar a interface do usuário do pipeline de gráficos do jogo permite que você trabalhe nela independentemente do mecanismo de renderização de gráficos do jogo e diminui significativamente a complexidade do código do jogo.

Veja como o jogo de exemplo estrutura a máquina de estado da sobreposição.

void GameMain::SetGameInfoOverlay(GameInfoOverlayState state)
{
    m_gameInfoOverlayState = state;
    switch (state)
    {
    case GameInfoOverlayState::Loading:
        m_uiControl->SetGameLoading(m_loadingCount);
        break;

    case GameInfoOverlayState::GameStats:
        ...
        break;

    case GameInfoOverlayState::LevelStart:
        ...
        break;

    case GameInfoOverlayState::GameOverCompleted:
        ...
        break;

    case GameInfoOverlayState::GameOverExpired:
        ...
        break;

    case GameInfoOverlayState::Pause:
        ...
        break;
    }
}

Tratamento de eventos

Como vimos no tópico Definir a estrutura do aplicativo UWP do jogo, muitos dos métodos do provedor de exibição da classe App registram manipuladores de eventos. Esses métodos precisam lidar corretamente com esses eventos importantes antes de adicionarmos mecânicas de jogo ou iniciarmos o desenvolvimento de gráficos.

O tratamento adequado dos eventos em questão é fundamental para a experiência do aplicativo UWP. Como um aplicativo UWP pode ser ativado, desativado, redimensionado, ajustado, desajustado, suspenso ou retomado a qualquer momento, o jogo deve se registrar para esses eventos o mais rápido possível e tratá-los de uma maneira que mantenha a experiência suave e previsível para o jogador.

Esses são os manipuladores de eventos usados neste exemplo e os eventos que eles manipulam.

Manipulador de eventos Descrição
OnActivated Identifica CoreApplicationView::Activated. O aplicativo do jogo foi colocado em primeiro plano, então a janela principal é ativada.
OnDpiChanged Lida com gráficos::D isplay::D isplayInformation::D piChanged. O DPI da tela mudou e o jogo ajusta seus recursos de acordo.
ObservaçãoAs coordenadas CoreWindow estão em DIPs (pixels independentes de dispositivo) para Direct2D. Como resultado, você deve notificar o Direct2D sobre a alteração no DPI para exibir todos os ativos 2D ou primitivos corretamente.
OnOrientationChanged Lida com gráficos::D isplay::D isplayInformation::OrientationChanged. A orientação da exibição muda e a renderização precisa ser atualizada.
OnDisplayContentsInvalidated Lida com gráficos::D isplay::D isplayInformation::D isplayContentsInvalidated. A exibição requer redesenho e seu jogo precisa ser renderizado novamente.
Ao retomar Manipula CoreApplication::Resuming. O aplicativo do jogo restaura o jogo de um estado suspenso.
SobreSuspensão Lida com CoreApplication::Suspending. O aplicativo do jogo salva seu estado no disco. Tem 5 segundos para salvar o estado no armazenamento.
OnVisibilityChanged Identifica CoreWindow::VisibilityChanged. O aplicativo do jogo mudou de visibilidade e se tornou visível ou ficou invisível por outro aplicativo se tornar visível.
OnWindowActivationChanged Lida com CoreWindow::Activated. A janela principal do aplicativo de jogo foi desativada ou ativada, portanto, deve remover o foco e pausar o jogo ou recuperar o foco. Em ambos os casos, a sobreposição indica que o jogo está pausado.
OnWindowClosed Identifica CoreWindow::Closed. O aplicativo do jogo fecha a janela principal e suspende o jogo.
OnWindowSizeChanged Identifica CoreWindow::SizeChanged. O aplicativo de jogo realoca os recursos gráficos e a sobreposição para acomodar a alteração de tamanho e, em seguida, atualiza o destino de renderização.

Próximas etapas

Neste tópico, vimos como o fluxo geral do jogo é gerenciado usando estados de jogo e que um jogo é composto de várias máquinas de estado diferentes. Também vimos como atualizar a interface do usuário e gerenciar os principais manipuladores de eventos do aplicativo. Agora estamos prontos para mergulhar no loop de renderização, no jogo e em sua mecânica.

Você pode percorrer os tópicos restantes que documentam este jogo em qualquer ordem.