Определение платформы приложений UWP игры

Замечание

Этот раздел является частью серии руководств по созданию простой игры на универсальной платформе Windows (UWP) с использованием DirectX. Тема на той ссылке определяет контекст для серии.

Первым шагом в кодировании игры на универсальной платформе Windows (UWP) является создание каркаса, позволяющего объекту приложения взаимодействовать с Windows, включая функции среды выполнения Windows, такие как обработка событий приостановки и возобновления; изменения в видимости окна и функция привязки.

Цели

  • Настройте структуру для игры на универсальной платформе Windows (UWP) с использованием DirectX и реализуйте конечный автомат, определяющий основной игровой процесс.

Замечание

Чтобы ознакомиться с этим разделом, ознакомьтесь с исходным кодом Simple3DGameDX скачанным примером игры.

Введение

В разделе "Настройка проекта игры" мы представили функцию wWinMain, а также интерфейсы IFrameworkViewSource и IFrameworkView. Мы узнали, что класс App (который можно увидеть в файле исходного кода App.cpp в проекте Simple3DGameDX) служит как фабрикой поставщиков представлений , так и поставщиком представлений .

В этом разделе рассматриваются более подробные сведения о том, как класс 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_ptr и std::make_unique.
  • Для общих указателей используйте std::shared_ptr и std::make_shared.

Метод App::SetWindow

После инициализацииWindows вызывает нашу реализацию IFrameworkView::SetWindow, передавая объект CoreWindow, который представляет главное окно игры.

В Приложении::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.h и GameMain.cpp.

Конструктор GameMain::GameMain

Конструктор GameMain (и другие вызываемые функции-члены) начинает набор асинхронных операций загрузки для создания игровых объектов, загрузки графических ресурсов и инициализации машины состояний игры. Мы также выполняем все необходимые приготовления перед началом игры, например, настройка любых начальных состояний или глобальных значений.

Windows устанавливает ограничение на время, которое может пройти до начала обработки входных данных вашей игры. Таким образом, использование асинхронного режима означает, что загрузка может быстро вернуться во время работы, которая началась в фоновом режиме. Если загрузка занимает много времени или есть много ресурсов, то рекомендуется предоставить пользователям часто обновляемую панель хода выполнения.

Если вы не знакомы с асинхронным программированием, ознакомьтесь с параллелизмом и асинхронными операциями с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. Дополнительные сведения см. в разделе "Определение основного игрового объекта".
  • Создайте объект управления пользовательским интерфейсом игры и отобразите наложение информации о игре, чтобы показать индикатор прогресса во время загрузки файлов ресурсов. Для получения дополнительной информации см. раздел «Добавление пользовательского интерфейса».
  • Создайте объект контроллера для чтения входных данных с контроллера (сенсорный, мышь или игровой контроллер). Дополнительные сведения см. в разделе Добавление элементов управления.
  • Определите две прямоугольные области в левом нижнем и правом нижнем углах экрана для элементов управления перемещением и сенсорным экраном камеры соответственно. Игрок использует нижний левый прямоугольник (определенный в вызове SetMoveRect) в качестве виртуальной панели управления для перемещения камеры вперед и назад, а также из стороны в сторону. Прямоугольник в правом нижнем углу (определяется методом SetFireRect SetFireRect) используется в качестве виртуальной кнопки для запуска ammo.
  • Используйте корутины для разбиения загрузки ресурсов на отдельные этапы. Доступ к контексту устройства Direct3D ограничен потоком, на котором этот контекст был создан, тогда как доступ к устройству Direct3D для создания объектов является многопоточным. Следовательно, GameRenderer::CreateGameDeviceResourcesAsync coroutine может выполняться в отдельном потоке задачи завершения (GameRenderer::FinalizeCreateGameDeviceResources), которая выполняется в исходном потоке.
  • Мы используем аналогичный шаблон для загрузки ресурсов уровня с помощью Simple3DGame::LoadLevelAsync и Simple3DGame::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и Load настроили этап. Теперь, когда игра запущена и работает, вызывается наша реализация IFrameworkView::Run.

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

Опять же, работа делегирована GameMain.

Метод GameMain::Run

GameMain::Run — это основной цикл игры; его можно найти в GameMain.cpp. Основная логика заключается в том, что, пока окно для игры остается открытым, отправляет все события, обновляет таймер, а затем отрисовывает и отображает результаты графического конвейера. Кроме того, здесь события, используемые для перехода между игровыми состояниями, отправляются и обрабатываются.

Код здесь также связан с двумя состояниями в машине состояний игрового движка.

  • UpdateEngineState::Деактивирован. Это указывает, что окно игры деактивировано (потеряло фокус) или защелкнуто.
  • UpdateEngineState::TooSmall. Это указывает, что окно клиента слишком мало для отображения игры.

В любом из этих состояний игра приостанавливает обработку событий и ожидает, пока окно не станет активным, его открепления или изменения его размера.

Хотя окно игры отображается (Window.Visibletrue), необходимо обрабатывать каждое событие в очереди сообщений по мере поступления и поэтому необходимо вызвать CoreWindowDispatch.ProcessEvents с параметром ProcessAllIfPresent. Другие варианты могут привести к задержкам в обработке событий сообщений, что может сделать вашу игру менее отзывчивой или привести к тому, что сенсорное управление будет казаться вялым.

Если игра не видна (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. Это наша возможность выполнить очистку. Закрытие окна приложения не прекратит процесс приложения, а вместо этого записывает состояние синглтона приложения в память. Если что-нибудь особенное должно произойти, когда система освобождает эту память, включая любую специальную очистку ресурсов, то поместите код для этой очистки в Uninitialize.

В нашем случае App::Uninitialize является no-op.

void Uninitialize()
{
}

Советы

При разработке собственной игры создайте код запуска вокруг методов, описанных в этом разделе. Ниже приведен простой список основных предложений для каждого метода.

  • Используйте Initialize, чтобы распределить основные классы и подключить базовые обработчики событий.
  • Используйте SetWindow, чтобы подписаться на любые события, связанные с окном, и передать главное окно объекту ресурсов, зависимым от устройства, чтобы оно пользовалось этим окном при создании цепочки буферов.
  • Используйте загрузку для обработки оставшихся настроек, а также для инициирования асинхронного создания объектов и загрузки ресурсов. Если вам нужно создать временные файлы или данные, такие как процедурно созданные ресурсы, сделайте это здесь тоже.

Дальнейшие шаги

В этой статье рассматривается некоторая базовая структура игры UWP, использующая DirectX. Рекомендуется помнить об этих методах, так как мы будем ссылаться на некоторые из них в последующих разделах.

В следующем разделе — управление потоками игр — мы подробно рассмотрим, как управлять состояниями игры и обработкой событий, чтобы обеспечить поток игры.