Compartir a través de


Administración de flujo de juegos

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 juego ahora tiene una ventana, ha registrado algunos controladores de eventos y ha cargado recursos de forma asincrónica. En este tema se explica el uso de estados de juego, cómo administrar estados clave específicos del juego y cómo crear un bucle de actualización para el motor de juegos. A continuación, veremos el flujo de la interfaz de usuario y, por último, comprenderemos más sobre los controladores de eventos necesarios para un juego para UWP.

Estados de juego usados para administrar el flujo del juego

Hacemos uso de estados de juego para administrar el flujo del juego.

Cuando el juego de ejemplo Simple3DGameDX se ejecuta por primera vez en una máquina, se encuentra en un estado donde no se ha iniciado ningún juego. Veces posteriores se ejecuta el juego, puede estar en cualquiera de estos estados.

  • No se ha iniciado ningún juego o el juego está entre niveles (la puntuación alta es cero).
  • El bucle del juego se está ejecutando y está en medio de un nivel.
  • El bucle del juego no se está ejecutando debido a que se ha completado un juego (la puntuación alta tiene un valor distinto de cero).

Tu juego puede tener tantos estados como necesites. Pero recuerde que se puede terminar en cualquier momento. Y cuando se reanuda, el usuario espera que se reanude en el estado en el que se encontraba cuando se terminó.

Administración del estado del juego

Por lo tanto, durante la inicialización del juego, tendrás que admitir el arranque en frío del juego, así como reanudar el juego después de detenerlo en vuelo. El ejemplo Simple3DGameDX siempre guarda su estado del juego para dar la impresión de que nunca se detuvo.

En respuesta a un evento de suspensión, el juego se suspende, pero los recursos del juego siguen en memoria. Del mismo modo, el evento resume se controla para asegurarse de que el juego de ejemplo recoge en el estado en que estaba en cuando se suspendió o finalizó. Dependiendo del estado, se presentan diferentes opciones al jugador.

  • Si el juego reanuda el nivel medio, aparece en pausa y la superposición ofrece la opción de continuar.
  • Si el juego se reanuda en un estado en el que se completa el juego, muestra las puntuaciones altas y una opción para jugar un nuevo juego.
  • Por último, si el juego se reanuda antes de que se haya iniciado un nivel, la superposición presenta una opción de inicio al usuario.

El juego de ejemplo no distingue si el juego está en frío, se inicia por primera vez sin un evento de suspensión o se reanuda desde un estado suspendido. Este es el diseño adecuado para cualquier aplicación para UWP.

En este ejemplo, la inicialización de los estados del juego se produce en GameMain::InitializeGameState (se muestra un esquema de ese método en la sección siguiente).

Este es un diagrama de flujo que le ayudará a visualizar el flujo. Abarca tanto la inicialización como el bucle de actualización.

  • La inicialización comienza en el nodo Inicio al comprobar el estado actual del juego. Para obtener código de juego, consulta GameMain::InitializeGameState en la sección siguiente.

la máquina de estado principal para nuestro juego

El método GameMain::InitializeGameState

El método GameMain::InitializeGameState se llama indirectamente a través del constructor de la clase GameMain, que es el resultado de crear una instancia de GameMain dentro de App::Load.

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) : ...
{
    m_deviceResources->RegisterDeviceNotify(this);
    ...
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    ...
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();
    ...
}

void GameMain::InitializeGameState()
{
    // Set up the initial state machine for handling Game playing state.
    if (m_game->GameActive() && m_game->LevelActive())
    {
        // The last time the game terminated it was in the middle
        // of a level.
        // We are waiting for the user to continue the game.
        ...
    }
    else if (!m_game->GameActive() && (m_game->HighScore().totalHits > 0))
    {
        // The last time the game terminated the game had been completed.
        // Show the high score.
        // We are waiting for the user to acknowledge the high score and start a new game.
        // The level resources for the first level will be loaded later.
        ...
    }
    else
    {
        // This is either the first time the game has run or
        // the last time the game terminated the level was completed.
        // We are waiting for the user to begin the next level.
        ...
    }
    m_uiControl->ShowGameInfoOverlay();
}

Actualización del motor de juegos

El método App::Run llama a GameMain::Run. Dentro de GameMain::Run es una máquina de estado básica para controlar todas las acciones principales que un usuario puede realizar. El nivel más alto de esta máquina de estado se ocupa de cargar un juego, jugar un nivel específico o continuar un nivel después de que el juego se haya pausado (por el sistema o por el usuario).

En el juego de ejemplo, hay 3 estados principales (representados por la enumeración UpdateEngineState ) en el que el juego puede estar.

  1. UpdateEngineState::WaitingForResources. El bucle del juego es ciclismo, no se puede realizar la transición hasta que haya recursos (específicamente recursos gráficos) disponibles. Cuando se completen las tareas asincrónicas de carga de recursos, actualizamos el estado a UpdateEngineState::ResourcesLoaded. Esto suele ocurrir entre niveles cuando el nivel carga nuevos recursos desde el disco, desde un servidor de juegos o desde un back-end en la nube. En el juego de ejemplo, simulamos este comportamiento, ya que la muestra no necesita ningún recurso adicional por nivel en ese momento.
  2. UpdateEngineState::WaitingForPress. El bucle del juego es ciclismo, esperando la entrada específica del usuario. Esta entrada es una acción del jugador para cargar un juego, iniciar un nivel o continuar con un nivel. El código de ejemplo hace referencia a estos subestados a través de la enumeración PressResultState .
  3. UpdateEngineState::D ynamics. El bucle del juego se ejecuta con el usuario jugando. Mientras el usuario está jugando, el juego comprueba si hay tres condiciones en las que puede realizar la transición:
  • GameState::TimeExpired. Expiración del límite de tiempo de un nivel.
  • GameState::LevelComplete. Finalización de un nivel por el jugador.
  • GameState::GameComplete. Finalización de todos los niveles por el jugador.

Un juego es simplemente una máquina de estado que contiene varias máquinas de estado más pequeñas. Cada estado específico debe definirse mediante criterios muy específicos. Las transiciones de un estado a otro deben basarse en la entrada discreta del usuario o en acciones del sistema (como la carga de recursos gráficos).

Mientras planeas tu juego, considera la posibilidad de dibujar todo el flujo del juego para asegurarte de que has solucionado todas las posibles acciones que el usuario o el sistema pueden realizar. Un juego puede ser muy complicado, por lo que una máquina de estado es una herramienta eficaz para ayudarle a visualizar esta complejidad y hacer que sea más fácil de administrar.

Echemos un vistazo al código del bucle de actualización.

El método GameMain::Update

Esta es la estructura de la máquina de estado utilizada para actualizar el motor de juegos.

void GameMain::Update()
{
    // The controller object has its own update loop.
    m_controller->Update(); 

    switch (m_updateState)
    {
    case UpdateEngineState::WaitingForResources:
        ...
        break;

    case UpdateEngineState::ResourcesLoaded:
        ...
        break;

    case UpdateEngineState::WaitingForPress:
        if (m_controller->IsPressComplete())
        {
            ...
        }
        break;

    case UpdateEngineState::Dynamics:
        if (m_controller->IsPauseRequested())
        {
            ...
        }
        else
        {
            // When the player is playing, work is done by Simple3DGame::RunGame.
            GameState runState = m_game->RunGame();
            switch (runState)
            {
            case GameState::TimeExpired:
                ...
                break;

            case GameState::LevelComplete:
                ...
                break;

            case GameState::GameComplete:
                ...
                break;
            }
        }

        if (m_updateState == UpdateEngineState::WaitingForPress)
        {
            // Transitioning state, so enable waiting for the press event.
            m_controller->WaitForPress(
                m_renderer->GameInfoOverlayUpperLeft(),
                m_renderer->GameInfoOverlayLowerRight());
        }
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            // Transitioning state, so shut down the input controller
            // until resources are loaded.
            m_controller->Active(false);
        }
        break;
    }
}

Actualización de la interfaz de usuario

Necesitamos mantener al jugador atento al estado del sistema y permitir que el estado del juego cambie según las acciones del jugador y las reglas que definen el juego. Muchos juegos, incluido este juego de ejemplo, suelen usar elementos de la interfaz de usuario (UI) para presentar esta información al jugador. La interfaz de usuario contiene representaciones del estado del juego y otra información específica del juego, como puntuación, munición o el número de posibilidades restantes. La interfaz de usuario también se denomina superposición porque se representa por separado de la canalización de gráficos principal y se coloca encima de la proyección 3D.

Parte de la información de la interfaz de usuario también se presenta como una pantalla de cabeza (HUD) para permitir al usuario ver esa información sin quitar los ojos por completo del área de juego principal. En el juego de ejemplo, creamos esta superposición mediante las API de Direct2D. Como alternativa, podríamos crear esta superposición mediante XAML, que tratamos en Extensión del juego de ejemplo.

Hay dos componentes para la interfaz de usuario.

  • El HUD que contiene la puntuación e información sobre el estado actual del juego.
  • Mapa de bits de pausa, que es un rectángulo negro con texto superpuesto durante el estado pausado o suspendido del juego. Esta es la superposición del juego. Se explica más en Adición de una interfaz de usuario.

Sin duda, la superposición también tiene una máquina de estado. La superposición puede mostrar un inicio de nivel o un mensaje de partida. Es esencialmente un lienzo en el que podemos generar cualquier información sobre el estado del juego que queremos mostrar al jugador mientras el juego está en pausa o suspendido.

La superposición representada puede ser una de estas seis pantallas, dependiendo del estado del juego.

  1. Pantalla de progreso de carga de recursos al principio del juego.
  2. Pantalla de estadísticas de juego.
  3. Pantalla de mensaje de inicio de nivel.
  4. Pantalla a través del juego cuando se completan todos los niveles sin que se agote el tiempo.
  5. Juego a través de la pantalla cuando se agota el tiempo.
  6. Pantalla de menú Pausar.

Separar la interfaz de usuario de la canalización de gráficos de tu juego te permite trabajar en ella independientemente del motor de representación de gráficos del juego y reduce significativamente la complejidad del código del juego.

Así es como el juego de ejemplo estructura la máquina de estado de la superposición.

void GameMain::SetGameInfoOverlay(GameInfoOverlayState state)
{
    m_gameInfoOverlayState = state;
    switch (state)
    {
    case GameInfoOverlayState::Loading:
        m_uiControl->SetGameLoading(m_loadingCount);
        break;

    case GameInfoOverlayState::GameStats:
        ...
        break;

    case GameInfoOverlayState::LevelStart:
        ...
        break;

    case GameInfoOverlayState::GameOverCompleted:
        ...
        break;

    case GameInfoOverlayState::GameOverExpired:
        ...
        break;

    case GameInfoOverlayState::Pause:
        ...
        break;
    }
}

Control de eventos

Como vimos en el tema Definir el marco de la aplicación para UWP del juego, muchos de los métodos de proveedor de vistas de los controladores de eventos de registro de clases de aplicación. Estos métodos deben controlar correctamente estos eventos importantes antes de agregar mecánicas de juego o iniciar el desarrollo de gráficos.

El control adecuado de los eventos en cuestión es fundamental para la experiencia de la aplicación para UWP. Dado que una aplicación para UWP puede activarse, desactivarse, cambiar el tamaño, ajustar, desasignar, suspender o reanudar, el juego debe registrarse para estos eventos tan pronto como pueda y controlarlos de una manera que mantenga la experiencia fluida y predecible para el jugador.

Estos son los controladores de eventos que se usan en este ejemplo y los eventos que controlan.

Controlador de eventos Descripción
OnActivated Controla CoreApplicationView::Activated. La aplicación del juego se ha traído al primer plano, por lo que se activa la ventana principal.
OnDpiChanged Controla Graphics::D isplay::D isplayInformation::D piChanged. El PPP de la pantalla ha cambiado y el juego ajusta sus recursos en consecuencia.
NotaLas coordenadas CoreWindow están en píxeles independientes del dispositivo (DIP) para Direct2D. Como resultado, debe notificar a Direct2D el cambio en PPP para mostrar los activos 2D o primitivos correctamente.
OnOrientationChanged Controla Graphics::D isplay::D isplayInformation::OrientationChanged. Es necesario actualizar la orientación de los cambios de visualización y la representación.
OnDisplayContentsInvalidated Controla Graphics::D isplay::D isplayInformation::D isplayContentsInvalidated. La pantalla requiere volver a dibujar y el juego debe volver a representarse.
OnResuming Controla CoreApplication::Resuming. La aplicación de juego restaura el juego a partir de un estado suspendido.
OnSuspending Controla CoreApplication::Suspending. La aplicación de juego guarda su estado en el disco. Tiene 5 segundos para guardar el estado en el almacenamiento.
OnVisibilityChanged Controla CoreWindow::VisibilityChanged. La aplicación de juego ha cambiado la visibilidad y se ha vuelto visible o ha sido invisible por otra aplicación que se vuelve visible.
OnWindowActivationChanged Controla CoreWindow::Activated. La ventana principal de la aplicación del juego se ha desactivado o activado, por lo que debe quitar el foco y pausar el juego, o recuperar el foco. En ambos casos, la superposición indica que el juego está en pausa.
OnWindowClosed Controla CoreWindow::Closed. La aplicación del juego cierra la ventana principal y suspende el juego.
OnWindowSizeChanged Controla CoreWindow::SizeChanged. La aplicación de juego reasigna los recursos gráficos y la superposición para dar cabida al cambio de tamaño y, a continuación, actualiza el destino de representación.

Pasos siguientes

En este tema, hemos visto cómo se administra el flujo general del juego mediante estados de juego y que un juego está formado por varias máquinas de estado diferentes. También hemos visto cómo actualizar la interfaz de usuario y administrar controladores de eventos de la aplicación clave. Ahora estamos listos para profundizar en el bucle de representación, el juego y sus mecánicas.

Puedes pasar por los temas restantes que documentan este juego en cualquier orden.