Compartir a través de


Estructura de la aplicación Marble Maze

La estructura de una aplicación de directX Plataforma universal de Windows (UWP) difiere de la de una aplicación de escritorio tradicional. En lugar de trabajar con tipos de identificador como HWND y funciones como CreateWindow, Windows Runtime proporciona interfaces como Windows::UI::Core::ICoreWindow para que puedas desarrollar aplicaciones para UWP de forma más moderna y orientada a objetos. En esta sección de la documentación se muestra cómo se estructura el código de la aplicación Marble Maze.

Nota:

El código de ejemplo que corresponde a este documento se encuentra en el ejemplo de juego Marble Maze de DirectX.

Estos son algunos de los puntos clave que describe este documento para cuando estructura el código del juego:

  • En la fase de inicialización, configure los componentes de tiempo de ejecución y biblioteca que usa el juego y cargue recursos específicos del juego.
  • Las aplicaciones para UWP deben iniciar el procesamiento de eventos en un plazo de 5 segundos desde el inicio. Por lo tanto, cargue solo los recursos esenciales al cargar la aplicación. Los juegos deben cargar grandes recursos en segundo plano y mostrar una pantalla de progreso.
  • En el bucle del juego, responda a eventos de Windows, lea la entrada del usuario, actualice los objetos de escena y represente la escena.
  • Use controladores de eventos para responder a eventos de ventana. (Estos reemplazan los mensajes de ventana de las aplicaciones de Windows de escritorio).
  • Usa una máquina de estado para controlar el flujo y el orden de la lógica del juego.

Organización de archivos

Algunos de los componentes de Marble Maze se pueden reutilizar con cualquier juego con poca o ninguna modificación. Para su propio juego, puedes adaptar la organización y las ideas que proporcionan estos archivos. En la tabla siguiente se describen brevemente los archivos de código fuente importantes.

Archivos Descripción
App.h, App.cpp Define las clases App y DirectXApplicationSource , que encapsulan la vista (ventana, subproceso y eventos) de la aplicación.
Audio.h, Audio.cpp Define la clase Audio , que administra los recursos de audio.
BasicLoader.h, BasicLoader.cpp Define la clase BasicLoader , que proporciona métodos de utilidad que le ayudan a cargar texturas, mallas y sombreadores.
BasicMath.h Define estructuras y funciones que le ayudan a trabajar con datos y cálculos vectoriales y de matriz. Muchas de estas funciones son compatibles con los tipos de sombreador HLSL.
BasicReaderWriter.h, BasicReaderWriter.cpp Define la clase BasicReaderWriter , que usa Windows Runtime para leer y escribir datos de archivo en una aplicación para UWP.
BasicShapes.h, BasicShapes.cpp Define la clase BasicShapes , que proporciona métodos de utilidad para crear formas básicas como cubos y esferas. (La implementación de Marble Maze no usa estos archivos).
Camera.h, Camera.cpp Define la clase Camera , que proporciona la posición y la orientación de una cámara.
Collision.h, Collision.cpp Administra la información de colisión entre la canica y otros objetos, como el laberinto.
DDSTextureLoader.h, DDSTextureLoader.cpp Define la función CreateDDSTextureFromMemory , que carga texturas que están en .dds formato desde un búfer de memoria.
DirectXHelper.h Define las funciones auxiliares de DirectX que son útiles para muchas aplicaciones para UWP de DirectX.
LoadScreen.h, LoadScreen.cpp Define la clase LoadScreen, que muestra una pantalla de carga durante la inicialización de la aplicación.
MarbleMazeMain.h, MarbleMazeMain.cpp Define la clase MarbleMazeMain , que administra recursos específicos del juego y define gran parte de la lógica del juego.
MediaStreamer.h, MediaStreamer.cpp Define la clase MediaStreamer , que usa Media Foundation para ayudar al juego a administrar los recursos de audio.
PersistentState.h, PersistentState.cpp Define la clase PersistentState , que lee y escribe tipos de datos primitivos desde y en un almacén de respaldo.
Física.h, Physics.cpp Define la clase Física , que implementa la simulación física entre la canica y el laberinto.
Primitives.h Define los tipos geométricos que usa el juego.
SampleOverlay.h, SampleOverlay.cpp Define la clase SampleOverlay , que proporciona datos y operaciones comunes de interfaz de usuario y 2D.
SDKMesh.h, SDKMesh.cpp Define la clase SDKMesh , que carga y representa las mallas que están en formato sdk Mesh (.sdkmesh).
StepTimer.h Define la clase StepTimer , que proporciona una manera sencilla de obtener tiempos totales y transcurridos.
UserInterface.h, UserInterface.cpp Define la funcionalidad relacionada con la interfaz de usuario, como el sistema de menús y la tabla de puntuación alta.

 

Formatos de recursos en tiempo de diseño frente a en tiempo de ejecución

Cuando puedas, usa formatos en tiempo de ejecución en lugar de formatos en tiempo de diseño para cargar de forma más eficaz los recursos del juego.

Un formato en tiempo de diseño es el formato que se usa al diseñar el recurso. Normalmente, los diseñadores 3D funcionan con formatos en tiempo de diseño. Algunos formatos en tiempo de diseño también se basan en texto para que pueda modificarlos en cualquier editor basado en texto. Los formatos en tiempo de diseño pueden ser detallados y contener más información de lo que requiere tu juego. Un formato en tiempo de ejecución es el formato binario leído por el juego. Los formatos en tiempo de ejecución suelen ser más compactos y eficaces para cargarse que los formatos en tiempo de diseño correspondientes. Esta es la razón por la que la mayoría de los juegos usan recursos en tiempo de ejecución en tiempo de ejecución.

Aunque tu juego puede leer directamente un formato en tiempo de diseño, hay varias ventajas para usar un formato en tiempo de ejecución independiente. Dado que los formatos en tiempo de ejecución suelen ser más compactos, requieren menos espacio en disco y requieren menos tiempo para transferir a través de una red. Además, los formatos en tiempo de ejecución se suelen representar como estructuras de datos asignadas a memoria. Por lo tanto, se pueden cargar en memoria mucho más rápido que, por ejemplo, un archivo de texto basado en XML. Por último, dado que los formatos en tiempo de ejecución independientes suelen estar codificados en binarios, son más difíciles para que el usuario final modifique.

Los sombreadores HLSL son un ejemplo de recursos que usan diferentes formatos en tiempo de diseño y en tiempo de ejecución. Marble Maze usa .hlsl como formato en tiempo de diseño y .cso como formato en tiempo de ejecución. Un archivo .hlsl contiene código fuente para el sombreador; Un archivo .cso contiene el código de bytes del sombreador correspondiente. Cuando conviertes archivos .hlsl sin conexión y proporcionas archivos .cso con tu juego, evitas la necesidad de convertir archivos de código fuente HLSL en código byte cuando tu juego se cargue.

Por motivos informativos, el proyecto Marble Maze incluye tanto el formato en tiempo de diseño como el formato en tiempo de ejecución para muchos recursos, pero solo tienes que mantener los formatos en tiempo de diseño en el proyecto de origen para tu propio juego porque puedes convertirlos a formatos en tiempo de ejecución cuando los necesites. En esta documentación se muestra cómo convertir los formatos en tiempo de diseño a los formatos en tiempo de ejecución.

Ciclo de vida de la aplicación

Marble Maze sigue el ciclo de vida de una aplicación para UWP típica. Para obtener más información sobre el ciclo de vida de una aplicación para UWP, consulta Ciclo de vida de la aplicación.

Cuando se inicializa un juego para UWP, normalmente inicializa componentes en tiempo de ejecución como Direct3D, Direct2D y cualquier entrada, audio o bibliotecas físicas que use. También carga recursos específicos del juego necesarios antes de que comience el juego. Esta inicialización se produce una vez durante una sesión de juego.

Después de la inicialización, los juegos suelen ejecutar el bucle del juego. En este bucle, los juegos suelen realizar cuatro acciones: procesar eventos de Windows, recopilar entradas, actualizar objetos de escena y representar la escena. Cuando el juego actualiza la escena, puede aplicar el estado de entrada actual a los objetos de escena y simular eventos físicos, como colisiones de objetos. El juego también puede realizar otras actividades, como reproducir efectos de sonido o enviar datos a través de la red. Cuando el juego representa la escena, captura el estado actual de la escena y lo dibuja en el dispositivo de visualización. En las secciones siguientes se describen estas actividades con más detalle.

Agregar a la plantilla

La plantilla Aplicación DirectX 11 (Windows universal) crea una ventana principal a la que puede representar con Direct3D. La plantilla también incluye la clase DeviceResources que crea todos los recursos de dispositivo Direct3D necesarios para representar contenido 3D en una aplicación para UWP.

La clase App crea el objeto de clase MarbleMazeMain, inicia la carga de recursos, bucles para actualizar el temporizador y llama al método MarbleMazeMain::Render cada fotograma. Los métodos App::OnWindowSizeChanged, App::OnDpiChanged y App::OnOrientationChanged llaman al método MarbleMazeMain::CreateWindowSizeDependentResources y el método App::Run llama a los métodos MarbleMazeMain::Update y MarbleMazeMain::Render .

En el ejemplo siguiente se muestra dónde el método App::SetWindow crea el objeto de clase MarbleMazeMain . La clase DeviceResources se pasa al método para que pueda usar los objetos Direct3D para la representación.

    m_main = std::unique_ptr<MarbleMazeMain>(new MarbleMazeMain(m_deviceResources));

La clase App también comienza a cargar los recursos diferidos para el juego. Consulte la sección siguiente para obtener más detalles.

Además, la clase App configura los controladores de eventos para los eventos CoreWindow . Cuando se llama a los controladores de estos eventos, pasan la entrada a la clase MarbleMazeMain .

Carga de recursos del juego en segundo plano

Para asegurarte de que tu juego pueda responder a eventos de ventana en un plazo de 5 segundos después de iniciarlo, te recomendamos que cargues los recursos del juego de forma asincrónica o en segundo plano. A medida que los recursos se cargan en segundo plano, el juego puede responder a eventos de ventana.

Nota:

También puede mostrar el menú principal cuando esté listo y permitir que los recursos restantes continúen cargando en segundo plano. Si el usuario selecciona una opción en el menú antes de cargar todos los recursos, puede indicar que los recursos de la escena siguen cargando mostrando una barra de progreso, por ejemplo.

 

Incluso si tu juego contiene relativamente pocos recursos de juego, es recomendable cargarlos de forma asincrónica por dos motivos. Una razón es que es difícil garantizar que todos los recursos se cargarán rápidamente en todos los dispositivos y todas las configuraciones. Además, al incorporar la carga asincrónica temprano, el código está listo para escalar a medida que se agrega funcionalidad.

La carga de recursos asincrónica comienza con el método App::Load . Este método usa la clase de tarea para cargar los recursos del juego en segundo plano.

    task<void>([=]()
    {
        m_main->LoadDeferredResources(true, false);
    });

La clase MarbleMazeMain define la marca m_deferredResourcesReady para indicar que se ha completado la carga asincrónica. El método MarbleMazeMain::LoadDeferredResources carga los recursos del juego y, a continuación, establece esta marca. Las fases de actualización (MarbleMazeMain::Update) y representación (MarbleMazeMain::Render) de la aplicación comprueban esta marca. Cuando se establece esta marca, el juego continúa de la forma normal. Si la marca aún no está establecida, el juego muestra la pantalla de carga.

Para obtener más información sobre la programación asincrónica para aplicaciones para UWP, consulta Programación asincrónica en C++.

Sugerencia

Si estás escribiendo código de juego que forma parte de una biblioteca de C++ de Windows Runtime (es decir, un archivo DLL), considera si quieres leer Crear operaciones asincrónicas en C++ para aplicaciones para UWP para aprender a crear operaciones asincrónicas que las aplicaciones y otras bibliotecas pueden consumir.

 

Bucle del juego

El método App::Run ejecuta el bucle principal del juego (MarbleMazeMain::Update). Este método se denomina cada fotograma.

Para ayudar a separar la vista y el código de ventana del código específico del juego, hemos implementado el método App::Run para reenviar las llamadas de actualización y representación al objeto MarbleMazeMain .

En el ejemplo siguiente se muestra el método App::Run , que incluye el bucle principal del juego. El bucle del juego actualiza las variables de tiempo total y tiempo de período y, a continuación, actualiza y representa la escena. Esto también garantiza que el contenido solo se represente cuando la ventana esté visible.

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
}

La máquina de estado

Los juegos suelen contener una máquina de estado (también conocida como máquina de estado finito o FSM) para controlar el flujo y el orden de la lógica del juego. Una máquina de estado contiene un número determinado de estados y la capacidad de realizar la transición entre ellos. Normalmente, una máquina de estado comienza desde un estado inicial , realiza la transición a uno o varios estados intermedios y, posiblemente, termina en un estado terminal .

Un bucle de juego suele usar una máquina de estado para que pueda realizar la lógica específica del estado actual del juego. Marble Maze define la enumeración GameState , que define cada estado posible del juego.

enum class GameState
{
    Initial,
    MainMenu,
    HighScoreDisplay,
    PreGameCountdown,
    InGameActive,
    InGamePaused,
    PostGameResults,
};

El estado MainMenu , por ejemplo, define que aparece el menú principal y que el juego no está activo. Por el contrario, el estado InGameActive define que el juego está activo y que el menú no aparece. La clase MarbleMazeMain define la variable miembro m_gameState para contener el estado activo del juego.

Los métodos MarbleMazeMain::Update y MarbleMazeMain::Render usan instrucciones switch para realizar lógica para el estado actual. En el ejemplo siguiente se muestra el aspecto de una instrucción switch para el método MarbleMazeMain::Update (se quitan los detalles para ilustrar la estructura).

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;
}

Cuando la lógica o la representación del juego dependen de un estado de juego específico, lo destacamos en esta documentación.

Control de eventos de aplicación y ventana

Windows Runtime proporciona un sistema de control de eventos orientado a objetos para que pueda administrar más fácilmente los mensajes de Windows. Para consumir un evento en una aplicación, debe proporcionar un controlador de eventos o un método de control de eventos que responda al evento. También debe registrar el controlador de eventos con el origen del evento. Este proceso se conoce a menudo como cableado de eventos.

Compatibilidad con la suspensión, reanudación y reinicio

Marble Maze se suspende cuando el usuario cambia de él o cuando Windows entra en un estado de baja potencia. El juego se reanuda cuando el usuario lo mueve al primer plano o cuando Windows sale de un estado de baja potencia. Por lo general, no cierra las aplicaciones. Windows puede finalizar la aplicación cuando se encuentra en estado suspendido y Windows requiere los recursos, como la memoria, que la aplicación está usando. Windows notifica a una aplicación cuando está a punto de suspenderse o reanudarse, pero no notifica a la aplicación cuando está a punto de finalizarse. Por lo tanto, la aplicación debe ser capaz de guardar (en el momento en que Windows notifica a la aplicación que está a punto de suspenderse), los datos necesarios para restaurar el estado de usuario actual cuando se reinicia la aplicación. Si la aplicación tiene un estado de usuario significativo que es costoso de guardar, es posible que también tengas que guardar el estado regularmente, incluso antes de que la aplicación reciba la notificación de suspensión. Marble Maze responde a las notificaciones de suspensión y reanudación por dos razones:

  1. Cuando se suspende la aplicación, el juego guarda el estado actual del juego y pausa la reproducción de audio. Cuando se reanuda la aplicación, el juego reanuda la reproducción de audio.
  2. Cuando se cierra la aplicación y se reinicia más tarde, el juego se reanuda desde su estado anterior.

Marble Maze realiza las siguientes tareas para admitir la suspensión y reanudación:

  • Guarda su estado en el almacenamiento persistente en puntos clave del juego, como cuando el usuario llega a un punto de control.
  • Responde a las notificaciones de suspensión guardando su estado en el almacenamiento persistente.
  • Responde para reanudar las notificaciones cargando su estado desde el almacenamiento persistente. También carga el estado anterior durante el inicio.

Para admitir la suspensión y reanudación, Marble Maze define la clase PersistentState . (Consulte PersistentState.h y PersistentState.cpp). Esta clase usa la interfaz Windows::Foundation::Collections::IPropertySet para leer y escribir propiedades. La clase PersistentState proporciona métodos que leen y escriben tipos de datos primitivos (como bool, int, float, XMFLOAT3 y Platform::String), desde y en un almacén de respaldo.

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;
};

La clase MarbleMazeMain contiene un objeto PersistentState . El constructor MarbleMazeMain inicializa este objeto y proporciona el almacén de datos de la aplicación local como almacén de datos de respaldo.

m_persistentState = ref new PersistentState();

m_persistentState->Initialize(
    Windows::Storage::ApplicationData::Current->LocalSettings->Values,
    "MarbleMaze");

Marble Maze guarda su estado cuando la canica pasa por un punto de control o el objetivo (en el método MarbleMazeMain::Update ) y cuando la ventana pierde el foco (en el método MarbleMazeMain::OnFocusChange ). Si tu juego contiene una gran cantidad de datos de estado, te recomendamos que guardes ocasionalmente el estado en el almacenamiento persistente de forma similar porque solo tienes unos segundos para responder a la notificación de suspensión. Por lo tanto, cuando la aplicación recibe una notificación de suspensión, solo tiene que guardar los datos de estado que han cambiado.

Para responder a las notificaciones de suspensión y reanudación, la clase MarbleMazeMain define los métodos SaveState y LoadState a los que se llama en suspensión y reanudación. El método MarbleMazeMain::OnSuspending controla el evento suspend y el método MarbleMazeMain::OnResuming controla el evento resume.

El método MarbleMazeMain::OnSuspending guarda el estado del juego y suspende el audio.

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

El método MarbleMazeMain::SaveState guarda valores de estado del juego, como la posición actual y la velocidad de la canica, el punto de control más reciente y la tabla de puntuación 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);
    }
}

Cuando el juego se reanuda, solo tiene que reanudar el audio. No tiene que cargar el estado del almacenamiento persistente porque el estado ya está cargado en la memoria.

El modo en que el juego suspende y reanuda el audio se explica en el documento Agregar audio al ejemplo marble Maze.

Para admitir el reinicio, el constructor MarbleMazeMain , al que se llama durante el inicio, llama al método MarbleMazeMain::LoadState . El método MarbleMazeMain::LoadState lee y aplica el estado a los objetos del juego. Este método también establece el estado actual del juego en pausa si el juego estaba en pausa o activo cuando se suspendió. Pausamos el juego para que el usuario no se sorprenda de la actividad inesperada. También se mueve al menú principal si el juego no estaba en estado de juego cuando se suspendió.

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

Marble Maze no distingue entre el inicio en frío, es decir, a partir por primera vez sin un evento de suspensión anterior y reanudándose desde un estado suspendido. Este es el diseño recomendado para todas las aplicaciones para UWP.

Para obtener más información sobre los datos de la aplicación, consulta Almacenar y recuperar la configuración y otros datos de la aplicación.

Pasos siguientes

Lea Adición de contenido visual al ejemplo marble Maze para obtener información sobre algunas de las prácticas clave que se deben tener en cuenta al trabajar con recursos visuales.