注释
本主题是 使用 DirectX 教程系列创建简单的通用 Windows 平台(UWP)游戏的一部分。 该链接中的主题设置序列的上下文。
布局示例游戏的基本框架并实现处理高级用户和系统行为的状态机后,你需要检查将示例游戏转换为游戏的规则和机制。 让我们看看示例游戏的主要对象的详细信息,以及如何将游戏规则转换为与游戏世界的交互。
目标
- 了解如何应用基本开发技术来实现 UWP DirectX 游戏的游戏规则和机制。
主游戏对象
在 Simple3DGameDX 示例游戏中, Simple3DGame 是主要的游戏对象类。 Simple3DGame 的实例通过 App::Load 方法间接构造。
下面是 Simple3DGame 类 的一些功能。
- 包含游戏逻辑的实现。
- 包含传达这些详细信息的方法。
- 游戏状态更改到应用框架中定义的状态机。
- 游戏状态从应用更改为游戏对象本身。
- 更新游戏用户界面(包括覆盖和抬头显示)、动画和物理(动态)的详细信息。
注释
更新图形由 GameRenderer 类处理,该类包含获取和使用游戏使用的图形设备资源的方法。 有关详细信息,请参阅 渲染框架 I:渲染简介。
- 用作定义游戏会话、关卡或生命周期的数据容器,具体取决于你在高层次上如何定义游戏。 在这种情况下,游戏状态数据为游戏的生存期,并在用户启动游戏时初始化一次。
若要查看此类定义的方法和数据,请参阅下面的 Simple3DGame 类 。
初始化并启动游戏
当玩家启动游戏时,游戏对象必须初始化其状态,创建并添加覆盖,设置跟踪玩家性能的变量,并实例化它将用于生成级别的对象。 在此示例中,在 App::Load 中创建 GameMain 实例时完成此作。
GameMain::GameMain 构造函数
Simple3DGame::Initialize 方法
示例游戏在游戏对象中设置这些组件。
- 将创建新的音频播放对象。
- 为游戏的图形基元创建数组,包括级别基元、弹药和障碍的数组。
- 保存游戏状态数据的位置将创建、命名 为 Game,并放置在 ApplicationData::Current 指定的应用数据设置存储位置中。
- 将创建游戏计时器和初始游戏内覆盖位图。
- 使用一组特定的视图和投影参数创建新的相机。
- 输入设备(控制器)设置为与相机相同的起始音调和偏航,因此玩家在起始控制位置和相机位置之间具有 1 到 1 的对应关系。
- 创建播放器对象并将其设置为活动对象。 我们使用球体对象来检测玩家接近墙壁和障碍物,并防止相机置于可能破坏沉浸的位置。
- 创建游戏世界基元。
- 圆柱体障碍物已生成。
- 目标(人脸 对象)已经被创建并编号。
- 创建弹药球体。
- 已创建级别。
- 高分已加载完成。
- 之前保存的游戏状态被加载。
游戏现在有所有关键组成部分的实例-世界,玩家,障碍,目标和弹药球体。 它还包含级别的实例,这些实例表示上述所有组件的配置以及每个特定级别的行为。 现在让我们看看游戏如何构建关卡。
构建和加载游戏关卡
在示例解决方案的 Level[N].h/.cpp
文件夹中找到的 文件中,完成关卡构建的大部分核心工作。 因为它侧重于非常具体的实现,因此我们不会在此处介绍它们。 重要的是,每个级别的代码作为单独的 Level[N] 对象运行。 如果要扩展游戏,可以创建一个 Level[N] 对象,该对象采用分配的数字作为参数,并随机放置障碍和目标。 或者,可以从资源文件甚至 Internet 中加载级别配置数据。
定义游戏
此时,我们拥有开发游戏所需的所有组件。 这些级别已在内存中从基元构建,并已准备好供玩家开始交互。
最佳游戏会立即对玩家输入做出反应,并提供即时反馈。 对于任何类型的游戏,从即时反应游戏、实时第一人称射击游戏到考验思考的基于回合的策略游戏,都是如此。
Simple3DGame::RunGame 方法
当关卡正在进行时,游戏处于 动态 状态。
GameMain::Update 是每个帧一次更新应用程序状态的主更新循环,如下所示。 如果游戏处于 Dynamics 状态,更新循环将调用 Simple3DGame::RunGame 方法来处理工作。
// 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 处理定义游戏循环当前迭代游戏状态的数据集。
下面是 Simple3DGame::RunGame
- 该方法更新用于倒计时关卡完成时间的计时器,并检查关卡的时间是否已用完。 这是游戏规则之一 - 当时间用完时,如果不是所有目标都被射杀,那么游戏就结束了。
- 如果时间已用完,该方法将设置 TimeExpired 游戏状态,并返回到上一代码中的 Update 方法。
- 如果还有时间,可查询移动观察控制器以更新相机位置;具体而言,更新从相机平面投射出的法线视角角度(即玩家正在观看的方向),以及自上次查询控制器以来该角度移动的距离。
- 相机根据新的移动观测控制器数据进行更新。
- 游戏世界中独立于玩家控件的对象动态或动画和行为已更新。 在此示例游戏中,调用 Simple3DGame::UpdateDynamics 方法更新已触发的弹药球体的运动、支柱障碍的动画和目标移动。 有关详细信息,请参阅 更新游戏世界。
- 该方法检查是否满足成功完成级别的条件。 如果是这样,它将完成该关卡的得分,并检查这是否是最后一个关卡(共 6 个)。 如果它是最后一个级别,则该方法返回 GameState::GameComplete 游戏状态;否则,它将返回 GameState::LevelComplete 游戏状态。
- 如果级别未完成,该方法会将游戏状态设置为 GameState::Active,并返回。
更新游戏世界
在此示例中,当游戏运行时,Simple3DGame::UpdateDynamics 方法从 Simple3DGame::RunGame 方法(从 GameMain::Update) 调用该方法以更新游戏场景中呈现的对象。
UpdateDynamics 等循环调用用于使游戏世界在不依赖玩家输入的情况下运转的所有相关方法,以创造沉浸式游戏体验,使关卡栩栩如生。 这包括需要呈现的图形,以及运行动画循环以带来动态世界,即使没有玩家输入也是如此。 在你的游戏中,这可能包括树木在风中摇摆,海浪沿着海岸线翻腾,机械冒烟,外星怪物伸展和四处移动。 它还包括对象之间的交互,包括玩家球体与世界之间的碰撞,或弹药与障碍和目标之间的碰撞。
除非游戏专门暂停,游戏循环应继续更新游戏世界,无论是基于游戏逻辑、物理算法,还是纯随机。
在示例游戏中,这一原则被称为 动态,涵盖了支柱障碍物的升降运动,以及弹药球体在发射和运动过程中的动力学和物理行为。
Simple3DGame::UpdateDynamics 方法
此方法处理这四组计算。
- 已发射的弹药球体在全球中的位置。
- 柱子障碍的动画。
- 玩家和世界边界的交集。
- 弹药球体与障碍物、目标、其他弹药球体以及世界的碰撞。
障碍的动画发生在 Animate.h/.cpp 源代码文件中定义的循环中。 弹药和任何碰撞的行为由简化的物理算法定义,在代码中提供,并由游戏世界的一组全局常量参数化,包括重力和材料属性。 这一切都在游戏世界坐标空间中计算。
审查流程
现在,我们已经更新了场景中的所有对象,并计算了任何冲突,因此我们需要使用此信息来绘制相应的视觉更改。
GameMain::Update 完成游戏循环的当前迭代后,该示例会立即调用 GameRenderer::Render 来获取更新的对象数据,并生成要呈现给玩家的新场景,如下所示。
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.
}
呈现游戏世界的画面
建议经常更新游戏中的图形,理想情况下与主游戏循环迭代的频率完全相同。 循环迭代时,游戏世界的状态会更新,无论是否有玩家输入。 这样就可以顺利显示计算的动画和行为。 试想一下,如果我们有一个简单的水流场景,只在玩家按按钮时才会移动。 那是不现实的,一个好的游戏看起来总是顺滑流畅。
回忆一下上面示例游戏的循环,如 GameMain::Run所示。 如果游戏的主窗口可见且未贴靠或停用,则游戏将继续更新并显示该更新的结果。 GameRenderer::Render 方法是我们接下来要研究的,它呈现了该状态的表示形式。 在调用 GameMain::Update后立即完成该操作,其中包括 Simple3DGame::RunGame 以更新状态,如前一部分所述。
GameRenderer::Render 绘制 3D 世界的投影,然后在其上方绘制 Direct2D 叠加层。 完成后,它将显示最终的交换链,其中包含已合并用于显示的缓冲区。
注释
示例游戏的 Direct2D 叠加层有两种状态:一种是显示包含暂停菜单位图的游戏信息叠加层,另一种是显示十字准心和触屏移动-观察控制器的矩形。 在两种状态下都会显示分数文本。 有关详细信息,请参阅 渲染框架 I:渲染介绍。
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
);
}
...
}
}
Simple3DGame 类
这些是 Simple3DGame 类定义的方法和数据成员。
成员函数
Simple3DGame 定义的公共成员函数包括以下函数。
- 初始化。 设置全局变量的起始值,并初始化游戏对象。 在初始化并启动游戏 一节中有介绍。
- 加载游戏。 初始化一个新关卡,并开始加载。
- LoadLevelAsync。 初始化关卡的协程,然后在渲染器上调用另一个协程来加载设备特定的关卡资源。 此方法在单独的线程中运行;因此,只能从此线程调用 ID3D11Device 方法(而不是 ID3D11DeviceContext 方法)。 在 FinalizeLoadLevel 方法中调用所有设备上下文的方法。 如果您对异步编程不熟悉,请参阅 使用 C++/WinRT 进行并发和异步操作。
- 完成加载级别。 完成任何需要在主线程上完成的级别加载工作。 这包括对 Direct3D 11 设备上下文(ID3D11DeviceContext)方法的任何调用。
- StartLevel。 为新级别启动游戏。
- PauseGame。 暂停游戏。
- RunGame。 执行游戏循环的一次迭代。 每次游戏循环迭代时,如果游戏状态为激活,则会从 App::Update 调用一次。
- OnSuspending 和 OnResuming。 分别暂停和恢复游戏音频。
下面是专用成员函数。
- LoadSavedState 和 SaveState。 分别加载/保存游戏的当前状态。
- LoadHighScore 和 SaveHighScore。 分别在各游戏之间加载/保存高分。
- InitializeAmmo。 将用作弹药的每个球体对象的状态重置回每轮开始的原始状态。
- UpdateDynamics。 这是一个重要的方法,因为它可以根据预设动画例程、物理和控制输入更新所有游戏对象。 这是定义游戏的交互性的核心。 在 更新游戏世界 部分中对此进行了说明。
其他公共方法是属性访问器,用于获取与游戏和覆盖相关的特定信息,并将这些信息返回到应用框架中以供显示。
数据成员
随着游戏循环的运行,这些对象将更新。
- MoveLookController 对象。 表示玩家输入。 有关详细信息,请参阅 添加控件。
- GameRenderer 对象。 表示 Direct3D 11 渲染器,该渲染器处理所有设备特有的对象及其渲染。 有关详细信息,请参阅 呈现框架 I。
- 音频 对象。 控制游戏的音频播放。 有关详细信息,请参阅 添加声音。
游戏变量的其余部分包含基元的列表及其各自的游戏内数量,以及特定于游戏的数据和约束。
后续步骤
我们尚未讨论实际的渲染引擎——即如何调用更新后的基元上的 渲染 方法,将其转换为屏幕上的像素。 这些方面分为两个部分:呈现框架 I:关于渲染 简介和 呈现框架 II:游戏渲染。 如果对玩家控件如何更新游戏状态更感兴趣,请参阅 添加控件。