Estrutura do aplicativo do Marble Maze
A estrutura de um aplicativo da Plataforma Universal do Windows (UWP) do DirectX difere da de um aplicativo da área de trabalho tradicional. Em vez de trabalhar com tipos de identificador como HWND e funções como CreateWindow, o Tempo de Execução do Windows fornece interfaces como Windows::UI::Core::ICoreWindow para que você possa desenvolver aplicativos UWP de uma maneira mais moderna e orientada a objetos. Esta seção da documentação mostra como o código do aplicativo Marble Maze é estruturado.
Observação
A amostra de código que corresponde a este documento pode ser encontrada na amostra do jogo Marble Maze no DirectX .
Aqui estão alguns dos principais pontos que este documento discute para quando você estrutura o código do jogo:
- Na fase de inicialização, configure os componentes de tempo de execução e biblioteca que seu jogo usa e carregue recursos específicos do jogo.
- Os aplicativos UWP devem começar a processar eventos dentro de 5 segundos após a inicialização. Portanto, carregue apenas os recursos essenciais ao carregar seu aplicativo. Os jogos devem carregar grandes recursos em segundo plano e exibir uma tela de progresso.
- No loop do jogo, responda a eventos do Windows, leia a entrada do usuário, atualize objetos de cena e renderize a cena.
- Use manipuladores de eventos para responder a eventos de janela. (Eles substituem as mensagens de janela de aplicativos da área de trabalho do Windows.)
- Use uma máquina de estado para controlar o fluxo e a ordem da lógica do jogo.
Organização de arquivos
Alguns dos componentes do Marble Maze podem ser reutilizados com qualquer jogo com pouca ou nenhuma modificação. Para seu próprio jogo, você pode adaptar a organização e as ideias que esses arquivos fornecem. A tabela a seguir descreve brevemente os arquivos de código-fonte importantes.
Arquivos | Descrição |
---|---|
App.h, App.cpp | Define as classes App e DirectXApplicationSource , que encapsulam a exibição (janela, thread e eventos) do aplicativo. |
Audio.h, Audio.cpp | Define a classe Audio , que gerencia recursos de áudio. |
BasicLoader.h, BasicLoader.cpp | Define a classe BasicLoader , que fornece métodos utilitários que ajudam a carregar texturas, malhas e sombreadores. |
BasicMath.h | Define estruturas e funções que ajudam você a trabalhar com dados e cálculos vetoriais e matriciais. Muitas dessas funções são compatíveis com os tipos de sombreador HLSL. |
BasicReaderWriter.h, BasicReaderWriter.cpp | Define a classe BasicReaderWriter , que usa o Tempo de Execução do Windows para ler e gravar dados de arquivo em um aplicativo UWP. |
BasicShapes.h, BasicShapes.cpp | Define a classe BasicShapes , que fornece métodos utilitários para criar formas básicas, como cubos e esferas. (Esses arquivos não são usados pela implementação do Marble Maze). |
Camera.h, Camera.cpp | Define a classe Camera , que fornece a posição e a orientação de uma câmera. |
Collision.h, Collision.cpp | Gerencia informações de colisão entre a bola de gude e outros objetos, como o labirinto. |
DDSTextureLoader.h, DDSTextureLoader.cpp | Define a função CreateDDSTextureFromMemory , que carrega texturas que estão no formato .dds de um buffer de memória. |
DirectXHelper.h | Define funções auxiliares do DirectX que são úteis para muitos aplicativos UWP do DirectX. |
LoadScreen.h, LoadScreen.cpp | Define a classe LoadScreen, que exibe uma tela de carregamento durante a inicialização do aplicativo. |
MarbleMazeMain.h, MarbleMazeMain.cpp | Define a classe MarbleMazeMain , que gerencia recursos específicos do jogo e define grande parte da lógica do jogo. |
MediaStreamer.h, MediaStreamer.cpp | Define a classe MediaStreamer , que usa o Media Foundation para ajudar o jogo a gerenciar recursos de áudio. |
PersistentState.h, PersistentState.cpp | Define a classe PersistentState , que lê e grava tipos de dados primitivos de e para um repositório de backup. |
Física.h, Physics.cpp | Define a aula de Física , que implementa a simulação de física entre a bola de gude e o labirinto. |
Primitivos.h | Define os tipos geométricos usados pelo jogo. |
SampleOverlay.h, SampleOverlay.cpp | Define a classe SampleOverlay , que fornece dados e operações comuns de 2D e interface do usuário. |
SDKMesh.h, SDKMesh.cpp | Define a classe SDKMesh , que carrega e renderiza malhas que estão no formato SDK Mesh (.sdkmesh). |
StepTimer.h | Define a classe StepTimer , que fornece uma maneira fácil de obter os tempos totais e decorridos. |
UserInterface.h, UserInterface.cpp | Define a funcionalidade relacionada à interface do usuário, como o sistema de menus e a tabela de pontuação mais alta. |
Formatos de recursos de tempo de design versus tempo de execução
Quando puder, use formatos de tempo de execução em vez de formatos de tempo de design para carregar recursos de jogo com mais eficiência.
Um formato de tempo de design é o formato que você usa ao projetar seu recurso. Normalmente, os designers 3D trabalham com formatos de tempo de design. Alguns formatos de tempo de design também são baseados em texto para que você possa modificá-los em qualquer editor baseado em texto. Os formatos de tempo de design podem ser detalhados e conter mais informações do que o jogo exige. Um formato de tempo de execução é o formato binário que é lido pelo seu jogo. Os formatos de tempo de execução geralmente são mais compactos e mais eficientes de carregar do que os formatos de tempo de design correspondentes. É por isso que a maioria dos jogos usa recursos de tempo de execução em tempo de execução.
Embora seu jogo possa ler diretamente um formato de tempo de design, há vários benefícios em usar um formato de tempo de execução separado. Como os formatos de tempo de execução geralmente são mais compactos, eles exigem menos espaço em disco e menos tempo para serem transferidos por uma rede. Além disso, os formatos de tempo de execução geralmente são representados como estruturas de dados mapeadas na memória. Portanto, eles podem ser carregados na memória muito mais rápido do que, por exemplo, um arquivo de texto baseado em XML. Por fim, como os formatos de tempo de execução separados geralmente são codificados em binário, eles são mais difíceis de modificar para o usuário final.
Os sombreadores HLSL são um exemplo de recursos que usam diferentes formatos de tempo de design e tempo de execução. O Marble Maze usa .hlsl como o formato de tempo de design e .cso como o formato de tempo de execução. Um arquivo .hlsl contém o código-fonte do sombreador; Um arquivo .cso contém o código de byte do sombreador correspondente. Ao converter arquivos .hlsl offline e fornecer arquivos .cso com o jogo, você evita a necessidade de converter arquivos de origem HLSL em código de byte quando o jogo é carregado.
Por motivos instrucionais, o projeto Marble Maze inclui o formato de tempo de design e o formato de tempo de execução para muitos recursos, mas você só precisa manter os formatos de tempo de design no projeto de origem para seu próprio jogo, pois você pode convertê-los em formatos de tempo de execução quando precisar deles. Esta documentação mostra como converter os formatos de tempo de design para os formatos de tempo de execução.
Ciclo de vida do aplicativo
O Marble Maze segue o ciclo de vida de um aplicativo UWP típico. Para obter mais informações sobre o ciclo de vida de um aplicativo UWP, consulte Ciclo de vida do aplicativo.
Quando um jogo UWP é inicializado, ele normalmente inicializa componentes de runtime, como Direct3D, Direct2D e quaisquer bibliotecas de entrada, áudio ou física que ele usa. Ele também carrega recursos específicos do jogo que são necessários antes do início do jogo. Essa inicialização ocorre uma vez durante uma sessão de jogo.
Após a inicialização, os jogos normalmente executam o loop do jogo. Nesse loop, os jogos normalmente executam quatro ações: processar eventos do Windows, coletar entrada, atualizar objetos de cena e renderizar a cena. Quando o jogo atualiza a cena, ele pode aplicar o estado de entrada atual aos objetos de cena e simular eventos físicos, como colisões de objetos. O jogo também pode realizar outras atividades, como reproduzir efeitos sonoros ou enviar dados pela rede. Quando o jogo renderiza a cena, ele captura o estado atual da cena e o desenha para o dispositivo de exibição. As seções a seguir descrevem essas atividades com mais detalhes.
Adicionando ao modelo
O modelo de aplicativo DirectX 11 (Universal Windows) cria uma janela principal para a qual você pode renderizar com o Direct3D. O modelo também inclui a classe DeviceResources que cria todos os recursos de dispositivo Direct3D necessários para renderizar conteúdo 3D em um aplicativo UWP.
A classe App cria o objeto de classe MarbleMazeMain , inicia o carregamento de recursos, faz loops para atualizar o temporizador e chama o método MarbleMazeMain::Render a cada quadro. Os métodos App::OnWindowSizeChanged, App::OnDpiChanged e App::OnOrientationChanged chamam o método MarbleMazeMain::CreateWindowSizeDependentResources e o método App::Run chama os métodos MarbleMazeMain::Update e MarbleMazeMain::Render .
O exemplo a seguir mostra onde o método App::SetWindow cria o objeto de classe MarbleMazeMain . A classe DeviceResources é passada para o método para que ele possa usar os objetos Direct3D para renderização.
m_main = std::unique_ptr<MarbleMazeMain>(new MarbleMazeMain(m_deviceResources));
A classe App também começa a carregar os recursos adiados para o jogo. Consulte a próxima seção para obter mais detalhes.
Além disso, a classe App configura os manipuladores de eventos para os eventos CoreWindow . Quando os manipuladores desses eventos são chamados, eles passam a entrada para a classe MarbleMazeMain .
Carregando ativos do jogo em segundo plano
Para garantir que seu jogo possa responder a eventos de janela dentro de 5 segundos após o lançamento, recomendamos que você carregue seus ativos de jogo de forma assíncrona ou em segundo plano. À medida que os ativos são carregados em segundo plano, seu jogo pode responder a eventos de janela.
Observação
Você também pode exibir o menu principal quando estiver pronto e permitir que os ativos restantes continuem sendo carregados em segundo plano. Se o usuário selecionar uma opção no menu antes que todos os recursos sejam carregados, você poderá indicar que os recursos da cena continuam a ser carregados exibindo uma barra de progresso, por exemplo.
Mesmo que seu jogo contenha relativamente poucos ativos de jogo, é uma boa prática carregá-los de forma assíncrona por dois motivos. Um dos motivos é que é difícil garantir que todos os seus recursos serão carregados rapidamente em todos os dispositivos e em todas as configurações. Além disso, ao incorporar o carregamento assíncrono antecipadamente, seu código está pronto para ser dimensionado à medida que você adiciona funcionalidade.
O carregamento assíncrono de ativos começa com o método App::Load . Esse método usa a classe task para carregar ativos do jogo em segundo plano.
task<void>([=]()
{
m_main->LoadDeferredResources(true, false);
});
A classe MarbleMazeMain define o sinalizador m_deferredResourcesReady para indicar que o carregamento assíncrono foi concluído. O método MarbleMazeMain::LoadDeferredResources carrega os recursos do jogo e define esse sinalizador. As fases de atualização (MarbleMazeMain::Update) e renderização (MarbleMazeMain::Render) do aplicativo verificam esse sinalizador. Quando esta bandeira é definida, o jogo continua normalmente. Se a bandeira ainda não estiver definida, o jogo mostra a tela de carregamento.
Para obter mais informações sobre programação assíncrona para aplicativos UWP, consulte Programação assíncrona em C++.
Dica
Se você estiver escrevendo código de jogo que faz parte de uma Biblioteca C++ do Tempo de Execução do Windows (em outras palavras, uma DLL), considere ler Criando operações assíncronas em C++ para aplicativos UWP para saber como criar operações assíncronas que podem ser consumidas por aplicativos e outras bibliotecas.
O loop do jogo
O método App::Run executa o loop principal do jogo (MarbleMazeMain::Update). Esse método é chamado de cada quadro.
Para ajudar a separar o código de exibição e janela do código específico do jogo, implementamos o método App::Run para encaminhar chamadas de atualização e renderização para o objeto MarbleMazeMain .
O exemplo a seguir mostra o método App::Run , que inclui o loop do jogo principal. O loop do jogo atualiza as variáveis de tempo total e tempo do quadro e, em seguida, atualiza e renderiza a cena. Isso também garante que o conteúdo seja renderizado apenas quando a janela estiver visível.
void App::Run()
{
while (!m_windowClosed)
{
if (m_windowVisible)
{
CoreWindow::GetForCurrentThread()->Dispatcher->
ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
m_main->Update();
if (m_main->Render())
{
m_deviceResources->Present();
}
}
else
{
CoreWindow::GetForCurrentThread()->Dispatcher->
ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
}
}
// The app is exiting so do the same thing as if the app were being suspended.
m_main->OnSuspending();
#ifdef _DEBUG
// Dump debug info when exiting.
DumpD3DDebug();
#endif //_DEGBUG
}
A máquina de estado
Os jogos normalmente contêm uma máquina de estado (também conhecida como máquina de estado finito, ou FSM) para controlar o fluxo e a ordem da lógica do jogo. Uma máquina de estado contém um determinado número de estados e a capacidade de fazer a transição entre eles. Uma máquina de estado normalmente começa a partir de um estado inicial , faz a transição para um ou mais estados intermediários e, possivelmente, termina em um estado terminal .
Um loop de jogo geralmente usa uma máquina de estado para que possa executar a lógica específica do estado atual do jogo. O Marble Maze define a enumeração GameState , que define cada estado possível do jogo.
enum class GameState
{
Initial,
MainMenu,
HighScoreDisplay,
PreGameCountdown,
InGameActive,
InGamePaused,
PostGameResults,
};
O estado MainMenu , por exemplo, define que o menu principal aparece e que o jogo não está ativo. Por outro lado, o estado InGameActive define que o jogo está ativo e que o menu não aparece. A classe MarbleMazeMain define a variável de membro m_gameState para manter o estado ativo do jogo.
Os métodos MarbleMazeMain::Update e MarbleMazeMain::Render usam instruções switch para executar a lógica para o estado atual. O exemplo a seguir mostra a aparência de uma instrução switch para o método MarbleMazeMain::Update (os detalhes são removidos para ilustrar a estrutura).
switch (m_gameState)
{
case GameState::MainMenu:
// Do something with the main menu.
break;
case GameState::HighScoreDisplay:
// Do something with the high-score table.
break;
case GameState::PostGameResults:
// Do something with the game results.
break;
case GameState::InGamePaused:
// Handle the paused state.
break;
}
Quando a lógica ou a renderização do jogo dependem de um estado específico do jogo, enfatizamos isso nesta documentação.
Manipulando eventos de aplicativo e janela
O Tempo de Execução do Windows fornece um sistema de manipulação de eventos orientado a objetos para que você possa gerenciar mais facilmente as mensagens do Windows. Para consumir um evento em um aplicativo, você deve fornecer um manipulador de eventos, ou método de manipulação de eventos, que responda ao evento. Você também deve registrar o manipulador de eventos com a origem do evento. Esse processo é frequentemente chamado de fiação de eventos.
Suporte para suspensão, retomada e reinicialização
O Marble Maze é suspenso quando o usuário sai dele ou quando o Windows entra em um estado de baixa energia. O jogo é retomado quando o usuário o move para o primeiro plano ou quando o Windows sai de um estado de baixo consumo de energia. Geralmente, você não fecha aplicativos. O Windows pode encerrar o aplicativo quando ele está no estado suspenso e o Windows requer os recursos, como memória, que o aplicativo está usando. O Windows notifica um aplicativo quando ele está prestes a ser suspenso ou retomado, mas não notifica o aplicativo quando ele está prestes a ser encerrado. Portanto, seu aplicativo deve ser capaz de salvar, no momento em que o Windows notifica seu aplicativo de que ele está prestes a ser suspenso, todos os dados que seriam necessários para restaurar o estado atual do usuário quando o aplicativo for reiniciado. Se o app tiver um estado de usuário significativo que seja caro para salvar, talvez seja necessário salvar o estado regularmente, mesmo antes de o app receber a notificação de suspensão. O Marble Maze responde às notificações de suspensão e retomada por dois motivos:
- Quando o aplicativo é suspenso, o jogo salva o estado atual do jogo e pausa a reprodução de áudio. Quando o aplicativo é retomado, o jogo retoma a reprodução de áudio.
- Quando o aplicativo é fechado e reiniciado posteriormente, o jogo é retomado de seu estado anterior.
O Marble Maze executa as seguintes tarefas para dar suporte à suspensão e retomada:
- Ele salva seu estado em armazenamento persistente em pontos-chave do jogo, como quando o usuário chega a um ponto de verificação.
- Ele responde para suspender notificações salvando seu estado no armazenamento persistente.
- Ele responde às notificações de retomada carregando seu estado do armazenamento persistente. Ele também carrega o estado anterior durante a inicialização.
Para dar suporte à suspensão e retomada, o Marble Maze define a classe PersistentState . (Veja PersistentState.h e PersistentState.cpp). Essa classe usa a interface Windows::Foundation::Collections::IPropertySet para ler e gravar propriedades. A classe PersistentState fornece métodos que leem e gravam tipos de dados primitivos (como bool, int, float, XMFLOAT3 e Platform::String), de e para um repositório de backup.
ref class PersistentState
{
internal:
void Initialize(
_In_ Windows::Foundation::Collections::IPropertySet^ settingsValues,
_In_ Platform::String^ key
);
void SaveBool(Platform::String^ key, bool value);
void SaveInt32(Platform::String^ key, int value);
void SaveSingle(Platform::String^ key, float value);
void SaveXMFLOAT3(Platform::String^ key, DirectX::XMFLOAT3 value);
void SaveString(Platform::String^ key, Platform::String^ string);
bool LoadBool(Platform::String^ key, bool defaultValue);
int LoadInt32(Platform::String^ key, int defaultValue);
float LoadSingle(Platform::String^ key, float defaultValue);
DirectX::XMFLOAT3 LoadXMFLOAT3(
Platform::String^ key,
DirectX::XMFLOAT3 defaultValue);
Platform::String^ LoadString(
Platform::String^ key,
Platform::String^ defaultValue);
private:
Platform::String^ m_keyName;
Windows::Foundation::Collections::IPropertySet^ m_settingsValues;
};
A classe MarbleMazeMain contém um objeto PersistentState . O construtor MarbleMazeMain inicializa esse objeto e fornece o armazenamento de dados do aplicativo local como o armazenamento de dados de backup.
m_persistentState = ref new PersistentState();
m_persistentState->Initialize(
Windows::Storage::ApplicationData::Current->LocalSettings->Values,
"MarbleMaze");
O Marble Maze salva seu estado quando a bola de gude passa por um ponto de verificação ou a meta (no método MarbleMazeMain::Update ) e quando a janela perde o foco (no método MarbleMazeMain::OnFocusChange ). Se o jogo tiver uma grande quantidade de dados de estado, recomendamos que você salve ocasionalmente o estado no armazenamento persistente de maneira semelhante, pois você tem apenas alguns segundos para responder à notificação de suspensão. Portanto, quando seu aplicativo recebe uma notificação de suspensão, ele só precisa salvar os dados de estado que foram alterados.
Para responder às notificações de suspensão e retomada, a classe MarbleMazeMain define os métodos SaveState e LoadState que são chamados em suspender e retomar. O método MarbleMazeMain::OnSuspending manipula o evento de suspensão e o método MarbleMazeMain::OnResuming manipula o evento de retomada.
O método MarbleMazeMain::OnSuspending salva o estado do jogo e suspende o áudio.
void MarbleMazeMain::OnSuspending()
{
SaveState();
m_audio.SuspendAudio();
}
O método MarbleMazeMain::SaveState salva valores de estado do jogo, como a posição atual e a velocidade da bola de gude, o ponto de verificação mais recente e a tabela de pontuação mais alta.
void MarbleMazeMain::SaveState()
{
m_persistentState->SaveXMFLOAT3(":Position", m_physics.GetPosition());
m_persistentState->SaveXMFLOAT3(":Velocity", m_physics.GetVelocity());
m_persistentState->SaveSingle(
":ElapsedTime",
m_inGameStopwatchTimer.GetElapsedTime());
m_persistentState->SaveInt32(":GameState", static_cast<int>(m_gameState));
m_persistentState->SaveInt32(":Checkpoint", static_cast<int>(m_currentCheckpoint));
int i = 0;
HighScoreEntries entries = m_highScoreTable.GetEntries();
const int bufferLength = 16;
char16 str[bufferLength];
m_persistentState->SaveInt32(":ScoreCount", static_cast<int>(entries.size()));
for (auto iter = entries.begin(); iter != entries.end(); ++iter)
{
int len = swprintf_s(str, bufferLength, L"%d", i++);
Platform::String^ string = ref new Platform::String(str, len);
m_persistentState->SaveSingle(
Platform::String::Concat(":ScoreTime", string),
iter->elapsedTime);
m_persistentState->SaveString(
Platform::String::Concat(":ScoreTag", string),
iter->tag);
}
}
Quando o jogo é retomado, ele só precisa retomar o áudio. Ele não precisa carregar o estado do armazenamento persistente porque o estado já está carregado na memória.
Como o jogo suspende e retoma o áudio é explicado no documento Adicionando áudio ao exemplo do Marble Maze.
Para dar suporte à reinicialização, o construtor MarbleMazeMain , que é chamado durante a inicialização, chama o método MarbleMazeMain::LoadState . O método MarbleMazeMain::LoadState lê e aplica o estado aos objetos do jogo. Esse método também define o estado atual do jogo como pausado se o jogo foi pausado ou ativo quando foi suspenso. Pausamos o jogo para que o usuário não seja surpreendido por atividades inesperadas. Ele também se move para o menu principal se o jogo não estiver em um estado de jogo quando foi suspenso.
void MarbleMazeMain::LoadState()
{
XMFLOAT3 position = m_persistentState->LoadXMFLOAT3(
":Position",
m_physics.GetPosition());
XMFLOAT3 velocity = m_persistentState->LoadXMFLOAT3(
":Velocity",
m_physics.GetVelocity());
float elapsedTime = m_persistentState->LoadSingle(":ElapsedTime", 0.0f);
int gameState = m_persistentState->LoadInt32(
":GameState",
static_cast<int>(m_gameState));
int currentCheckpoint = m_persistentState->LoadInt32(
":Checkpoint",
static_cast<int>(m_currentCheckpoint));
switch (static_cast<GameState>(gameState))
{
case GameState::Initial:
break;
case GameState::MainMenu:
case GameState::HighScoreDisplay:
case GameState::PreGameCountdown:
case GameState::PostGameResults:
SetGameState(GameState::MainMenu);
break;
case GameState::InGameActive:
case GameState::InGamePaused:
m_inGameStopwatchTimer.SetVisible(true);
m_inGameStopwatchTimer.SetElapsedTime(elapsedTime);
m_physics.SetPosition(position);
m_physics.SetVelocity(velocity);
m_currentCheckpoint = currentCheckpoint;
SetGameState(GameState::InGamePaused);
break;
}
int count = m_persistentState->LoadInt32(":ScoreCount", 0);
const int bufferLength = 16;
char16 str[bufferLength];
for (int i = 0; i < count; i++)
{
HighScoreEntry entry;
int len = swprintf_s(str, bufferLength, L"%d", i);
Platform::String^ string = ref new Platform::String(str, len);
entry.elapsedTime = m_persistentState->LoadSingle(
Platform::String::Concat(":ScoreTime", string),
0.0f);
entry.tag = m_persistentState->LoadString(
Platform::String::Concat(":ScoreTag", string),
L"");
m_highScoreTable.AddScoreToTable(entry);
}
}
Importante
O Marble Maze não distingue entre a inicialização a frio, ou seja, o início pela primeira vez sem um evento de suspensão anterior, e a retomada de um estado suspenso. Esse é o design recomendado para todos os aplicativos UWP.
Para obter mais informações sobre dados de aplicativos, consulte Armazenar e recuperar configurações e outros dados de aplicativos.
Próximas etapas
Leia Adicionando conteúdo visual ao exemplo do Marble Maze para obter informações sobre algumas das principais práticas a serem lembradas ao trabalhar com recursos visuais.
Tópicos relacionados
- Adicionar conteúdo visual ao exemplo do Marble Maze
- Conceitos básicos de exemplo do Marble Maze
- Como desenvolver o Marble Maze, um jogo UWP em C++ no DirectX