Definir el marco de la aplicación para UWP del juego

Nota:

Este tema forma parte de la serie de tutoriales Crear un juego sencillo para la Plataforma universal de Windows (UWP) con DirectX. El tema de ese vínculo establece el contexto de la serie.

El primer paso para codificar un juego para la Plataforma universal de Windows (UWP) es crear el marco que permite que el objeto de aplicación interactúe con Windows, incluidas las características de Windows Runtime, como el control de eventos suspend-resume, los cambios en la visibilidad de la ventana y el ajuste.

Objetivos

  • Configure el marco para un juego DirectX de la Plataforma universal de Windows (UWP) e implemente la máquina de estado que define el flujo general del juego.

Nota:

Para seguir este tema, busque en el código fuente el juego de ejemplo Simple3DGameDX que ha descargado.

Introducción

En el tema Configurar el proyecto de juego, presentamos la función wWinMain, así como las interfaces IFrameworkViewSource e IFrameworkView. Hemos aprendido que la clase App (que puede ver definida en el archivo de código fuente App.cpp en el proyecto Simple3DGameDX) actúa como fábrica de view-provider y view-provider.

Este tema comienza desde ahí, y se incluyen muchos más detalles sobre cómo la clase App de un juego debe implementar los métodos de IFrameworkView.

El método App::Initialize

Tras el inicio de la aplicación, el primer método al que llama Windows es nuestra implementación de IFrameworkView::Initialize.

La implementación debe controlar los comportamientos más fundamentales de un juego para UWP, como asegurarse de que el juego pueda controlar un evento de suspensión (y una posible reanudación posterior) al suscribirse a dichos eventos. También tenemos acceso al dispositivo del adaptador de pantalla aquí, por lo que podemos crear recursos gráficos que dependen del dispositivo.

void Initialize(CoreApplicationView const& applicationView)
{
    applicationView.Activated({ this, &App::OnActivated });

    CoreApplication::Suspending({ this, &App::OnSuspending });

    CoreApplication::Resuming({ this, &App::OnResuming });

    // At this point we have access to the device. 
    // We can create the device-dependent resources.
    m_deviceResources = std::make_shared<DX::DeviceResources>();
}

Evite punteros sin procesar siempre que sea posible (y casi siempre es posible).

  • En el caso de los tipos de Windows Runtime, puede evitar punteros por completo y simplemente construir un valor en la pila. Si necesita un puntero, use winrt::com_ptr (veremos un ejemplo de esto pronto).
  • Para punteros únicos, use std::unique_ptr y std::make_unique.
  • Para punteros compartidos, use std::shared_ptr y std::make_shared.

El método App::SetWindow

Después de Initialize, Windows llama a nuestra implementación de IFrameworkView::SetWindow, pasando un objeto CoreWindow que representa la ventana principal del juego.

En App::SetWindow, nos suscribimos a eventos relacionados con ventanas y configuramos algunos comportamientos de ventana y visualización. Por ejemplo, creamos un puntero del mouse (a través de la clase CoreCursor), que se puede usar tanto en los controles táctiles como en el mouse. También pasamos el objeto window al objeto de recursos dependientes del dispositivo.

Hablaremos más sobre el control de eventos en el tema Administración de flujos de juegos.

void SetWindow(CoreWindow const& window)
{
    //CoreWindow window = CoreWindow::GetForCurrentThread();
    window.Activate();

    window.PointerCursor(CoreCursor(CoreCursorType::Arrow, 0));

    PointerVisualizationSettings visualizationSettings{ PointerVisualizationSettings::GetForCurrentView() };
    visualizationSettings.IsContactFeedbackEnabled(false);
    visualizationSettings.IsBarrelButtonFeedbackEnabled(false);

    m_deviceResources->SetWindow(window);

    window.Activated({ this, &App::OnWindowActivationChanged });

    window.SizeChanged({ this, &App::OnWindowSizeChanged });

    window.Closed({ this, &App::OnWindowClosed });

    window.VisibilityChanged({ this, &App::OnVisibilityChanged });

    DisplayInformation currentDisplayInformation{ DisplayInformation::GetForCurrentView() };

    currentDisplayInformation.DpiChanged({ this, &App::OnDpiChanged });

    currentDisplayInformation.OrientationChanged({ this, &App::OnOrientationChanged });

    currentDisplayInformation.StereoEnabledChanged({ this, &App::OnStereoEnabledChanged });

    DisplayInformation::DisplayContentsInvalidated({ this, &App::OnDisplayContentsInvalidated });
}

El método App::Load

Ahora que se establece la ventana principal, se llama a nuestra implementación de IFrameworkView::Load. Load es un mejor lugar para capturar previamente los datos o recursos del juego que Initialize y SetWindow.

void Load(winrt::hstring const& /* entryPoint */)
{
    if (!m_main)
    {
        m_main = winrt::make_self<GameMain>(m_deviceResources);
    }
}

Como puede ver, el trabajo real se delega al constructor del objeto GameMain que creamos aquí. La clase GameMain se define en GameMain.h y GameMain.cpp.

El constructor GameMain::GameMain

El constructor GameMain (y las demás funciones miembro a las que llama) comienza un conjunto de operaciones de carga asincrónicas para crear los objetos de juego, cargar recursos gráficos e inicializar la máquina de estado del juego. También realizamos cualquier preparación necesaria antes de que comience el juego, como establecer cualquier estado inicial o valores globales.

Windows impone un límite en el tiempo que el juego puede tardar antes de empezar a procesar la entrada. Por tanto, al usar async (como lo hacemos aquí), esto significa que Load puede devolver rápidamente mientras el trabajo que ha comenzado continúa en segundo plano. Si la carga tarda mucho tiempo o si hay muchos recursos, es una mejor idea proporcionar a los usuarios una barra de progreso que se actualice frecuentemente.

Si no está familiarizado con la programación asincrónica, consulte Operaciones asincrónicas y simultaneidad con C++/WinRT.

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) :
    m_deviceResources(deviceResources),
    m_windowClosed(false),
    m_haveFocus(false),
    m_gameInfoOverlayCommand(GameInfoOverlayCommand::None),
    m_visible(true),
    m_loadingCount(0),
    m_updateState(UpdateEngineState::WaitingForResources)
{
    m_deviceResources->RegisterDeviceNotify(this);

    m_renderer = std::make_shared<GameRenderer>(m_deviceResources);
    m_game = std::make_shared<Simple3DGame>();

    m_uiControl = m_renderer->GameUIControl();

    m_controller = std::make_shared<MoveLookController>(CoreWindow::GetForCurrentThread());

    auto bounds = m_deviceResources->GetLogicalSize();

    m_controller->SetMoveRect(
        XMFLOAT2(0.0f, bounds.Height - GameUIConstants::TouchRectangleSize),
        XMFLOAT2(GameUIConstants::TouchRectangleSize, bounds.Height)
        );
    m_controller->SetFireRect(
        XMFLOAT2(bounds.Width - GameUIConstants::TouchRectangleSize, bounds.Height - GameUIConstants::TouchRectangleSize),
        XMFLOAT2(bounds.Width, bounds.Height)
        );

    SetGameInfoOverlay(GameInfoOverlayState::Loading);
    m_uiControl->SetAction(GameInfoOverlayCommand::None);
    m_uiControl->ShowGameInfoOverlay();

    // Asynchronously initialize the game class and load the renderer device resources.
    // By doing all this asynchronously, the game gets to its main loop more quickly
    // and in parallel all the necessary resources are loaded on other threads.
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    auto lifetime = get_strong();

    m_game->Initialize(m_controller, m_renderer);

    co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);

    // The finalize code needs to run in the same thread context
    // as the m_renderer object was created because the D3D device context
    // can ONLY be accessed on a single thread.
    // co_await of an IAsyncAction resumes in the same thread context.
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();

    if (m_updateState == UpdateEngineState::WaitingForResources)
    {
        // In the middle of a game so spin up the async task to load the level.
        co_await m_game->LoadLevelAsync();

        // The m_game object may need to deal with D3D device context work so
        // again the finalize code needs to run in the same thread
        // context as the m_renderer object was created because the D3D
        // device context can ONLY be accessed on a single thread.
        m_game->FinalizeLoadLevel();
        m_game->SetCurrentLevelToSavedState();
        m_updateState = UpdateEngineState::ResourcesLoaded;
    }
    else
    {
        // The game is not in the middle of a level so there aren't any level
        // resources to load.
    }

    // Since Game loading is an async task, the app visual state
    // may be too small or not be activated. Put the state machine
    // into the correct state to reflect these cases.

    if (m_deviceResources->GetLogicalSize().Width < GameUIConstants::MinPlayableWidth)
    {
        m_updateStateNext = m_updateState;
        m_updateState = UpdateEngineState::TooSmall;
        m_controller->Active(false);
        m_uiControl->HideGameInfoOverlay();
        m_uiControl->ShowTooSmall();
        m_renderNeeded = true;
    }
    else if (!m_haveFocus)
    {
        m_updateStateNext = m_updateState;
        m_updateState = UpdateEngineState::Deactivated;
        m_controller->Active(false);
        m_uiControl->SetAction(GameInfoOverlayCommand::None);
        m_renderNeeded = true;
    }
}

void GameMain::InitializeGameState()
{
    // Set up the initial state machine for handling Game playing state.
    ...
}

Este es un esquema de la secuencia de trabajo que ha iniciado el constructor.

  • Cree e inicialice un objeto de tipo GameRenderer. Para más información, consulte Marco de representación I: Introducción a la representación.
  • Cree e inicialice un objeto de tipo Simple3DGame. Para más información, consulte Definir el objeto de juego principal.
  • Crea el objeto de control de interfaz de usuario del juego y muestra la superposición de información del juego para mostrar una barra de progreso a medida que se cargan los archivos de recursos. Para obtener más información, consulte Adición de una interfaz de usuario.
  • Cree un objeto de controlador para leer la entrada del controlador (táctil, mouse o controlador de juego). Para obtener más información, consulte Agregar controles.
  • Defina dos áreas rectangulares en las esquinas inferior izquierda e inferior derecha de la pantalla para los controles táctiles de movimiento y cámara, respectivamente. El reproductor usa el rectángulo inferior izquierdo (definido en la llamada a SetMoveRect) como un panel de control virtual para mover la cámara hacia delante y hacia atrás, y lateral a lado. El rectángulo inferior derecho (definido con el método SetFireRect) se usa como botón virtual para disparar munición.
  • Use corrutinas para dividir la carga de recursos en fases independientes. El acceso al contexto del dispositivo Direct3D está restringido al subproceso en el que se ha creado el contexto del dispositivo; mientras que el acceso al dispositivo Direct3D para la creación de objetos es libre. Por consiguiente, la corrutina GameRenderer::CreateGameDeviceResourcesAsync se puede ejecutar en un subproceso independiente de la tarea de finalización (GameRenderer::FinalizeCreateGameDeviceResources), que se ejecuta en el subproceso original.
  • Usamos un patrón similar para cargar recursos de nivel con Simple3DGame::LoadLevelAsync y Simple3DGame::FinalizeLoadLevel.

Veremos más de GameMain::InitializeGameState en el tema siguiente ( Administración de flujos de juegos).

El método App::OnActivated

Después, se genera el evento CoreApplicationView::Activated. Por tanto, se llama a cualquier controlador de eventos OnActivated que tenga (como nuestro método App::OnActivated).

void OnActivated(CoreApplicationView const& /* applicationView */, IActivatedEventArgs const& /* args */)
{
    CoreWindow window = CoreWindow::GetForCurrentThread();
    window.Activate();
}

El único trabajo que hacemos aquí es activar CoreWindow principal. Como alternativa, puede optar por hacerlo en App::SetWindow.

El método App::Run

Initialize, SetWindow y Load han establecido la fase. Ahora que el juego está en funcionamiento, se llama a nuestra implementación de IFrameworkView::Run.

void Run()
{
    m_main->Run();
}

De nuevo, el trabajo se delega a GameMain.

El método GameMain::Run

GameMain::Run es el bucle principal del juego; puede encontrarlo en GameMain.cpp. La lógica básica es que mientras la ventana del juego permanece abierta, distribuye todos los eventos, actualiza el temporizador y, después, representa y presenta los resultados de la canalización de gráficos. También aquí, los eventos usados para realizar la transición entre estados del juego se envían y procesan.

El código aquí también se preocupa por dos de los estados en la máquina de estado del motor de juego.

  • UpdateEngineState::Deactivated. Esto especifica que la ventana del juego está desactivada (ha perdido el foco) o se ajusta.
  • UpdateEngineState::TooSmall. Esto especifica que el área de cliente es demasiado pequeña para representar el juego.

En cualquiera de estos estados, el juego suspende el procesamiento de eventos y espera a que se active la ventana, que se desacople o que se cambie el tamaño.

Mientras la ventana del juego es visible (Window.Visible es true), debe controlar todos los eventos de la cola de mensajes a medida que llega, por lo que debe llamar a CoreWindowDispatch.ProcessEvents con la opción ProcessAllIfPresent. Otras opciones pueden provocar retrasos en el procesamiento de eventos de mensajes, lo que puede hacer que el juego no responda, o dar lugar a comportamientos táctiles que se sientan lentos.

Cuando el juego no esté visible (Window.Visible es false), o cuando se suspende, o cuando es demasiado pequeño (está acoplado), se preferirá que no consuma ningún recurso que vaya a enviar mensajes que nunca llegarán. En este caso, el juego debe usar la opción ProcessOneAndAllPending. Esa opción se bloquea hasta que obtiene un evento y, a continuación, procesa ese evento (así como cualquier otro que llegue a la cola de procesos durante el procesamiento del primero). CoreWindowDispatch.ProcessEvents vuelve inmediatamente después de procesar la cola.

En el código de ejemplo que se muestra a continuación, el miembro de datos m_visible representa la visibilidad de la ventana. Cuando se suspende el juego, su ventana no está visible. Cuando la ventana está visible, el valor de m_updateState (una enumeración UpdateEngineState) determina aún más si la ventana está desactivada (foco perdido), es demasiado pequeña (acoplada) o tiene el tamaño correcto.

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible)
        {
            switch (m_updateState)
            {
            case UpdateEngineState::Deactivated:
            case UpdateEngineState::TooSmall:
                if (m_updateStateNext == UpdateEngineState::WaitingForResources)
                {
                    WaitingForResourceLoading();
                    m_renderNeeded = true;
                }
                else if (m_updateStateNext == UpdateEngineState::ResourcesLoaded)
                {
                    // In the device lost case, we transition to the final waiting state
                    // and make sure the display is updated.
                    switch (m_pressResult)
                    {
                    case PressResultState::LoadGame:
                        SetGameInfoOverlay(GameInfoOverlayState::GameStats);
                        break;

                    case PressResultState::PlayLevel:
                        SetGameInfoOverlay(GameInfoOverlayState::LevelStart);
                        break;

                    case PressResultState::ContinueLevel:
                        SetGameInfoOverlay(GameInfoOverlayState::Pause);
                        break;
                    }
                    m_updateStateNext = UpdateEngineState::WaitingForPress;
                    m_uiControl->ShowGameInfoOverlay();
                    m_renderNeeded = true;
                }

                if (!m_renderNeeded)
                {
                    // The App is not currently the active window and not in a transient state so just wait for events.
                    CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
                    break;
                }
                // otherwise fall through and do normal processing to get the rendering handled.
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
                Update();
                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.
}

El método App::Uninitialize

Cuando finaliza el juego, se llama a nuestra implementación IFrameworkView::Uninitialize. Esta es nuestra oportunidad de realizar la limpieza. Cerrar la ventana de la aplicación no elimina el proceso de la aplicación; pero, en su lugar, escribe el estado del singleton de la aplicación en la memoria. Si algo especial debe ocurrir cuando el sistema reclama esta memoria, incluida cualquier limpieza especial de recursos, coloque el código para esa limpieza en Sin inicializar.

En nuestro caso, App::Uninitialize es una operación sin operación.

void Uninitialize()
{
}

Sugerencias

Al desarrollar su propio juego, diseñe el código de inicio en torno a los métodos descritos en este tema. A continuación, encontrará una lista con sugerencias básicas para cada método.

  • Use Initialize para asignar las clases principales y conectar los controladores de eventos básicos.
  • Use SetWindow para suscribirse a cualquier evento específico de la ventana y pasar la ventana principal al objeto de recursos dependientes del dispositivo para que pueda usar esa ventana al crear una cadena de intercambio.
  • Use Load para controlar cualquier configuración restante y para iniciar la creación asincrónica de objetos y la carga de recursos. Si necesita crear archivos o datos temporales, como recursos generados por procedimientos, haga esto también aquí.

Pasos siguientes

En este tema, se han tratado algunas de las estructuras básicas de un juego para UWP que usa DirectX. Es una buena idea tener en cuenta estos métodos, ya que volveremos a hacer referencia a algunos de ellos en temas posteriores.

En el tema siguiente, Administración del flujo de juegos, veremos en profundidad cómo administrar los estados de juego y el control de eventos para mantener el flujo del juego.