Gerenciamento de fluxo de jogo

Observação

Este tópico faz parte da série de tutoriais Criar um jogo de Plataforma Universal do Windows simples (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 do jogo, como gerenciar estados de jogos-chave específicos 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 um computador, ele está em um estado em que nenhum jogo foi iniciado. Nas horas subsequentes em que o jogo é executado, ele pode estar em qualquer um desses estados.

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

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

Gerenciamento de estado do jogo

Assim, durante a inicialização do jogo, você precisará dar suporte ao início frio do jogo, bem como retomar o jogo depois de pará-lo em vôo. O exemplo Simple3DGameDX sempre salva o estado do jogo para dar a impressão de que ele nunca parou.

Em resposta a um evento de suspensão, a jogabilidade é suspensa, mas os recursos do jogo ainda estão na memória. Da mesma forma, o evento de retomada é tratado 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 para o jogador.

  • Se o jogo for retomado no nível médio, ele aparecerá em pausa 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 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 para o usuário.

O jogo de exemplo não distingue se o jogo é inicializado a frio, iniciando pela primeira vez sem um evento de suspensão ou retomando de um estado suspenso. Este é 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.

a máquina de estado principal do nosso jogo

O método GameMain::InitializeGameState

O método GameMain::InitializeGameState é chamado indiretamente por meio do construtor da classe GameMain, que é o resultado da criação de uma instância de GameMain em 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 o mecanismo do jogo

O método App::Run chama GameMain::Run. No GameMain::Run há um computador de estado básico para lidar com todas as ações principais que um usuário pode executar. O nível mais alto dessa máquina de estado lida com o carregamento de um jogo, o jogo em 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á três estados principais (representados pela enumeração UpdateEngineState ) em que o jogo pode estar.

  1. UpdateEngineState::WaitingForResources. O loop do jogo está em exibição cíclica, não será possível transitar até os recursos (especificamente os recursos gráficos) estarem disponíveis. Quando as tarefas de carregamento de recursos assíncronas forem concluídas, atualizaremos o estado para UpdateEngineState::ResourcesLoaded. Isso geralmente ocorre entre níveis quando o nível está carregando novos recursos do disco, de um servidor de jogos ou de um back-end de nuvem. No jogo de exemplo, simulamos esse comportamento, pois o exemplo não precisa de recursos adicionais por nível no momento.
  2. UpdateEngineState::WaitingForPress. O loop do jogo está em exibição cíclica, 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 sub-estados 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 três condições na qual ele pode transitar:
  • GameState::TimeExpired. Expiração do limite de tempo 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 que contém vários computadores 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 em 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 executar. 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

Essa é a estrutura da máquina de estado usada para atualizar o mecanismo de 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 seja alterado de acordo com as ações do jogador e as 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 gráfico main e colocada sobre a projeção 3D.

Algumas informações de interface do usuário também são apresentadas como um HUD (aviso de exibição) para permitir que o usuário veja essas informações sem tirar os olhos totalmente da área de jogo main. No jogo de exemplo, criamos essa sobreposição usando as APIs direct2D. Como alternativa, poderíamos criar essa sobreposição usando XAML, que discutimos em Estender o jogo de exemplo.

Há dois componentes na interface do usuário.

  • O HUD que contém a pontuação e as 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. Ela será abordada com mais detalhes em Adicionando uma interface do usuário

Como era de se esperar, a sobreposição também tem uma máquina de estado. A sobreposição pode exibir um início de nível ou uma mensagem de 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á em pausa ou suspenso.

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

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

Separar a interface do usuário do pipeline de gráficos do jogo permite que você trabalhe nele 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;
    }
}

Manipulação de eventos

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

O tratamento adequado dos eventos em questão é fundamental para a experiência do aplicativo UWP. Como um aplicativo UWP pode, a qualquer momento, ser ativado, desativado, redimensionado, ajustado, não mapeado, suspenso ou retomado, o jogo deve se registrar para esses eventos assim que puder 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 Manipula CoreApplicationView::Activated. O app de jogo foi trazido para o primeiro plano. Por isso, a janela principal foi ativada.
OnDpiChanged Manipula Graphics::Display::DisplayInformation::DpiChanged. O DPI da janela mudou e o jogo ajusta devidamente seus recursos.
Observação As coordenadas do 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 quaisquer ativos ou primitivas 2D corretamente.
OnOrientationChanged Manipula Graphics::Display::DisplayInformation::OrientationChanged. A orientação da tela é alterada e a renderização precisa ser atualizada.
OnDisplayContentsInvalidated Manipula Graphics::Display::DisplayInformation::DisplayContentsInvalidated. A exibição precisa ser redesenhada e o jogo precisa ser renderizado novamente.
OnResuming Manipula CoreApplication::Resuming. O aplicativo de jogo restaura o jogo de um estado suspenso.
OnSuspending Manipula CoreApplication::Suspending. O aplicativo de jogo salva seu estado em disco. Ele tem cinco segundos para salvar o estado no armazenamento.
OnVisibilityChanged Manipula CoreWindow::VisibilityChanged. O aplicativo de jogo tem visibilidade alterada e se torna visível ou foi tornado invisível por outro aplicativo que se tornou visível.
OnWindowActivationChanged Manipula CoreWindow::Activated. A janela principal do aplicativo de jogo foi desativada ou ativada. Por isso, ela deve remover o foco e pausar o jogo ou obter o foco novamente. Nos dois casos, a sobreposição indica que o jogo está em pausa.
OnWindowClosed Manipula CoreWindow::Closed. O aplicativo de jogo fecha a janela principal e suspende o jogo.
OnWindowSizeChanged Manipula CoreWindow::SizeChanged. O app de jogo realoca os recursos gráficos e a sobreposição para acomodar a mudança 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 por 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 na mecânica dele.

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