定义游戏的 UWP 应用框架

注释

本主题是 使用 DirectX 教程系列创建简单的通用 Windows 平台(UWP)游戏的一部分。 该链接中的主题设置序列的上下文。

编码通用 Windows 平台 (UWP) 游戏的第一步是搭建框架,使应用对象能与 Windows 交互,尤其是 Windows 运行时功能,如处理暂停和恢复事件、窗口可见性变化以及窗口贴靠功能。

目标

  • 为通用 Windows 平台 (UWP) DirectX 游戏设置框架,并实现定义整个游戏流的状态机。

注释

若要遵循本主题,请查看下载的 Simple3DGameDX 示例游戏的源代码。

介绍

设置游戏项目 主题中,我们介绍了 wWinMain 函数以及 IFrameworkViewSourceIFrameworkView 接口。 我们了解到,App 类(可在 App.cpp 项目中的 源代码文件中看到)既作为 视图提供程序工厂,也作为 视图提供程序

本主题接续上文,并更深入地详细介绍了游戏中 App 类应如何实现 IFrameworkView的方法。

App:Initialize方法

应用程序启动后,Windows 调用的第一个方法是我们实现的 IFrameworkView::Initialize

你的实现应处理 UWP 游戏的核心功能,例如,通过订阅这些事件来确保游戏能够应对暂停(以及可能的后续恢复)事件。 我们还有权访问此处的显示适配器设备,因此我们可以创建依赖于设备的图形资源。

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>();
}

尽可能避免使用原始指针(几乎总是可以做到)。

  • 对于 Windows 运行时类型,通常可以完全避免指针,只需在堆栈上构造值。 如果需要指针,请使用 winrt::com_ptr (我们将很快看到该示例)。
  • 对于唯一指针,请使用 std::unique_ptrstd::make_unique
  • 对于共享指针,请使用 std::shared_ptrstd::make_shared

App::SetWindow 方法

初始化后,Windows 调用我们对 IFrameworkView::SetWindow的实现,传递一个表示游戏主窗口的 CoreWindow 对象。

App::SetWindow中,我们订阅与窗口相关的事件,并配置一些窗口和显示行为。 例如,我们构造鼠标指针(通过 CoreCursor 类),该指针可由鼠标和触摸控件使用。 我们还将窗口对象传递给依赖于设备的资源对象。

我们将讨论如何在 游戏流管理 主题中处理事件。

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 });
}

App::Load 方法

一旦主窗口设置完成,就调用 IFrameworkView::Load 的实现。 加载初始化SetWindow更适合预提取游戏数据或素材。

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

如你所看到的,实际工作被委托给我们在此处创建的 GameMain 对象的构造器。 GameMain 类在 GameMain.hGameMain.cpp中定义。

GameMain::GameMain 构造函数

GameMain 构造函数(及其调用的其他成员函数)开始一组异步加载操作,以创建游戏对象、加载图形资源以及初始化游戏的状态机。 我们还在游戏开始前执行任何必要的准备,例如设置任何起始状态或全局值。

Windows 对游戏开始处理输入之前可能需要的时间施加限制。 因此,使用异步,就像我们在这里做的一样,意味着Load可以快速返回,而它启动的工作会继续在后台进行。 如果加载需要很长时间,或者存在大量资源,则为用户提供频繁更新的进度栏是个好主意。

如果您对异步编程不熟悉,请参阅 使用 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.
    ...
}

下面是构造函数启动的工作序列的概述。

  • 创建并初始化 GameRenderer类型的对象。 有关详细信息,请参阅 渲染框架 I:渲染介绍
  • 创建并初始化 Simple3DGame类型的对象。 有关详细信息,请参阅 定义主游戏对象
  • 创建游戏 UI 控件对象,并显示游戏信息叠加层,以便在资源文件加载时显示进度栏。 有关详细信息,请参阅 “添加用户界面”。
  • 创建控制器对象以从控制器(触摸、鼠标或游戏控制器)读取输入。 有关详细信息,请参阅 添加控件
  • 分别在屏幕左下角和右下角定义两个矩形区域,分别用于移动控件和相机触摸控件。 玩家使用在调用 SetMoveRect时定义的左下角矩形作为虚拟控制器,用于向前、向后及左右移动相机。 右下角矩形(由 SetFireRect 方法定义)用作触发弹药的虚拟按钮。
  • 使用协同例程将资源加载分解为单独的阶段。 对 Direct3D 设备上下文的访问仅限于创建设备上下文的线程;而访问用于创建对象的 Direct3D 设备是自由线程的。 因此,GameRenderer::CreateGameDeviceResourcesAsync 协程可以在与原始线程分开的单独线程上运行,而完成任务的协程(GameRenderer::FinalizeCreateGameDeviceResources)则在原始线程上运行。
  • 我们使用类似的模式通过 Simple3DGame::LoadLevelAsyncSimple3DGame::FinalizeLoadLevel加载关卡资源。

我们将在下一个主题(游戏流管理)中更详细地讨论 GameMain::InitializeGameState

App::OnActivated 方法

接下来,将引发 CoreApplicationView::Activated 事件。 因此,将调用你拥有的任何 OnActivated 事件处理程序(例如我们的 App::OnActivated 方法)。

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

我们在这里的唯一工作就是激活主要的 CoreWindow。 或者,可以选择在 App::SetWindow中执行此操作。

App::Run 方法

初始化SetWindow加载 已做好准备。 游戏启动并运行后,将调用 IFrameworkView::Run 的实现。

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

同样,工作被委托给 GameMain

GameMain::Run 方法

GameMain::Run 是游戏的主要循环;可以在 GameMain.cpp中找到它。 基本逻辑是,当游戏窗口保持打开状态时,调度所有事件,更新计时器,然后呈现和呈现图形管道的结果。 此外,还会调度和处理用于在游戏状态之间转换的事件。

此处的代码还涉及游戏引擎状态机中的两种状态。

  • UpdateEngineState::Deactivated。 这说明游戏窗口已停用(失去焦点)或固定。
  • UpdateEngineState: TooSmall。 指定的客户端区域太小,无法渲染游戏。

在这两种状态下,游戏会暂停事件处理,并等待窗口激活、退出固定状态或调整大小。

虽然游戏窗口可见(Window.Visibletrue),但必须在消息队列到达时处理每个事件,因此必须使用 ProcessAllIfPresent 选项调用 CoreWindowDispatch.ProcessEvents。 其他选项可能会导致处理消息事件的延迟,这可能会使游戏感觉无响应,或导致触摸行为感觉迟钝。

当游戏 可见时(Window.Visiblefalse),暂停时,或窗口太小(如贴靠时),您不会希望它在循环调度永远不会到达的消息时消耗任何资源。 在这种情况下,游戏必须使用 ProcessOneAndAllPending 选项。 该选项会一直等待,直到获取一个事件,然后处理该事件(以及在处理第一个事件期间进入进程队列的其他任何事件)。 CoreWindowDispatch.ProcessEvents 在处理完队列后立即返回。

在如下所示的示例代码中,m_visible 数据成员表示窗口的可见性。 游戏暂停时,其窗口不可见。 当窗口 可见时,m_updateState 的值(UpdateEngineState 枚举)将进一步确定窗口是否已停用(失去焦点)、太小(贴靠),还是大小正确。

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.
}

App::Uninitialize 方法

游戏结束后,将调用 IFrameworkView::Uninitialize 的实现。 这是我们进行整理的机会。 关闭应用窗口不会终止应用的进程;而是将应用单一实例的状态写入内存。 如果系统回收此内存时必须出现任何特殊情况(包括任何特殊的资源清理),请将该清理的代码放入 反初始化

在本例中,App::Uninitialize 是 no-op。

void Uninitialize()
{
}

提示

开发自己的游戏时,围绕本主题中所述的方法设计启动代码。 下面是每个方法的基本建议的简单列表。

  • 使用 初始化 来分配主类,并连接基本事件处理程序。
  • 使用 SetWindow 来订阅任何窗口特定的事件,并将主窗口传递给设备相关的资源对象,以便在创建交换链时使用该窗口。
  • 使用 加载 来处理任何剩余的设置,并启动对象的异步创建以及资源加载。 如果需要创建任何临时文件或数据(如过程生成的资产),也在此处执行此操作。

后续步骤

本主题介绍了使用 DirectX 的 UWP 游戏的一些基本结构。 记住这些方法是个好主意,因为我们将在后面的主题中回顾其中一些方法。

在下一个主题(游戏流管理)中,我们将深入探讨如何管理游戏状态和事件处理,以保持游戏流。