Share via


Definir o objeto principal do jogo

Observação

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

Depois de definir a estrutura básica do jogo de exemplo e implementar uma máquina de estado que manipula os comportamentos de alto nível do usuário e do sistema, você desejará examinar as regras e a mecânica que transformam o jogo de exemplo em um jogo. Vamos examinar os detalhes do objeto main do jogo de exemplo e como traduzir regras de jogo em interações com o mundo do jogo.

Objetivos

  • Saiba como aplicar técnicas básicas de desenvolvimento para implementar regras de jogo e mecânica para um jogo DirectX UWP.

Objeto principal do jogo

No jogo de exemplo Simple3DGameDX, Simple3DGame é a classe de objeto de jogo main. Uma instância do Simple3DGame é construída, indiretamente, por meio do método App::Load .

Aqui estão alguns dos recursos da classe Simple3DGame .

  • Contém a implementação da lógica de jogo.
  • Contém métodos que comunicam esses detalhes.
    • Alterações no estado do jogo para o computador de estado definido na estrutura do aplicativo.
    • Alterações no estado do jogo do aplicativo para o próprio objeto de jogo.
    • Detalhes para atualizar a interface do usuário do jogo (sobreposição e exibição de cabeçalho), animações e física (a dinâmica).

    Observação

    A atualização de elementos gráficos é tratada pela classe GameRenderer , que contém métodos para obter e usar recursos de dispositivo gráfico usados pelo jogo. Para obter mais informações, consulte Renderizando a estrutura I: Introdução à renderização.

  • Serve como um contêiner para os dados que definem uma sessão de jogo, nível ou tempo de vida, dependendo de como você define seu jogo em um alto nível. Nesse caso, os dados de estado do jogo são para o tempo de vida do jogo e são inicializados uma vez quando um usuário inicia o jogo.

Para exibir os métodos e os dados definidos por essa classe, consulte a classe Simple3DGame abaixo.

Inicializar e iniciar o jogo

Quando um jogado inicia o jogo, o objeto de jogo deve iniciar seu estado, criar e adicionar a sobreposição, definir as variáveis que acompanham o desempenho do jogador e instanciar os objetos usados para criar os níveis. Neste exemplo, isso é feito quando a instância do GameMain é criada em App::Load.

O objeto de jogo, do tipo Simple3DGame, é criado no construtor GameMain::GameMain . Em seguida, ele é inicializado usando o método Simple3DGame::Initialize durante a corrotina GameMain::ConstructInBackground fire-and-forget, que é chamada de GameMain::GameMain.

O método Simple3DGame::Initialize

O jogo de exemplo configura esses componentes no objeto de jogo.

  • Um novo objeto de reprodução de áudio foi criado.
  • As matrizes para as primitivas gráficas do jogo são criadas, incluindo matrizes para as primitivas de nível, munição e obstáculos.
  • Um local para salvar os dados de estado do jogo é criado, chamado Jogo e colocado no local de armazenamento das configurações de dados do aplicativo especificado pelo ApplicationData::Current.
  • Um temporizador de jogo e o bitmap de sobreposição inicial no jogo são criados.
  • Uma nova câmera é criada com um conjunto específico de parâmetros de exibição e projeção.
  • O dispositivo de entrada (o controlador) é definido para a mesma arfagem e guinada da câmera, para que o jogador tenha uma correspondência de 1 para 1 entre a posição inicial do controle e a posição da câmera.
  • O objeto do jogador é criado e definido como ativo. Usamos um objeto sphere para detectar a proximidade do jogador com paredes e obstáculos e impedir que a câmera seja colocada em uma posição que possa interromper a imersão.
  • A primitiva do ambiente do jogo é criada.
  • Os obstáculos cilíndricos são criados.
  • Os destinos (objetos Face) são criados e numerados.
  • As esferas de munição são criadas.
  • Os níveis são criados.
  • A pontuação máxima é carregada.
  • Qualquer estado de jogo salvo anteriormente é carregado.

O jogo agora tem instâncias de todos os principais componentes: o mundo, o jogador, os obstáculos, os destinos e as esferas de munição. Ele também tem instâncias dos níveis, que representam configurações de todos os componentes anteriores e seus comportamentos para cada nível de modo específico. Agora vamos ver como o jogo cria os níveis.

Criar e carregar níveis de jogo

A maior parte do trabalho pesado para a construção de nível é feita nos Level[N].h/.cpp arquivos encontrados na pasta GameLevels da solução de exemplo. Como ele se concentra em uma implementação muito específica, não os abordaremos aqui. O importante é que o código para cada nível seja executado como um objeto Level[N] separado. Se você quiser estender o jogo, poderá criar um objeto Level[N] que usa um número atribuído como um parâmetro e coloca aleatoriamente os obstáculos e destinos. Ou, você pode fazer com que eles carreguem dados de configuração de nível de carga de um arquivo de recurso ou até mesmo da Internet.

Definir a jogabilidade

Neste ponto, temos todos os componentes que precisamos para desenvolver o jogo. Os níveis foram construídos na memória dos primitivos e estão prontos para o jogador começar a interagir.

Os melhores jogos reagem instantaneamente à entrada do jogador e fornecem comentários imediatos. Isso é verdade para qualquer tipo de jogo, desde twitch-action, atiradores em primeira pessoa em tempo real até jogos de estratégia atenciosos e baseados em turnos.

O método Simple3DGame::RunGame

Enquanto um nível de jogo está em andamento, o jogo está no estado do Dynamics .

GameMain::Update é o loop de atualização main que atualiza o estado do aplicativo uma vez por quadro, conforme mostrado abaixo. O loop de atualização chama o método Simple3DGame::RunGame para lidar com o trabalho se o jogo estiver no estado do Dynamics .

// Updates the application state once per frame.
void GameMain::Update()
{
    // The controller object has its own update loop.
    m_controller->Update();

    switch (m_updateState)
    {
    ...
    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)
            {
                ...

Simple3DGame::RunGame manipula o conjunto de dados que define o estado atual do jogo para a iteração atual do loop de jogo.

Aqui está a lógica de fluxo de jogo em Simple3DGame::RunGame.

  • O método atualiza o temporizador que conta os segundos até que o nível seja concluído e testa se o tempo do nível expirou. Essa é uma das regras do jogo, quando o tempo se esgota, se nem todos os alvos foram disparados, é o fim do jogo.
  • Se o tempo tiver se esgotado, o método definirá o estado do jogo TimeExpired e retornará ao método Update no código anterior.
  • Se o tempo permanecer, o controlador de aparência de movimento será sondado para obter uma atualização para a posição da câmera; especificamente, uma atualização para o ângulo da projeção normal de exibição do plano da câmera (onde o jogador está olhando) e a distância que o ângulo moveu desde que o controlador foi sondado por último.
  • A câmera é atualizada com base nos novos dados do controlador de movimento/visão.
  • A dinâmica (ou seja, as animações e os comportamentos dos objetos no ambiente de jogo que não dependem do controle do jogador) é atualizada. Neste jogo de exemplo, o método Simple3DGame::UpdateDynamics é chamado para atualizar o movimento das esferas de munição que foram disparadas, a animação dos obstáculos do pilar e o movimento dos destinos. Para obter mais informações, consulte Atualizar o mundo do jogo.
  • O método verifica se os critérios para a conclusão bem-sucedida de um nível foram atendidos. Nesse caso, ele finaliza a pontuação para o nível e verifica se esse é o último nível (de 6). Se for o último nível, o método retornará o estado do jogo GameState::GameComplete ; caso contrário, ele retorna o estado do jogo GameState::LevelComplete .
  • Se o nível não estiver concluído, o método definirá o estado do jogo como GameState::Active e retornará.

Atualizar o mundo do jogo

Neste exemplo, quando o jogo está em execução, o método Simple3DGame::UpdateDynamics é chamado do método Simple3DGame::RunGame (que é chamado de GameMain::Update) para atualizar objetos renderizados em uma cena de jogo.

Um loop como UpdateDynamics chama todos os métodos usados para colocar o mundo do jogo em movimento, independentemente da entrada do jogador, para criar uma experiência de jogo imersiva e fazer o nível ganhar vida. Isso inclui gráficos que precisam ser renderizados e loops de animação em execução para trazer um mundo dinâmico, mesmo quando não há entrada do player. No seu jogo, isso pode incluir árvores balançando ao vento, ondas se arrastando ao longo das linhas da costa, máquinas fumando e monstros alienígenas se estendendo e se movendo. Ela também compreende a interação entre objetos, inclusive colisões entre a esfera do jogador e o ambiente ou entre a munição e os obstáculos/alvos.

Exceto quando o jogo está especificamente pausado, o loop de jogo deve continuar atualizando o mundo do jogo; se isso é baseado na lógica do jogo, algoritmos físicos ou se é simplesmente aleatório.

No jogo de exemplo, esse princípio é chamado de dinâmica, e abrange a ascensão e queda dos obstáculos de pilar, e o movimento e os comportamentos físicos das esferas de munição à medida que são disparados e em movimento.

O método Simple3DGame::UpdateDynamics

Esse método lida com esses quatro conjuntos de cálculos.

  • As posições das esferas de munição disparadas no mundo.
  • A animação dos obstáculos de pilar.
  • A intersecção do jogador e as fronteiras do mundo.
  • As colisões das esferas de munição com os obstáculos, os alvos, outras esferas de munição e o ambiente.

A animação dos obstáculos ocorre em um loop definido nos arquivos de código-fonte Animate.h/.cpp . O comportamento da munição e quaisquer colisões são definidos por algoritmos de física simplificados, fornecidos no código e parametrizados por um conjunto de constantes globais para o mundo do jogo, incluindo gravidade e propriedades materiais. Isso tudo é calculado no espaço da coordenada do ambiente do jogo.

Examinar o fluxo

Agora que atualizamos todos os objetos na cena e calculamos quaisquer colisões, precisamos usar essas informações para desenhar as alterações visuais correspondentes.

Depois que GameMain::Update tiver concluído a iteração atual do loop de jogo, o exemplo chama imediatamente GameRenderer::Render para pegar os dados de objeto atualizados e gerar uma nova cena para apresentar ao jogador, conforme mostrado abaixo.

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible)
        {
            switch (m_updateState)
            {
            case UpdateEngineState::Deactivated:
            case UpdateEngineState::TooSmall:
                ...
                // Otherwise, fall through and do normal processing to perform rendering.
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
                    CoreProcessEventsOption::ProcessAllIfPresent);
                // GameMain::Update calls Simple3DGame::RunGame. If game is in Dynamics
                // state, uses Simple3DGame::UpdateDynamics to update game world.
                Update();
                // Render is called immediately after the Update loop.
                m_renderer->Render();
                m_deviceResources->Present();
                m_renderNeeded = false;
            }
        }
        else
        {
            CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
                CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }
    m_game->OnSuspending();  // Exiting due to window close, so save state.
}

Renderizar os elementos gráficos do mundo do jogo

Recomendamos que os elementos gráficos em uma atualização de jogo sejam atualizados com frequência, idealmente exatamente com a frequência em que o loop de jogo main itera. À medida que o loop itera, o estado do mundo do jogo é atualizado, com ou sem entrada do jogador. Isso permite que as animações calculadas e os comportamentos sejam exibidos sem problemas. Imagine se tivéssemos uma cena simples de água que se movia apenas quando o jogador pressionava um botão. Isso não seria realista; um bom jogo parece suave e fluido o tempo todo.

Lembre-se do loop do jogo de exemplo, conforme mostrado acima em GameMain::Run. Se a janela main do jogo estiver visível e não for encaixada ou desativada, o jogo continuará a atualizar e renderizar os resultados dessa atualização. O método GameRenderer::Render que examinamos em seguida renderiza uma representação desse estado. Isso é feito imediatamente após uma chamada para GameMain::Update, que inclui Simple3DGame::RunGame para atualizar estados, conforme discutido na seção anterior.

GameRenderer::Render desenha a projeção do mundo 3D e, em seguida, desenha a sobreposição Direct2D sobre ele. Quando termina, ele apresenta a cadeia de permuta final com os buffers combinados para exibição.

Observação

Há dois estados para a sobreposição Direct2D do jogo de exemplo: um em que o jogo exibe a sobreposição de informações do jogo que contém o bitmap para o menu de pausa e outro em que o jogo exibe a mira junto com os retângulos do controlador de aparência de movimento touch. O texto da pontuação é gerado nos dois estados. Para obter mais informações, consulte Estrutura de renderização I: introdução à renderização.

O método GameRenderer::Render

void GameRenderer::Render()
{
    bool stereoEnabled{ m_deviceResources->GetStereoState() };

    auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
    auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };

    ...
        if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
        {
            // This section is only used after the game state has been initialized and all device
            // resources needed for the game have been created and associated with the game objects.
            ...
            for (auto&& object : m_game->RenderObjects())
            {
                object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
            }
        }

        d3dContext->BeginEventInt(L"D2D BeginDraw", 1);
        d2dContext->BeginDraw();

        // To handle the swapchain being pre-rotated, set the D2D transformation to include it.
        d2dContext->SetTransform(m_deviceResources->GetOrientationTransform2D());

        if (m_game != nullptr && m_gameResourcesLoaded)
        {
            // This is only used after the game state has been initialized.
            m_gameHud.Render(m_game);
        }

        if (m_gameInfoOverlay.Visible())
        {
            d2dContext->DrawBitmap(
                m_gameInfoOverlay.Bitmap(),
                m_gameInfoOverlayRect
                );
        }
        ...
    }
}

A classe Simple3DGame

Esses são os métodos e membros de dados definidos pela classe Simple3DGame .

Funções de membro

As funções de membro público definidas por Simple3DGame incluem as abaixo.

  • Inicializar. Define os valores iniciais das variáveis globais e inicializa os objetos do jogo. Isso é abordado na seção Inicializar e iniciar o jogo .
  • LoadGame. Inicializa um novo nível e começa a carregá-lo.
  • LoadLevelAsync. Uma corrotina que inicializa o nível e invoca outra corrotina no renderizador para carregar os recursos de nível específicos do dispositivo. Esse método é executado em um thread separado; assim, apenas métodos ID3D11Device (ao contrário de métodos ID3D11DeviceContext) podem ser chamados nesse thread. Os métodos de contexto de dispositivo são chamados no método FinalizeLoadLevel. Se você não estiver familiarizado com a programação assíncrona, consulte Simultaneidade e operações assíncronas com C++/WinRT.
  • FinalizeLoadLevel. Conclua todo o trabalho para o carregamento de nível que precisa ser feito no thread principal. Isso inclui todas as chamadas para os métodos de contexto de dispositivo do Direct3D 11 (ID3D11DeviceContext).
  • StartLevel. Inicia a jogabilidade para um novo nível.
  • PauseGame. Pausa o jogo.
  • RunGame. Executa uma iteração do loop do jogo. Ele é chamado em App::Update uma vez a cada iteração do loop do jogo caso o estado do jogo seja Active.
  • OnSuspending e OnResuming. Suspender/retomar o áudio do jogo, respectivamente.

Aqui estão as funções de membro privado.

  • LoadSavedState e SaveState. Carregue/salve o estado atual do jogo, respectivamente.
  • LoadHighScore e SaveHighScore. Carregue/salve a pontuação alta entre os jogos, respectivamente.
  • InitializeAmmo. Redefine o estado de cada objeto de esfera usado como munição de volta ao seu estado original para o início de cada rodada.
  • UpdateDynamics. Esse é um método importante porque atualiza todos os objetos do jogo com base em rotinas de animação enlatadas, física e entrada de controle. Esse é o núcleo da interatividade que define o jogo. Isso é abordado na seção Atualizar o mundo do jogo .

Os outros métodos públicos são o acessador de propriedade que retorna informações específicas de jogo e sobreposição para a estrutura do aplicativo para exibição.

Membros de dados

Esses objetos são atualizados à medida que o loop de jogo é executado.

  • Objeto MoveLookController . Representa a entrada do jogador. Para obter mais informações, consulte Adicionando controles.
  • Objeto GameRenderer . Representa um renderizador Direct3D 11, que manipula todos os objetos específicos do dispositivo e sua renderização. Para obter mais informações, consulte Renderizando a estrutura I.
  • Objeto de áudio. Controla a reprodução de áudio do jogo. Para obter mais informações, consulte Adicionando som.

O restante das variáveis de jogo contêm as listas dos primitivos e suas respectivas quantidades no jogo e dados e restrições específicos do jogo.

Próximas etapas

Ainda não falamos sobre o mecanismo de renderização real– como as chamadas para os métodos Render nos primitivos atualizados são transformadas em pixels em sua tela. Esses aspectos são abordados em duas partes: Estrutura de renderização I: Introdução à renderização e Renderização da estrutura II: renderização de jogos. Se você estiver mais interessado em como os controles do jogador atualizam o estado do jogo, consulte Adicionar controles.