Estrutura do aplicativo Marble Maze

Applies to Windows only

A estrutura de um aplicativo da Windows Store DirectX é diferente daquela de um aplicativo de área de trabalho tradicional. Em vez de funcionar com tipos de identificador como HWND e funções como CreateWindow, o Tempo de Execução do Windows oferece interfaces como Windows::UI::Core::ICoreWindow para que seja possível desenvolver aplicativos da Windows Store com o uso de um método mais moderno orientado a objetos. Esta seção da documentação mostra como o código do aplicativo Marble Maze está estruturado.

Observação  O código de amostra que corresponde a esse documento está disponível em Amostra do jogo DirectX Marble Maze.

Neste tópico

Veja a seguir alguns dos pontos-chave que este documento discute para quando você estruturar o código do seu jogo:

  • Na fase de inicialização, configure componentes de tempo de execução e biblioteca usados pelo jogo. Carregue também os recursos específicos do jogo.
  • Aplicativos da Windows Store devem iniciar o processamento de eventos em até 5 segundos após a inicialização. Portanto, carregue apenas os recursos essenciais ao carregar seu aplicativo. Os jogos devem carregar recursos grandes 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. (Estes substituem as mensagens de janela de aplicativos de desktop do Windows.)
  • Use uma máquina de estados para controlar o fluxo e a ordem da lógica do jogo.

Organização de arquivos

Alguns dos componentes no 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 fornecidas por esses arquivos. A tabela a seguir descreve resumidamente os arquivos de código-fonte importantes.

ArquivosDescrição
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.hDefine estruturas e funções que ajudam você a trabalhar com cálculos e dados de vetor e matriz. Muitas dessas funções são compatíveis com tipos de sombreadores HLSL.
BasicReaderWriter.h, BasicReaderWriter.cpp Define a classe BasicReaderWriter, que usa o Tempo de Execução do Windows para ler e gravar dados de arquivos em um aplicativo da Windows Store.
BasicShapes.h, BasicShapes.cpp Define a classe BasicShapes, que fornece métodos utilitário para a criação de formas básicas, como cubos e esferas. (Esses arquivos não são usados pela implementação do Marble Maze).
BasicTimer.h, BasicTimer.cpp Define a classe BasicTimer, que fornece uma maneira fácil de obter totais e tempos decorridos.
Camera.h, Camera.cpp Define a classe Camera, que fornece a posição e a orientação de uma câmara.
Collision.h, Collision.cpp Gerencia informações de colisão entre a bolinha e outros objetos, como o labirinto.
DDSTextureLoader.h, DDSTextureLoader.cpp Define a função CreateDDSTextureFromMemory, que carrega texturas no formato .dds a partir de um buffer de memória.
DirectXApp.h, DirectXApp.cpp Define as classes DirectXApp e DirectXAppSource, que encapsulam a exibição (janela, thread e eventos) do aplicativo.
DirectXBase.h, DirectXBase.cpp Define a classe DirectXBase, que fornece a infraestrutura comum a muitos aplicativos DirectX da Windows Store.
DirectXSample.hDefine as funções utilitárias que podem ser usadas por aplicativos DirectX da Windows Store.
LoadScreen.h, LoadScreen.cpp Define a classe LoadScreen, que exibe uma tela de carregamento durante a inicialização do aplicativo.
MarbleMaze.h, MarbleMaze.cpp Define a classe MarbleMaze, que administra recursos específicos do jogo e define muito 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.cppDefine a classe PersistentState, que lê e escreve tipos de dados primitivos de e em um repositório de suporte.
Physics.h, Physics.cpp Define a classe Physics, que implementa a simulação física entre o mármore e o labirinto.
Primitives.h Define tipos geométricos que são usados pelo jogo.
SampleOverlay.h, SampleOverlay.cpp Define a classe SampleOverlay, que fornece dados e operações comuns de interface de usuário e 2D.
SDKMesh.h, SDKMesh.cpp Define a classe SDKMesh, que carrega e renderiza malhas no formato de Malha SDK (.sdkmesh).
UserInterface.h, UserInterface.cpp Define a funcionalidade relacionada à interface do usuário, como o sistema de menus e a tabela de pontuações altas.

 

Formatos de recursos de tempo de design versus tempo de execução

Quando você puder, use formatos de tempo de execução em vez de formatos de tempo de design para carregar de forma mais eficiente os recursos do jogo.

Um formato de tempo de design é o formato usado ao projetar um recurso. Normalmente, os designers de 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 de texto. 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 lido pelo jogo. Formatos de tempo de execução são normalmente mais compactos e mais eficientes de carregar do que os formatos de tempo de design correspondentes. É por isso que a maioria dos jogos usa ativos de tempo de execução em tempo de execução.

Embora o jogo possa ler diretamente um formato de tempo de design, existem várias vantagens de se usar um formato de tempo de execução separado. Como formatos de tempo de execução são muitas vezes mais compacto, eles exigem menos espaço em disco e menos tempo para serem transferidos através de uma rede. Além disso, formatos de tempo de execução são muitas vezes representados como estruturas de dados mapeadas para a memória. Portanto, eles podem ser carregados na memória com muito mais rapidez do que, por exemplo, um arquivo de texto baseado em XML. Por mim, como formatos de tempo de execução separados são geralmente codificados como binários, eles são mais difíceis de serem modificados pelo usuário final.

Sombreadores HLSL são um exemplo de recursos que usam diferentes formatos de tempo de design e de tempo de execução. O Marble Maze usa o .hlsl como o formato de tempo de design e o .cso como o formato de tempo de execução. Um arquivo .hlsl tem um código-fonte para o 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 seu jogo, você evita a necessidade de converter os arquivos de origem HLSL no código de byte quando o jogo é carregado.

Por motivos didáticos, o projeto Marble Maze inclui tanto o formato de tempo de design quanto 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 o seu próprio jogo, pois será possível convertê-los em formatos de tempo de execução quando necessário. Esta documentação mostra como converter os formatos de tempo de design nos formatos de tempo de execução.

Ciclo de vida do aplicativo

O Marble Maze segue o ciclo de vida de um aplicativo típico da Windows Store. Para saber mais sobre o ciclo de vida um aplicativo da Windows Store, veja Ciclo de vida do aplicativo.

Quando um jogo da Windows Store é inicializado, ele normalmente inicializa componentes de tempo de execução, como o Direct3D, o Direct2D e qualquer biblioteca de entrada, áudio ou física que precisa usar. Ele também carrega recursos específicos que são necessários antes de o jogo começar. Essa inicialização ocorre uma única vez durante uma sessão do jogo.

Após a inicialização, os jogos geralmente executam o loop de jogo. Nesse loop, os jogos normalmente realizam quatro ações: processam eventos do Windows, coleta a entrada, atualizam objetos de cena e renderizam 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 através da rede. Quando o jogo renderiza a cena, ele captura o estado atual da cena e a desenha no dispositivo de vídeo. As seguintes seções descrevem essas atividades com mais detalhes.

Adicionando ao modelo

O modelo DirectX App cria uma janela principal que pode ser renderizada com o Direct3D. O modelo também inclui a classe DeviceResources que cria todos os recursos do dispositivo Direct3D necessários para a renderização de conteúdo 3D em um aplicativo da Windows Store. A classe AppMain cria o objeto de classe MarbleMaze, inicia o carregamento de recursos, executa loops para atualizar o temporizador e chama o método de renderização MarbleMaze para cada quadro. Os métodos de CreateWindowSizeDependentResources, Atualização e Renderização dessa classe chamam os métodos correspondentes da classe MarbleMaze. O exemplo a seguir mostra onde o construtor AppMain cria o objeto de classe MarbleMaze. A classe de recursos do dispositivo é transferida para a classe, para que ela possa usar os objetos Direct3D para renderização.


    m_marbleMaze = std::unique_ptr<MarbleMaze>(new MarbleMaze(m_deviceResources));
    m_marbleMaze->CreateWindowSizeDependentResources();


A classe AppMain também começa a carregar os recursos adiados para o jogo. Veja a próxima seção para saber mais detalhes. O construtor DirectXPage configura os manipuladores de eventos e cria as classes DeviceResources e AppMain.

Quando os manipuladores desses eventos são chamados, eles transferem a entrada para a classe MarbleMaze.

Carregando ativos de jogo em segundo plano

Para garantir que o seu jogo possa responder a eventos de janela em até 5 segundos depois de ser iniciado, convém carregar os ativos do jogo de maneira assíncrona ou em segundo plano. Como 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 ele estiver pronto e permitir que os ativos restantes continuem a ser carregados em segundo plano. Se o usuário selecionar uma opção no menu antes de todos os recursos serem carregados, você poderá indicar que os recursos da cena ainda estão sendo carregados exibindo uma barra de progresso, por exemplo.

Mesmo que o jogo contenha relativamente poucos ativos de jogo, uma prática recomendada é 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 configurações. Além disso, incorporando o carregamento assíncrono com antecedência, seu código estará pronto para ser escalado à medida que você adicionar funcionalidades.

O carregamento de ativos assíncrono começa com o método AppMain::Load. Esse método usa a classe task Class (Concurrency Runtime) para carregar ativos de jogo em segundo plano.



    task<void>([=]()
    {
        m_marbleMaze->LoadDeferredResources();
    });



A classe MarbleMaze define o sinalizador m_deferredResourcesReady para indicar que o carregamento assíncrono está concluído. O método MarbleMaze::LoadDeferredResources carrega os recursos do jogo e depois define esse sinalizador. As fases de atualização (MarbleMaze::Update) e renderização (MarbleMaze::Render) do aplicativo verificam esse sinalizador. Quando esse sinalizador está definido, o jogo continua normalmente. Se o sinalizador ainda não estiver definido, o jogo mostrará a tela de carregamento.

Para saber mais sobre programação assíncrona para aplicativos da Windows Store, veja Programação assíncrona em C++.

Dica  Se você estiver escrevendo um código de jogo que faz parte de uma Biblioteca C++ do Tempo de Execução do Windows (em outras palavras, uma DLL), considere a leitura de Criando operações assíncronas em C++ para aplicativos da Windows Store para aprender a criar operações assíncronas que podem ser consumidas por aplicativos e outras bibliotecas.

O loop de jogo

O método DirectPage::OnRendering executa o loop de jogo principal. Esse método é chamado a cada quadro.

Para ajudar a separar o código de janela e exibição do código específico do jogo, implementamos o método DirectXApp::Run para encaminhar chamadas de atualização e renderização ao objeto MarbleMaze. O método DirectPage::OnRendering também define o temporizador do jogo, que é usado para a simulação de animações e da física.

O exemplo a seguir mostra o método DirectPage::OnRendering, que inclui o loop de jogo principal. O loop de jogo atualiza as variáveis de tempo total e de tempo de quadros e depois atualiza e renderiza a cena. Isso também garante que o conteúdo seja renderizado apenas quando a janela estiver visível.




void DirectXPage::OnRendering(Object^ sender, Object^ args)
{
    if (m_windowVisible)
    {
        m_main->Update();

        if (m_main->Render())
        {
            m_deviceResources->Present();
        }
    }
}


A máquina de estados

Em geral, jogos contêm uma máquina de estados (também conhecida como máquina de estados finitos, ou FSM) para controlar o fluxo e a ordem da lógica do jogo. A máquina de estados contém um determinado número de estados e tem a capacidade de transicionar entre eles. Uma máquina de estados normalmente começa em um estado inicial, faz transições para um ou mais estados intermédios e termina possivelmente em um estado terminal.

Geralmente, um loop de jogo usa uma máquina de estados para poder realizar a lógica específica para o 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 está visível e que o jogo não está ativo. De forma contrária, o estado InGameActive define que o jogo está ativo e que o menu não está visível. A classe MarbleMaze define a variável de membro m_gameState para manter o estado ativo do jogo.

Os métodos MarbleMaze::Update e MarbleMaze::Render usam a instrução "switch" para realizar a lógica do estado atual. O exemplo a seguir mostra uma possível aparência dessa instrução "switch" para o método MarbleMaze::Update (os detalhes foram 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 do jogo ou a renderização depende de um estado de jogo específico, enfatizamos isso nesta documentação.

Manipulando eventos de janela e aplicativo

O Tempo de Execução do Windows fornece um sistema de manipulação de eventos orientado a objetos para que você possa gerenciar mensagens do Windows com mais facilidade. Para consumir um evento em um aplicativo, você deve fornecer um manipulador de eventos, ou o método de manipulação de eventos, que responde a esse evento. Você também deve registrar o manipulador de eventos na origem do evento. Esse processo é geralmente conhecido como conexão de eventos.

Dando suporte para processos de suspensão, retomada e reinicialização

O Marble Maze é suspenso quando o usuário alterna para outro aplicativo ou quando o Windows entra em um estado de baixo consumo de 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 precisa dos recursos que o aplicativo está usando, como a memória. O Windows notifica um aplicativo quando ele está prestes a ser suspenso ou retomado, mas não o notifica quando ele está prestes a ser encerrado. Portanto, seu aplicativo deve ser capaz de salvar (no momento em que o Windows notificar o aplicativo de que ele está prestes a ser suspenso) todos os dados necessários para restaurar o estado do usuário atual quando o aplicativo for reiniciado. Se o seu aplicativo tiver um estado de usuário significativo que é importante salvar, talvez também seja necessário salvar o estado regularmente, mesmo antes de o aplicativo receber a notificação de suspensão. O Marble Maze responde a notificações de suspensão e retomada notificações por dois motivos:

  1. Quando o aplicativo é suspenso, o jogo salva o estado atual e interrompe a reprodução do áudio. Quando o aplicativo é retomado, o jogo retoma a reprodução do áudio.
  2. Quando o aplicativo é fechado e reiniciado mais tarde, o jogo recomeça a partir do seu estado anterior.

O Marble Maze realiza as seguintes tarefas para dar suporte aos processos de suspensão e retomada:

  • Ele salva seu estado no armazenamento persistente em pontos-chave do jogo, como quando o usuário atinge um determinado ponto de controle.
  • Ele responde a notificações de suspensão salvando seu estado no armazenamento persistente.
  • Ele responde a notificações de retomada carregando seu estado a partir do armazenamento persistente. Ele também carrega o estado anterior durante a inicialização.

Para dar suporte a processos de 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 fazem a leitura e a gravação de tipos de dados primitivos (como bool, int, float, XMFLOAT3 e Platform::String) em um repositório de suporte.


ref class PersistentState
{
public:
    void Initialize(
        _In_ Windows::Foundation::Collections::IPropertySet^ m_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 MarbleMaze contém um objeto PersistentState. O construtor MarbleMaze inicializa o objeto e fornece o repositório de dados do aplicativo local como o repositório de dados de suporte.


m_persistentState = ref new PersistentState();
m_persistentState->Initialize(ApplicationData::Current->LocalSettings->Values, "MarbleMaze");

O Marble Maze salva seu estado quando a bolinha passa sobre um ponto de controle ou sobre o alvo (no método MarbleMaze::Update) e quando a janela perde o foco (no método MarbleMaze::OnFocusChange). Se o seu jogo tem uma grande quantidade de dados de estado, convém ocasionalmente salvar o estado no armazenamento persistente de maneira semelhante, pois você tem apenas alguns segundos para responder à notificação de suspensão. Portanto, quando o seu aplicativo receber uma notificação de suspensão, ele só precisará salvar os dados que mudaram.

Para responder a notificações de suspensão e retomada, a classe DirectXPage define os métodos SaveInternalState e LoadInternalState que são chamados mediante a suspensão e retomada. O método MarbleMaze::OnSuspending manipula o evento de suspensão, enquanto o método MarbleMaze::OnResuming manipula o evento de retomada.

O método MarbleMaze::OnSuspending salva o estado do jogo e suspende o áudio.


void MarbleMaze::OnSuspending()
{
    SaveState();
    m_audio.SuspendAudio();
}


O método MarbleMaze::SaveState salva valores de estado do jogo, como a posição atual e a velocidade da bolinha, o ponto de controle mais recente e a tabela de pontuações altas.


void MarbleMaze::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 recomeça, ele só precisa retomar o áudio. Ele não precisa carregar o estado a partir do armazenamento persistente porque esse estado já está carregado na memória.

A forma como o jogo suspende e retoma o áudio é explicada no documento Adicionando áudio à amostra do Marble Maze.

Para dar suporte à reinicialização, o método MarbleMaze::Initialize, que é chamado durante a inicialização, chama o método MarbleMaze::LoadState. O método MarbleMaze::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 estava pausado ou ativo no momento em que foi suspenso. Pausamos o jogo para que o usuário não se surpreenda com atividades inesperadas. Além disso, o método move para o menu principal se o jogo não estava em estado de interação com o usuário no momento em que foi suspenso.


void MarbleMaze::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 faz distinção entre inicializar a frio — ou seja, começar pela primeira vez sem um evento de suspensão anterior — e retomar a partir de um estado suspenso. Este é um design recomendado para todos os aplicativos da Windows Store.

Para ver mais exemplos que demonstram como armazenar e recuperar arquivos e configurações do repositório de dados local, veja Guia de início rápido: dados de aplicativo locais. Para saber mais sobre dados de aplicativo, veja Acessando dados do aplicativo com o Tempo de Execução do Windows.

Próximas etapas

Leia Adicionando conteúdo visual à amostra do Marble Maze para saber mais sobre algumas das principais práticas que você deve ter em mente ao trabalhar com recursos visuais.

Tópicos relacionados

Adicionando conteúdo visual à amostra do Marble Maze
Princípios básicos da amostra do Marble Maze
Desenvolvendo o Marble Maze, um jogo da Windows Store em C++ e DirectX

 

 

Mostrar:
© 2014 Microsoft