Definir el objeto principal 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.
Una vez que hayas diseñado el marco básico del juego de ejemplo e implementado una máquina de estado que controle los comportamientos de usuario y sistema de alto nivel, querrás examinar las reglas y mecánicas que convierten el juego de ejemplo en un juego. Veamos los detalles del objeto principal del juego de ejemplo y cómo traducir reglas de juego en interacciones con el mundo del juego.
Objetivos
- Aprende a aplicar técnicas de desarrollo básicas para implementar reglas de juego y mecánicas para un juego DirectX para UWP.
Objeto principal del juego
En el juego de ejemplo Simple3DGameDX , Simple3DGame es la clase de objetos de juego principal. Una instancia de Simple3DGame se construye indirectamente a través del método App::Load .
Estas son algunas de las características de la clase Simple3DGame .
- Contiene la implementación de la lógica del juego.
- Contiene métodos que comunican estos detalles.
- Cambia el estado del juego a la máquina de estado definida en el marco de la aplicación.
- Cambia el estado del juego de la aplicación al propio objeto del juego.
- Detalles para actualizar la interfaz de usuario del juego (superposición y visualización de encabezados), animaciones y física (la dinámica).
Nota:
La actualización de gráficos se controla mediante la clase GameRenderer , que contiene métodos para obtener y usar recursos de dispositivos gráficos utilizados por el juego. Para obtener más información, consulta Rendering framework I: Introducción a la representación.
- Sirve como contenedor para los datos que definen una sesión de juego, un nivel o una duración, en función de cómo definas tu juego en un nivel alto. En este caso, los datos de estado del juego son durante la vigencia del juego y se inicializan una vez cuando un usuario inicia el juego.
Para ver los métodos y los datos definidos por esta clase, vea la clase Simple3DGame siguiente.
Inicializar e iniciar el juego
Cuando un jugador inicia el juego, el objeto del juego debe inicializar su estado, crear y agregar la superposición, establecer las variables que realizan un seguimiento del rendimiento del jugador y crear instancias de los objetos que usará para crear los niveles. En este ejemplo, esto se hace cuando se crea la instancia de GameMain en App::Load.
El objeto de juego, de tipo Simple3DGame, se crea en el constructor GameMain::GameMain . A continuación, se inicializa con el método Simple3DGame::Initialize durante la corrutina GameMain::ConstructInBackground fire-and-forget, a la que se llama desde GameMain::GameMain.
El método Simple3DGame::Initialize
El juego de ejemplo configura estos componentes en el objeto de juego.
- Se crea un nuevo objeto de reproducción de audio.
- Se crean matrices para primitivos gráficos del juego, incluidas matrices para los primitivos de nivel, munición y obstáculos.
- Se crea una ubicación para guardar los datos de estado del juego, denominado Game y se coloca en la ubicación de almacenamiento de la configuración de datos de la aplicación especificada por ApplicationData::Current.
- Se crea un temporizador de juego y el mapa de bits de superposición inicial en el juego.
- Se crea una nueva cámara con un conjunto específico de parámetros de vista y proyección.
- El dispositivo de entrada (el controlador) se establece en la misma inclinación inicial y la guiñada que la cámara, por lo que el jugador tiene una correspondencia de 1 a 1 entre la posición de control inicial y la posición de la cámara.
- El objeto player se crea y se establece en activo. Usamos un objeto de esfera para detectar la proximidad del jugador a las paredes y obstáculos y para evitar que la cámara se coloque en una posición que pueda romper la inmersión.
- Se crea el primitivo del mundo del juego.
- Se crean los obstáculos del cilindro.
- Los destinos (objetos Face ) se crean y numeran.
- Se crean las esferas de munición.
- Se crean los niveles.
- Se carga la puntuación alta.
- Se carga cualquier estado de juego guardado anterior.
El juego ahora tiene instancias de todos los componentes clave: el mundo, el jugador, los obstáculos, los objetivos y las esferas de munición. También tiene instancias de los niveles, que representan configuraciones de todos los componentes anteriores y sus comportamientos para cada nivel específico. Ahora vamos a ver cómo el juego crea los niveles.
Crear y cargar niveles de juego
La mayor parte del trabajo pesado para la construcción de nivel se realiza en los Level[N].h/.cpp
archivos encontrados en la carpeta GameLevels de la solución de ejemplo. Dado que se centra en una implementación muy específica, no las trataremos aquí. Lo importante es que el código de cada nivel se ejecute como un objeto Level[N] independiente. Si quieres extender el juego, puedes crear un objeto Level[N] que tome un número asignado como parámetro y coloque aleatoriamente los obstáculos y los objetivos. O bien, puede tener datos de configuración de nivel de carga desde un archivo de recursos o incluso desde Internet.
Definir el juego
En este punto, tenemos todos los componentes que necesitamos para desarrollar el juego. Los niveles se han construido en memoria a partir de los primitivos y están listos para que el jugador empiece a interactuar.
Los mejores juegos reaccionan al instante a la entrada del jugador y proporcionan comentarios inmediatos. Esto es cierto para cualquier tipo de juego, desde twitch-action, disparos de primera persona en tiempo real hasta juegos de estrategia basados en turnos pensativos.
El método Simple3DGame::RunGame
Mientras un nivel de juego está en curso, el juego está en estado Dynamics .
GameMain::Update es el bucle de actualización principal que actualiza el estado de la aplicación una vez por fotograma, como se muestra a continuación. El bucle de actualización llama al método Simple3DGame::RunGame para controlar el trabajo si el juego está en estado Dynamics .
// Updates the application state once per frame.
void GameMain::Update()
{
// The controller object has its own update loop.
m_controller->Update();
switch (m_updateState)
{
...
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)
{
...
Simple3DGame::RunGame controla el conjunto de datos que define el estado actual del juego para la iteración actual del bucle del juego.
Esta es la lógica de flujo del juego en Simple3DGame::RunGame.
- El método actualiza el temporizador que cuenta los segundos hasta que se completa el nivel y comprueba si el tiempo del nivel ha expirado. Esta es una de las reglas del juego, cuando se agota el tiempo, si no todos los objetivos se han disparado, entonces es el juego.
- Si se agota el tiempo, el método establece el estado del juego TimeExpired y vuelve al método Update en el código anterior.
- Si el tiempo permanece, se sondea el controlador de aspecto de movimiento para obtener una actualización a la posición de la cámara; en concreto, una actualización al ángulo de la vista normal proyectado desde el plano de la cámara (donde el jugador está mirando) y la distancia que el ángulo se ha movido desde que se sondeó el controlador por última vez.
- La cámara se actualiza en función de los nuevos datos del controlador de movimiento.
- Las dinámicas, o las animaciones y comportamientos de los objetos en el mundo del juego independientemente del control del jugador, se actualizan. En este juego de ejemplo, se llama al método Simple3DGame::UpdateDynamics para actualizar el movimiento de las esferas de munición que se han desencadenado, la animación de los obstáculos del pilar y el movimiento de los objetivos. Para obtener más información, consulta Actualizar el mundo del juego.
- El método comprueba si se han cumplido los criterios para completar correctamente un nivel. Si es así, finaliza la puntuación del nivel y comprueba si este es el último nivel (de 6). Si es el último nivel, el método devuelve el estado del juego GameState::GameComplete ; de lo contrario, devuelve el estado del juego GameState::LevelComplete .
- Si el nivel no está completo, el método establece el estado del juego en GameState::Active y devuelve.
Actualizar el mundo del juego
En este ejemplo, cuando se ejecuta el juego, se llama al método Simple3DGame::UpdateDynamics desde el método Simple3DGame::RunGame (al que se llama desde GameMain::Update) para actualizar los objetos que se representan en una escena del juego.
Un bucle como UpdateDynamics llama a los métodos que se usan para establecer el mundo del juego en movimiento, independientemente de la entrada del jugador, para crear una experiencia de juego inmersiva y hacer que el nivel entre en vivo. Esto incluye gráficos que deben representarse y ejecutar bucles de animación para traer un mundo dinámico incluso cuando no hay ninguna entrada del reproductor. En tu juego, que podría incluir árboles que se deslizan en el viento, olas cresting a lo largo de líneas de costa, maquinaria fumando, y monstruos alienígenas que se extienden y se mueven alrededor. También abarca la interacción entre objetos, incluyendo colisiones entre la esfera del jugador y el mundo, o entre la munición y los obstáculos y objetivos.
Excepto cuando el juego está específicamente en pausa, el bucle del juego debe seguir actualizando el mundo del juego; si se basa en la lógica del juego, los algoritmos físicos o si es simplemente aleatorio.
En el juego de muestra, este principio se denomina dinámica, y abarca el ascenso y caída de los obstáculos del pilar, y el movimiento y los comportamientos físicos de las esferas de munición a medida que se desencadenan y en movimiento.
El método Simple3DGame::UpdateDynamics
Este método se ocupa de estos cuatro conjuntos de cálculos.
- Las posiciones de las esferas de munición disparadas en el mundo.
- Animación de los obstáculos del pilar.
- La intersección del jugador y los límites del mundo.
- Las colisiones de las esferas de munición con los obstáculos, los objetivos, otras esferas de munición y el mundo.
La animación de los obstáculos tiene lugar en un bucle definido en los archivos de código fuente Animate.h/.cpp . El comportamiento de la munición y las colisiones se definen mediante algoritmos físicos simplificados, proporcionados en el código y parametrizados por un conjunto de constantes globales para el mundo del juego, incluidas las propiedades de gravedad y material. Todo esto se calcula en el espacio de coordenadas del mundo del juego.
Revisión del flujo
Ahora que hemos actualizado todos los objetos de la escena y hemos calculado cualquier colisión, necesitamos usar esta información para dibujar los cambios visuales correspondientes.
Después de que GameMain::Update haya completado la iteración actual del bucle del juego, el ejemplo llama inmediatamente a GameRenderer::Render para tomar los datos de objeto actualizados y generar una nueva escena que se va a presentar al jugador, como se muestra a continuación.
void GameMain::Run()
{
while (!m_windowClosed)
{
if (m_visible)
{
switch (m_updateState)
{
case UpdateEngineState::Deactivated:
case UpdateEngineState::TooSmall:
...
// Otherwise, fall through and do normal processing to perform rendering.
default:
CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
CoreProcessEventsOption::ProcessAllIfPresent);
// GameMain::Update calls Simple3DGame::RunGame. If game is in Dynamics
// state, uses Simple3DGame::UpdateDynamics to update game world.
Update();
// Render is called immediately after the Update loop.
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.
}
Representar los gráficos del mundo del juego
Te recomendamos que los gráficos de una actualización del juego se actualicen a menudo, idealmente exactamente tan a menudo como el bucle principal del juego recorre en iteración. A medida que el bucle recorre en iteración, el estado del mundo del juego se actualiza, con o sin entrada del jugador. Esto permite que las animaciones calculadas y los comportamientos se muestren sin problemas. Imagine si teníamos una simple escena de agua que se movió solo cuando el jugador presionaba un botón. Eso no sería realista; un buen juego se ve suave y fluido todo el tiempo.
Recuerde el bucle del juego de ejemplo como se muestra anteriormente en GameMain::Run. Si la ventana principal del juego está visible y no está acoplada ni desactivada, el juego continúa actualizando y representando los resultados de esa actualización. El método GameRenderer::Render que examinamos a continuación representa una representación de ese estado. Esto se realiza inmediatamente después de una llamada a GameMain::Update, que incluye Simple3DGame::RunGame para actualizar los estados, como se describe en la sección anterior.
GameRenderer::Render dibuja la proyección del mundo 3D y, a continuación, dibuja la superposición direct2D sobre ella. Cuando se completa, presenta la cadena de intercambio final con los búferes combinados para su visualización.
Nota:
Hay dos estados para la superposición direct2D del juego de ejemplo, uno donde el juego muestra la superposición de información del juego que contiene el mapa de bits para el menú de pausa, y uno donde el juego muestra los puntos de conexión junto con los rectángulos para el controlador de movimiento de pantalla táctil. El texto de puntuación se dibuja en ambos estados. Para más información, consulte Marco de representación I: Introducción a la representación.
El método GameRenderer::Render
void GameRenderer::Render()
{
bool stereoEnabled{ m_deviceResources->GetStereoState() };
auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };
...
if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
{
// This section is only used after the game state has been initialized and all device
// resources needed for the game have been created and associated with the game objects.
...
for (auto&& object : m_game->RenderObjects())
{
object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
}
}
d3dContext->BeginEventInt(L"D2D BeginDraw", 1);
d2dContext->BeginDraw();
// To handle the swapchain being pre-rotated, set the D2D transformation to include it.
d2dContext->SetTransform(m_deviceResources->GetOrientationTransform2D());
if (m_game != nullptr && m_gameResourcesLoaded)
{
// This is only used after the game state has been initialized.
m_gameHud.Render(m_game);
}
if (m_gameInfoOverlay.Visible())
{
d2dContext->DrawBitmap(
m_gameInfoOverlay.Bitmap(),
m_gameInfoOverlayRect
);
}
...
}
}
La clase Simple3DGame
Estos son los métodos y miembros de datos definidos por la clase Simple3DGame .
Funciones miembro
Las funciones miembro públicas definidas por Simple3DGame incluyen las siguientes.
- Inicializar. Establece los valores iniciales de las variables globales e inicializa los objetos del juego. Esto se trata en la sección Inicializar e iniciar el juego .
- LoadGame. Inicializa un nuevo nivel y comienza a cargarlo.
- LoadLevelAsync. Corrutina que inicializa el nivel y, a continuación, invoca otra corrutina en el representador para cargar los recursos de nivel específicos del dispositivo. Este método se ejecuta en un subproceso independiente; Como resultado, solo se pueden llamar a métodos ID3D11Device (en lugar de los métodos ID3D11DeviceContext) desde este subproceso. Se llama a cualquier método de contexto de dispositivo en el método FinalizeLoadLevel . Si no está familiarizado con la programación asincrónica, consulte Operaciones asincrónicas y simultaneidad con C++/WinRT.
- FinalizeLoadLevel. Completa cualquier trabajo para la carga de nivel que se debe realizar en el subproceso principal. Esto incluye las llamadas a métodos de contexto de dispositivo direct3D 11 (ID3D11DeviceContext).
- StartLevel. Inicia el juego para un nuevo nivel.
- PauseGame. Pausa el juego.
- RunGame. Ejecuta una iteración del bucle del juego. Se llama desde App::Update una vez cada iteración del bucle del juego si el estado del juego es Activo.
- OnSuspending y OnResuming. Suspende/reanude el audio del juego, respectivamente.
Estas son las funciones miembro privadas.
- LoadSavedState y SaveState. Cargue o guarde el estado actual del juego, respectivamente.
- LoadHighScore y SaveHighScore. Cargue o guarde la puntuación alta en los juegos, respectivamente.
- InitializeAmmo. Restablece el estado de cada objeto de esfera utilizado como munición a su estado original para el principio de cada ronda.
- UpdateDynamics. Este es un método importante porque actualiza todos los objetos del juego en función de las rutinas de animación cannadas, la física y la entrada de control. Este es el corazón de la interactividad que define el juego. Esto se trata en la sección Actualizar el mundo del juego.
Los otros métodos públicos son descriptores de acceso de propiedad que devuelven información específica del juego y la superposición al marco de la aplicación para su visualización.
Miembros de datos
Estos objetos se actualizan a medida que se ejecuta el bucle del juego.
- MoveLookController (objeto). Representa la entrada del reproductor. Para obtener más información, consulte Agregar controles.
- Objeto GameRenderer . Representa un representador de Direct3D 11, que controla todos los objetos específicos del dispositivo y su representación. Para obtener más información, vea Rendering framework I.
- Objeto audio . Controla la reproducción de audio para el juego. Para obtener más información, vea Agregar sonido.
El resto de las variables de juego contienen las listas de los primitivos, y sus respectivas cantidades en el juego, y juegos de juegos específicos datos y restricciones específicos.
Pasos siguientes
Todavía tenemos que hablar sobre el motor de representación real: cómo las llamadas a los métodos Render en los primitivos actualizados se convierten en píxeles en la pantalla. Estos aspectos se tratan en dos partes: Marco de representación I: Introducción a la representación y marco de representación II: Representación de juegos. Si estás más interesado en cómo los controles del jugador actualizan el estado del juego, consulta Agregar controles.