Partilhar via


Definir o objeto principal do 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.

Depois de estabelecer a estrutura básica do jogo de amostra e implementar uma máquina de estado que lida com os comportamentos de alto nível do usuário e do sistema, você vai querer examinar as regras e mecânicas que transformam o jogo de amostra em um jogo. Vamos ver os detalhes do objeto principal do jogo de amostra e como traduzir as regras do jogo em interações com o mundo do jogo.

Objetivos

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

Objeto principal do jogo

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

Aqui estão algumas das funcionalidades 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 na máquina de estados definida no framework de aplicação.
    • Alterações no estado do jogo do aplicativo para o objeto do jogo em si.
    • Detalhes para atualizar a interface de utilizador do jogo (sobreposição e ecrã informativo), animações e física (a dinâmica).

    Observação

    A atualização de gráficos é tratada pela classe GameRenderer , que contém métodos para obter e usar recursos de dispositivos gráficos usados pelo jogo. Para obter mais informações, consulte Estrutura de renderização 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. Neste caso, os dados do estado do jogo referem-se ao tempo de vida do jogo e são inicializados uma vez quando um utilizador inicia o jogo.

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

Inicializar e iniciar o jogo

Quando um jogador inicia o jogo, o objeto do jogo deve inicializar seu estado, criar e adicionar a sobreposição, definir as variáveis que rastreiam o desempenho do jogador e instanciar os objetos que ele usará para construir os níveis. Neste exemplo, isso é feito quando a instância 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 o GameMain::ConstructInBackground corotina de fogo e esquecimento, 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 é criado.
  • Matrizes para primitivos gráficos do jogo são criadas, incluindo matrizes para primitivos de nível, munição e obstáculos.
  • Um local para salvar dados de estado do jogo é criado, chamado Jogo e colocado no local de armazenamento de configurações de dados do aplicativo especificado por ApplicationData::Current.
  • Um temporizador de jogo e o bitmap de sobreposição inicial dentro do jogo são criados.
  • Uma nova câmera é criada com um conjunto específico de parâmetros de visão e projeção.
  • O dispositivo de entrada (o controlador) é definido para a mesma inclinação e guinada iniciais que a câmera, para que o jogador tenha uma correspondência de 1:1 entre a posição de controlo inicial e a posição da câmara.
  • O objeto player é criado e definido como ativo. Usamos um objeto esfera para detetar a proximidade do jogador com paredes e obstáculos e para evitar que a câmera seja colocada em uma posição que possa quebrar a imersão.
  • O mundo primitivo do jogo é criado.
  • Os obstáculos de cilindro são criados.
  • Os alvos (objetosFace) são criados e numerados.
  • As esferas de munição são criadas.
  • Os níveis são criados.
  • A pontuação mais alta é carregada.
  • Qualquer estado de jogo salvo anteriormente é carregado.

O jogo agora tem instâncias de todos os componentes-chave - o mundo, o jogador, os obstáculos, os alvos e as esferas de munição. Ele também tem instâncias dos níveis, que representam configurações de todos os componentes acima e seus comportamentos para cada nível específico. Agora vamos ver como o jogo constrói os níveis.

Construa e carregue níveis de jogo

A maior parte do trabalho pesado para a construção do nível é feita nos arquivos encontrados na pasta GameLevels da solução de exemplo. Como ele se concentra em uma implementação muito específica, não vamos abordá-los 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, você pode criar um objeto Level[N] que toma um número atribuído como parâmetro e coloca aleatoriamente os obstáculos e alvos. Ou, você pode fazer com que ele carregue dados de configuração de nível de um arquivo de recurso, ou até mesmo da Internet.

Definir a jogabilidade

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

Os melhores jogos reagem instantaneamente à entrada do jogador e fornecem feedback imediato. Isso é verdade para qualquer tipo de jogo, desde jogos de ação rápida e de tiro em primeira pessoa em tempo real até jogos de estratégia baseados em turnos e ponderados.

O método Simple3DGame::RunGame

Enquanto um nível de jogo está a decorrer, o jogo encontra-se no estado Dynamics.

GameMain::Update é o loop de atualização principal 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 gestionar o trabalho se o jogo estiver no estado 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 lida com o conjunto de dados que define o estado atual do jogo para a iteração atual do loop do jogo.

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

  • O método atualiza o temporizador que conta os segundos até que o nível seja concluído e testa para ver se o tempo do nível expirou. Esta é uma das regras do jogo – quando o tempo se esgota, se nem todos os alvos foram disparados, então o jogo acabou.
  • Se o tempo se tiver esgotado, o método define o estado do jogo TimeExpired e retorna ao método Update no código anterior.
  • Se sobrar tempo, o controlador de movimento e olhar é sondado para uma atualização da posição da câmara; especificamente, uma atualização no ângulo de visão normal que se projeta do plano da câmara (onde o jogador está a olhar) e na distância que esse ângulo se deslocou desde a última sondagem do controlador.
  • A câmera é atualizada com base nos novos dados do controlador move-look.
  • As dinâmicas, ou as animações e comportamentos de objetos no mundo do jogo independente do controle do jogador, são atualizados. 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 alvos. Para obter mais informações, consulte Atualize o mundo do jogo.
  • O método verifica se os critérios para a conclusão bem-sucedida de um nível foram cumpridos. Se sim, ele finaliza a pontuação para o nível, e verifica se este é o último nível (de 6). Se for o último nível, o método retorna o estado do jogo GameState::GameComplete ; caso contrário, ele retorna o estado do jogo GameState::LevelComplete .
  • Se o nível não estiver completo, o método define o estado do jogo como GameState::Ative e retorna.

Atualize o mundo do jogo

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

Um loop como UpdateDynamics chama todos os métodos que são 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 criar um mundo dinâmico, mesmo quando não há entrada do jogador. No teu jogo, isso pode incluir árvores a balançar ao vento, ondas a desaguar ao longo das linhas costeiras, máquinas a fumegar e monstros alienígenas a esticar-se e a mexer-se. Também engloba a interação entre objetos, incluindo colisões entre a esfera do jogador e o mundo, ou entre a munição e os obstáculos e alvos.

Exceto quando o jogo é especificamente pausado, o loop do jogo deve continuar a atualizar o mundo do jogo; quer seja com base na lógica do jogo, algoritmos físicos ou se for simplesmente aleatório.

No jogo de amostra, este princípio é chamado de dinâmica , e engloba a ascensão e queda dos obstáculos do pilar, e o movimento e comportamentos físicos das esferas de munição à medida que são disparadas e em movimento.

O método Simple3DGame::UpdateDynamics

Este método lida com estes quatro conjuntos de cálculos.

  • As posições das esferas de munição disparadas no mundo.
  • A animação dos obstáculos em forma de pilares.
  • A intersecção entre o 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 mundo.

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. Tudo isso é computado no espaço de coordenadas do mundo do jogo.

Rever 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 do jogo, a amostra imediatamente chama GameRenderer::Render para pegar os dados atualizados do objeto e gerar uma nova cena para apresentar ao jogador, como 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 gráficos do mundo do videojogo

Recomendamos que os gráficos de um jogo sejam atualizados com frequência, idealmente exatamente com a mesma frequência com que o loop principal do jogo itera. À medida que o loop itera, o estado do mundo do jogo é atualizado, com ou sem a intervenção do jogador. Isso permite que as animações e comportamentos calculados 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 amostra como mostrado acima em GameMain::Run. Se a janela principal do jogo estiver visível e não estiver encaixada ou desativada, o jogo continuará a atualizar e exibir os resultados dessa atualização. O método GameRenderer::Render que examinamos a seguir renderiza uma representação desse estado. Isso é feito imediatamente após uma chamada para GameMain::Update, que inclui Simple3DGame::RunGame para atualizar os 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. Após a conclusão, 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 com informações do jogo que contém o bitmap para o menu de pausa, e outro em que o jogo exibe as miras junto com os retângulos para o controlador de movimentação e visualização no ecrã tátil. O texto da pontuação é exibido em ambos os 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

Estes 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 pelo Simple3DGame incluem as listadas 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 .
  • CarregarJogo. Inicializa um novo nível e começa a carregá-lo.
  • LoadLevelAsync. Uma co-rotina que inicializa o nível e, em seguida, invoca outra co-rotina no renderizador para carregar os recursos de nível específicos do dispositivo. Este método é executado em um thread separado; como resultado, apenas os métodos ID3D11Device (em oposição aos métodos ID3D11DeviceContext ) podem ser chamados a partir desse thread. Quaisquer métodos de contexto de dispositivo são chamados no método FinalizeLoadLevel. Se você é novo em programação assíncrona, consulte Simultaneidade e operações assíncronas com C++/WinRT.
  • FinalizeLoadLevel. Conclui qualquer trabalho relacionado ao carregamento de níveis que precise ser executado na linha de execução principal. Isso inclui quaisquer chamadas para os métodos de contexto de dispositivo Direct3D 11 (ID3D11DeviceContext).
  • NívelInicial. Inicia a jogabilidade para um novo nível.
  • PauseGame. Pausa o jogo.
  • RunGame. Executa uma iteração do loop do jogo. É chamado a partir de App::Update uma vez a cada iteração do loop do jogo se o estado do jogo for Ative.
  • OnSuspending e OnResuming. Suspenda/retome 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. Carregar/gravar a pontuação mais alta entre jogos, respectivamente.
  • InicializeAmmo. Redefine o estado de cada objeto esférico usado como munição de volta ao seu estado original para o início de cada rodada.
  • UpdateDynamics. Este é 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. Este é o coração da interatividade que define o jogo. Isso é abordado na seção Atualizar o mundo do jogo.

Os outros métodos públicos são acessadores de propriedade que retornam informações específicas de jogabilidade e de sobreposição para a estrutura da aplicação para exibição.

Membros de dados

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

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

O resto das variáveis do jogo contém as listas dos primitivos, e suas respetivas quantidades no jogo, e dados e restrições específicas do jogo.

Próximos passos

Ainda não falámos sobre o mecanismo real de renderização — como as chamadas aos métodos Render nos primitivos atualizados são convertidas em pixels no seu ecrã. Esses aspetos são abordados em duas partes:Framework de Renderização I: Introdução ao rendering e Framework de Renderização II: Renderização de jogos. Se estiver mais interessado em saber como os controlos do jogador atualizam o estado do jogo, consulte Adicionar controlos.