Определение структуры приложения UWP
Примечание.
Этот раздел является частью серии руководств по созданию простой игры универсальная платформа Windows (UWP) с помощью DirectX. Эта ссылка задает контекст для ряда.
Первым шагом в программировании игры универсальная платформа Windows (UWP) является создание платформы, которая позволяет объекту приложения взаимодействовать с Windows, включая среда выполнения Windows функции, такие как обработка событий приостановки возобновления, изменения в видимости окна и привязка.
Задачи
- Настройте платформу для игры DirectX универсальная платформа Windows (UWP) и реализуйте компьютер с состоянием, определяющий общий поток игры.
Примечание.
Чтобы следовать этому разделу, ознакомьтесь с исходным кодом для скачаемой игры 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 . Загрузка лучше всего подходит для предварительного получения игровых данных или ресурсов, чем Initialize и 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. Дополнительные сведения см. в разделе "Инфраструктура отрисовки" для отрисовки.
- Создание и инициализация объекта типа Simple3DGame. Дополнительные сведения см. в разделе "Определение основного игрового объекта".
- Создайте объект управления пользовательским интерфейсом игры и наложите сведения о игре, чтобы отобразить индикатор хода выполнения в виде загрузки файлов ресурсов. Дополнительные сведения см. в разделе "Добавление пользовательского интерфейса".
- Создайте объект контроллера для чтения входных данных с контроллера (сенсорный, мышь или игровой контроллер). Дополнительные сведения см. в разделе "Добавление элементов управления".
- Определите две прямоугольные области в левом нижнем и правом нижнем углах экрана для элементов управления перемещением и сенсорным экраном камеры соответственно. Проигрыватель использует прямоугольник в левом нижнем углу (определенный в вызове SetMoveRect) в качестве виртуальной панели управления для перемещения камеры вперед и назад и стороны на сторону. Прямоугольник в правом нижнем углу (определенный методом SetFireRect ) используется в качестве виртуальной кнопки для запуска ammo.
- Используйте корутины для разбиения загрузки ресурсов на отдельные этапы. Доступ к контексту устройства Direct3D ограничен потоком, на котором был создан контекст устройства; при доступе к устройству Direct3D для создания объекта предоставляется свободный поток. Следовательно, coroutine GameRenderer::CreateGameDeviceResourcesAsync может выполняться в отдельном потоке задачи завершения (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::D eactivated. Это указывает, что окно игры деактивировано (потерял фокус) или откреплено.
- UpdateEngineState::TooSmall. Это указывает, что область клиента слишком мала для отрисовки игры.
В любом из этих состояний игра приостанавливает обработку событий и ожидает активации окна, отмены или изменения размера.
Хотя окно игры отображается (Window.Visibletrue
), необходимо обрабатывать каждое событие в очереди сообщений по мере его поступления, поэтому необходимо вызвать CoreWindowDispatch.ProcessEvents с параметром ProcessAllIfPresent. Другие варианты могут привести к задержкам в обработке событий сообщений, что может сделать вашу игру неответствующей или привести к поведению касания, которые чувствуют себя вялыми.
Если игра не видна (Window.Visible false
имеет значение), или когда она приостановлена, или когда она слишком мала (она откреплена), вы не хотите, чтобы она потребляла какие-либо ресурсы для отправки сообщений, которые никогда не будут поступать. В этом случае игра должна использовать параметр 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()
{
}
Советы
При разработке собственной игры создайте код запуска вокруг методов, описанных в этом разделе. Ниже приведен простой список основных предложений для каждого метода.
- Инициализация для выделения основных классов и подключения основных обработчиков событий.
- Используйте SetWindow , чтобы подписаться на любые события, относящиеся к окне, и передать главное окно объекту ресурсов, зависимым от устройства, чтобы оно пользовалось этим окном при создании цепочки буферов.
- Используйте Load для обработки любой оставшейся установки, а также для запуска асинхронного создания объектов и загрузки ресурсов. Если вам нужно создать временные файлы или данные, такие как процедурно созданные ресурсы, сделайте это здесь тоже.
Следующие шаги
В этой статье рассматривается некоторая базовая структура игры UWP, использующая DirectX. Рекомендуется помнить об этих методах, так как мы будем ссылаться на некоторые из них в последующих разделах.
В следующем разделе — управление потоками игр — мы подробно рассмотрим, как управлять состояниями игры и обработкой событий, чтобы обеспечить поток игры.