Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Observação
Este tópico faz parte da série de tutoriais Criar um jogo simples usando a Plataforma Universal do Windows (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 principal do jogo de exemplo e como traduzir regras de jogo em interações com o mundo do jogo.
Objectivos
- Saiba como aplicar técnicas básicas de desenvolvimento para implementar regras de jogo e mecânica para um jogo UWP DirectX.
Objeto de jogo principal
No jogo de exemplo Simple3DGameDX, a classe de objeto principal do jogo é Simple3DGame. Uma instância de 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 a máquina de estados definida na estrutura do aplicativo.
- Alterações no estado do jogo do aplicativo para o próprio objeto do 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 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. 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 dados definidos por esta classe, veja 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 acompanham o desempenho do jogador e instanciar os objetos que ele usará para criar os níveis. Neste exemplo, isso é feito quando a instância de GameMain é criada no App::Load.
O objeto do jogo, do tipo Simple3DGame, é criado no construtor GameMain::GameMain. Em seguida, ele é inicializado usando o método Simple3DGame::Initialize durante a coroutine do tipo fire-and-forget GameMain::ConstructInBackground, que é chamada a partir de GameMain::GameMain.
O método Simple3DGame::Initialize
O jogo de exemplo configura esses componentes no objeto do jogo.
- Um novo objeto de reprodução de áudio é criado.
- Matrizes para os elementos gráficos do jogo são criadas, incluindo matrizes para elementos dos níveis, munições e obstáculos.
- Um local para salvar dados de estado do jogo é criado, chamado Gamee 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 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) é ajustado para os mesmos ângulos iniciais de inclinação e guinada que a câmera, de modo que o jogador tem uma correspondência de 1 para 1 entre a posição de controle inicial e a posição da câmera.
- O objeto player é 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.
- O elemento primitivo do mundo do jogo é criado.
- Os obstáculos cilíndricos 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 alta foi carregada.
- Qualquer estado de jogo salvo anteriormente é carregado.
O jogo agora tem instâncias de todos os componentes principais: 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 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 arquivos de Level[N].h/.cpp
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 é executado como um objeto Level[N]
Definir a jogabilidade
Neste ponto, 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 com eles.
Os melhores jogos reagem instantaneamente à entrada do jogador e fornecem comentários imediatos. Isso é verdade para qualquer tipo de jogo, desde jogos de ação rápida, tiroteios em primeira pessoa em tempo real, até jogos de estratégia baseados em turnos e reflexivos.
O método Simple3DGame::RunGame
Enquanto um nível de jogo está em andamento, o jogo está 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 lidar com 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 manipula 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. Essa é uma das regras do jogo , quando o tempo se esgota, se não todos os alvos foram disparados, então é 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 sobrar tempo, o controlador de movimentação e visão será consultado para uma atualização na posição da câmera; especificamente, uma atualização no ângulo da normal de visão projetada a partir do plano da câmera (onde o jogador está olhando) e a distância que esse ângulo se deslocou desde que o controlador foi consultado pela última vez.
- A câmera é atualizada com base nos novos dados do controlador de movimento e aparência.
- A dinâmica, ou as animações e comportamentos de objetos no mundo do jogo independentemente do controle do jogador, são atualizadas. 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 de pilares e o movimento dos alvos. Para obter mais informações, consulte Atualização do 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 do 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::Activee 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 tornar o nível vivo. Isso inclui elementos 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á nenhuma entrada do player. No seu jogo, isso pode incluir árvores balançando ao vento, ondas quebrando ao longo das linhas da costa, maquinário soltando fumaça, e monstros alienígenas se estendendo e se movendo ao redor. Ele também abrange 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 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 dinâmica, e abrange a subida e descida dos obstáculos pilares, e o movimento e os comportamentos físicos das esferas de munição quando são disparadas e estão 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 do pilar.
- A interseção do jogador e dos limites mundiais.
- 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. Isso tudo é calculado no espaço de coordenadas do mundo 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 do jogo, o exemplo imediatamente chama GameRenderer::Render para utilizar os dados atualizados do objeto 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 gráficos do mundo do jogo
Recomendamos que os gráficos em um jogo sejam atualizados com frequência, idealmente exatamente na mesma frequência que o loop principal itera. À medida que o loop faz iteração, o estado do mundo do jogo é atualizado, com ou sem a entrada 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 exemplo mostrado acima em GameMain::Run. Se a janela principal do jogo estiver visível e não for ajustada ou desativada, o jogo continuará atualizando e renderizando os resultados dessa atualização. O método GameRenderer::Render que será examinado a seguir 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, a sobreposição Direct2D sobre ela. Quando concluído, ele apresenta a cadeia de troca 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 juntamente com os retângulos para o controlador de movimento e visualização por toque. 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
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.
- carregarJogo. Inicializa um novo nível e começa a carregá-lo.
-
LoadLevelAsync. Uma coroutina que inicializa o nível e invoca outra coroutina no renderizador para carregar os recursos de nível específicos do dispositivo. Esse método é executado em um thread separado; como resultado, somente métodos
(em oposição aos métodos ID3D11DeviceContext deID3D11Device ) podem ser chamados desse thread. Qualquer método de contexto do dispositivo é chamado no método FinalizeLoadLevel. Se você não estiver familiarizado com a programação assíncrona, veja Concorrência e operações assíncronas com C++/WinRT. - FinalizeLoadLevel. Conclui qualquer trabalho de carregamento de nível que precisa ser feito em thread principal. Isso inclui todas as chamadas para métodos do contexto do 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. Ele é chamado de App::Update uma vez a cada iteração do loop do jogo se o estado do jogo estiver Ativo.
- OnSuspending e OnResuming. Suspender/retomar o áudio do jogo, respectivamente.
Aqui estão as funções de membro privado.
- CarregarEstadoSalvo e SalvarEstado. Carregue/salve o estado atual do jogo, respectivamente.
- LoadHighScore e SaveHighScore. Carregue/salve a pontuação máxima entre jogos, respectivamente.
- InitializeAmmo. Redefine o estado de cada objeto esfera usados como munição, retornando ao seu estado original no 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 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 jogo e superposição para o framework do aplicativo para exibição.
Membros de dados
Esses objetos são atualizados à medida que o loop do jogo é executado.
- objeto MoveLookController. Representa a entrada do jogador. Para obter mais informações, consulte Adicionar 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 estrutura de renderização 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 do jogo contém as listas dos primitivos e suas respectivas quantidades no jogo e os dados e restrições específicos do jogo.
Próximas etapas
Ainda não falamos sobre o mecanismo de renderização propriamente dito — como as chamadas para os métodos de renderização nos primitivos atualizados se transformam em pixels na sua tela. Esses aspectos são abordados em duas partes:estrutura de renderização I: Introdução à renderização e Estrutura de Renderização II: renderização de jogos. Se você estiver mais interessado em como os controles do jogador atualizam o estado do jogo, consulte Adicionar controles.