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 definir a estrutura básica do jogo de exemplo e implementar uma máquina de estado que lida com os comportamentos de usuário e sistema de alto nível, você desejará examinar as regras e mecânicas que transformam o jogo de exemplo em um jogo. Vejamos os detalhes do objeto principal do jogo de exemplo 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 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 estado definida 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 alerta), 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 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, nível ou tempo de vida do jogo, dependendo de como você define seu jogo em um nível alto. 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 essa classe, consulte A classe Simple3DGame abaixo.
Inicialize e inicie 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 criar 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 a corrotina Fire-and-forget GameMain::ConstructInBackground , que é chamada 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 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 dados de estado do jogo é criado, chamado Game e colocado no local de armazenamento de configurações de dados do aplicativo especificado por ApplicationData::Current.
- Um cronômetro 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 visualização e projeção.
- O dispositivo de entrada (o controlador) é definido para a mesma inclinação inicial e guinada da câmera, para que o jogador tenha 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 de esfera para detectar a proximidade do jogador com paredes e obstáculos e para evitar que a câmera seja colocada em uma posição que possa interromper a imersão.
- O mundo do jogo primitivo é criado.
- Os obstáculos do cilindro são criados.
- Os alvos (objetos de face ) 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 anterior é 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 possui 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 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 quiser estender o jogo, você pode criar um objeto Level[N] que usa 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.
Defina a jogabilidade
Neste ponto, temos todos os componentes necessários 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 feedback imediato. Isso é verdade para qualquer tipo de jogo, desde jogos de tiro em primeira pessoa em tempo real até jogos de estratégia baseados em turnos.
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 do fluxo do jogo em Simple3DGame::RunGame.
- O método atualiza o temporizador que faz a contagem regressiva dos 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 acabar, se nem todos os alvos forem baleados, o jogo acaba.
- 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 sobrar, o controlador move-look será sondado para uma atualização da posição da câmera; especificamente, uma atualização do ângulo da projeção normal de visão do plano da câmera (para onde o jogador está olhando) e a distância que o ângulo se moveu desde que o controlador foi pesquisado pela última vez.
- A câmera é atualizada com base nos novos dados do controlador move-look.
- A dinâmica, ou as animações e comportamentos de objetos no mundo do jogo independentes 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 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. Em caso afirmativo, ele finaliza a pontuação do nível e verifica se este é 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 retornará 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á.
Atualize 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 dar vida ao nível. Isso inclui gráficos que precisam ser renderizados e a execução de loops de animação para criar um mundo dinâmico, mesmo quando não há entrada do jogador. Em seu jogo, isso pode incluir árvores balançando ao vento, ondas ao longo das linhas costeiras, máquinas fumegando e monstros alienígenas se esticando e se movendo. 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 é especificamente pausado, o loop do jogo deve continuar atualizando o mundo do jogo; seja baseado na lógica do jogo, algoritmos físicos ou simplesmente aleatório.
No jogo de amostra, esse princípio é chamado de dinâmica e abrange a ascensão e queda dos obstáculos dos pilares e o movimento e os 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 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 os limites 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 físicos simplificados, fornecidos no código e parametrizados por um conjunto de constantes globais para o mundo do jogo, incluindo gravidade e propriedades do material. Tudo isso é calculado no espaço de coordenadas do mundo do jogo.
Revisar o fluxo
Agora que atualizamos todos os objetos na cena e calculamos todas as 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 chamará imediatamente GameRenderer::Render para obter 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.
}
Renderize os gráficos do mundo do jogo
Recomendamos que os gráficos em um jogo sejam atualizados com frequência, de preferência exatamente com a mesma frequência com que o loop principal do jogo é iterado. À medida que o loop é iterado, 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 simples cena de água que se movesse apenas quando o jogador pressionasse 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 principal do jogo estiver visível e não estiver ajustada ou desativada, o jogo continuará a atualizar e renderizar 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 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 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 junto com os retângulos para o controlador de aparência de movimento da tela sensível ao toque. O texto da pontuação é desenhado em ambos os estados. Para obter mais informações, confira 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 seguintes.
- 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, em seguida, invoca outra corrotina no renderizador para carregar os recursos de nível específicos do dispositivo. Esse método é executado em um thread separado; como resultado, somente os métodos ID3D11Device (em oposição aos métodos ID3D11DeviceContext ) podem ser chamados desse thread. Todos os métodos de contexto do dispositivo são chamados no método FinalizeLoadLevel . Se você não estiver familiarizado com a programação assíncrona, confira Simultaneidade e operações assíncronas com C++/WinRT.
- FinalizeLoadLevel. Conclui qualquer trabalho de carregamento de nível que precise ser feito no thread principal. Isso inclui todas as chamadas para métodos de contexto de dispositivo Direct3D 11 (ID3D11DeviceContext).
- StartLevel. Inicia a jogabilidade para um novo nível.
- Pausar o jogo. Pausa o jogo.
- Corra o jogo. 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 for Ativo.
- OnSuspending e OnResumeng. 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. Carregue/salve a pontuação mais alta em todos os jogos, respectivamente.
- InicializeMunição. Redefine o estado de cada objeto esférico usado como munição de volta 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 prontas, 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 sobreposição para a estrutura do aplicativo para exibição.
Membros de dados
Esses objetos são atualizados à medida que o loop do jogo é executado.
- MoveLookController . Representa a entrada do jogador. Para obter mais informações, confira Adicionar controles.
- GameRenderer . 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 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 dados e restrições específicos do jogo.
Próximas etapas
Ainda temos que falar sobre o mecanismo de renderização real — como as chamadas para os métodos Render nos primitivos atualizados são transformadas em pixels na 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 Adicionando controles.