定义主游戏对象
注意
本主题是使用 DirectX 创建简单的通用 Windows 平台 (UWP) 游戏教程系列的一部分。 此链接上的主题设置了该系列的上下文。
完成了该示例游戏的基本框架,并实现了处理高级用户和系统行为的状态机后,你需要检查将示例游戏转换为游戏的规则和机制。 我们来了解示例游戏主对象的详细信息,以及如何游戏规则转换为与游戏世界的交互。
目标
- 了解如何应用基本开发技术来实现 UWP DirectX 游戏的游戏规则和机制。
主要游戏对象
在 Simple3DGameDX 示例游戏中,Simple3DGame 是主游戏对象类。 Simple3DGame 的实例通过 App::Load 方法间接进行构造。
下面是 Simple3DGame 类的一些功能。
- 包含游戏玩法逻辑的实现。
- 包含用于传达以下详细信息的方法。
- 将游戏状态更改传达给应用框架中定义的状态机。
- 将游戏状态更改从应用传达给游戏对象本身。
- 有关更新游戏 UI(覆盖层和提醒显示)、动画和物理特性(动态)的详细信息。
注意
图形的更新由 GameRenderer 类进行处理,该类包含用于获取和使用游戏所使用的图形设备资源的方法。 有关详细信息,请参阅呈现框架 I:呈现简介。
- 用作定义游戏会话、级别或生命期的数据容器,具体取决于在较高级别定义游戏的方式。 在此情况下,游戏状态数据用于游戏的生命期,在用户启动游戏时初始化一次。
若要查看此类定义的方法和数据,请参阅下面的 Simple3DGame 类。
初始化并启动游戏
当玩家启动游戏时,游戏对象必须初始化其状态,创建和添加覆盖层,设置用于跟踪玩家成绩的变量,并实例化将用于构建级别的对象。 在此示例中,当在 App::Load 中创建 GameMain 实例时,会完成此操作。
在 GameMain::GameMain 构造函数中创建类型为 Simple3DGame 的游戏对象。 然后,在 GameMain::ConstructInBackground 发后不理协同例程(从 GameMain::GameMain 进行调用)期间,使用 Simple3DGame::Initialize 方法进行初始化。
Simple3DGame::Initialize 方法
示例游戏在游戏对象中设置以下组件。
- 创建新的音频播放对象。
- 创建游戏的图形基元的数组,包括级别基元、弹药和障碍的数组。
- 创建用于保存游戏状态数据的位置,名为游戏,并放入由 ApplicationData::Current 指定的应用数据设置存储位置。
- 创建游戏计时器和初始游戏内重叠位图。
- 使用一组特定的视图和投影参数创建新的相机。
- 输入设备(控制器)设置为与相机相同的俯仰和偏航,以使玩家在起始控制位置和相机位置之间具有 1 对 1 的对应性。
- 创建玩家对象并设置为活动。 我们使用球体对象以检测玩家对墙和障碍的邻近感应,防止相机被放入可能打破沉浸的位置。
- 创建游戏世界基元。
- 创建圆柱障碍。
- 创建目标(Face 对象)并编号。
- 创建弹药球体。
- 创建级别。
- 加载高分。
- 加载任何之前保存的游戏状态。
游戏现在已有全部关键组件的实例:游戏世界、玩家、障碍物、目标和弹药范围。 还有各级别的实例,表示上述所有组件的配置以及每个特定级别的行为。 现在让我们了解游戏如何构建级别。
构建和加载游戏级别
大多数重要的级别构建都在 Level[N].h/.cpp
Level.h/.cpp 文件(在示例解决方案的 GameLevels 文件夹中)中完成。 这不在我们此处的讨论范围内,因为它侧重于非常具体的实现。 重要的是每个级别的代码都作为单独的 Level[N] 对象运行。 如果你希望扩展游戏,则可以创建一个 Level[N] 对象,采用分配的数字作为参数并随机放置障碍物和目标。 或者,你可以让其从源文件甚至 Internet 加载级别配置数据。
定义游戏玩法
这时,我们已经有了开发游戏所需的全部组件。 已经在内存中从基元构建了级别,玩家可随时开始与之交互。
最好的游戏应能够立即响应玩家输入和提供即时反馈。 对于任何类型的游戏都是这样,从抽搐动作、实时第一人称射击游戏到基于思考、轮次的策略游戏等。
Simple3DGame::RunGame 方法
当游戏级别正在进行时,游戏处于 Dynamics 状态。
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::RunGame 方法(从 GameMain::Update 进行调用)调用 Simple3DGame::UpdateDynamics 方法,以更新游戏场景中呈现的对象。
诸如 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 定义的公共成员函数包括以下这些。
- Initialize。 设置全局变量的起始值并初始化游戏对象。 初始化并启动游戏部分对此进行了介绍。
- LoadGame。 初始化新级别并开始加载。
- LoadLevelAsync。 一个协同例程,它会初始化级别,然后对呈现器调用另一个协同例程以加载特定于设备的级别资源。 该方法在单独的线程中运行;因此,只可从该线程调用 ID3D11Device 方法(相对于 ID3D11DeviceContext 方法)。 任何设备上下文方法都在 FinalizeLoadLevel 方法中调用。 如果你不熟悉异步编程,请参阅使用 C++/WinRT 执行并发和异步操作。
- FinalizeLoadLevel。 完成需要在主线程上进行的关卡加载所需的任何工作。 这包括对 Direct3D 11 设备上下文 (ID3D11DeviceContext) 方法的任何调用。
- StartLevel。 开始执行游戏的新级别。
- PauseGame。 暂停游戏。
- RunGame。 运行游戏循环的迭代。 如果游戏状态为 Active,则游戏循环的每次迭代都将从 App::Update 中调用它一次。
- OnSuspending 和 OnResuming。 分别暂停/恢复游戏的音频。
下面是私有成员函数。
- LoadSavedState 和 SaveState。 分别加载/保存游戏的当前状态。
- LoadHighScore 和 SaveHighScore。 分别保存/加载游戏中的高分。
- InitializeAmmo。 在每一轮开始时,将用作弹药的各范围对象的状态重置回其初始状态。
- UpdateDynamics。 这是一个重要方法,因为它将根据封装的动画例程、物理特性和控件输入更新所有游戏对象。 这是定义游戏的交互性的核心。 更新游戏世界部分对此进行了介绍。
其他公共方法是属性访问器,用于将游戏执行和特定于覆盖层的信息返回到应用框架进行显示。
数据成员
这些对象在游戏循环运行时更新。
- MoveLookController 对象。 表示玩家输入。 有关详细信息,请参阅添加控件。
- GameRenderer 对象。 表示 Direct3D 11 呈现器,它处理所有特定于设备的对象及其呈现。 有关详细信息,请参阅呈现框架 I。
- Audio 对象。 控制游戏的音频播放。 有关更多信息,请参阅添加声音。
其余游戏变量包含基元列表及其相应的游戏内数量,以及特定于游戏执行的数据和约束。
后续步骤
我们尚未讨论实际的呈现引擎:在更新基元上对 Render 方法的调用如何转换为屏幕上的像素。 这些方面包含在两个部分中:呈现框架 I:呈现简介和呈现框架 II:游戏呈现。 如果你对玩家控件如何更新游戏状态更感兴趣,请参阅添加控件。