Определение структуры приложения для игры UWPDefine the game's UWP app framework

Примечание

Эта статья является частью серии руководств по созданию простой универсальная платформа Windows (UWP) с помощью DirectX .This topic is part of the Create a simple Universal Windows Platform (UWP) game with DirectX tutorial series. В разделе этой ссылки задается контекст для ряда.The topic at that link sets the context for the series.

Первым шагом в программировании игры универсальная платформа Windows (UWP) является создание платформы, которая позволяет объекту приложения взаимодействовать с окнами, включая среда выполнения Windows такие функции, как приостановка и возобновление обработки событий, изменения фокуса окна и привязка.The first step in coding a Universal Windows Platform (UWP) game is building the framework that lets the app object interact with Windows, including Windows Runtime features such as suspend-resume event handling, changes in window focus, and snapping.

ЦелиObjectives

  • Настройте платформу для игры DirectX универсальная платформа Windows (UWP) и реализуйте конечный автомат, который определяет общий поток игр.Set up the framework for a Universal Windows Platform (UWP) DirectX game, and implement the state machine that defines the overall game flow.

Примечание

Для работы с этим разделом найдите исходный код для скачанного примера игры Simple3DGameDX .To follow along with this topic, look in the source code for the Simple3DGameDX sample game that you downloaded.

ВведениеIntroduction

В разделе Настройка игрового проекта мы предоставили функцию wWinMain , а также интерфейсы IFrameworkViewSource и IFrameworkView .In the Set up the game project topic, we introduced the wWinMain function as well as the IFrameworkViewSource and IFrameworkView interfaces. Мы узнали, что класс приложения (который можно увидеть в App.cpp файле исходного кода в проекте Simple3DGameDX ) выступает в качестве фабрики и представления поставщика представления .We learned that the App class (which you can see defined in the App.cpp source code file in the Simple3DGameDX project) serves as both view-provider factory and view-provider.

В этом разделе мы расскажу об этом, и подробно узнаете, как класс приложения в игре должен реализовывать методы IFrameworkView.This topic picks up from there, and goes into much more detail about how the App class in a game should implement the methods of IFrameworkView.

Метод App:: InitializeThe App::Initialize method

При запуске приложения первым методом, который вызывает Windows, является наша реализация IFrameworkView:: Initialize.Upon application launch, the first method that Windows calls is our implementation of IFrameworkView::Initialize.

Ваша реализация должна справляться с наиболее фундаментальным поведением игры UWP, например убедиться, что игра может справиться с паузой (и возможной позже возобновлением) с помощью подписки на эти события.Your implementation should handle the most fundamental behaviors of a UWP game, such as making sure that the game can handle a suspend (and a possible later resume) event by subscribing to those events. У нас также есть доступ к устройству видеоадаптера, поэтому мы можем создавать графические ресурсы, зависящие от устройства.We also have access to the display adapter device here, so we can create graphics resources that depend on the device.

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

Старайтесь избегать необработанных указателей, когда это возможно (и почти всегда возможно).Avoid raw pointers whenever possible (and it's nearly always possible).

  • Для среда выполнения Windows типов можно очень часто избегать указателей и просто создать значение в стеке.For Windows Runtime types, you can very often avoid pointers altogether and just construct a value on the stack. Если вам нужен указатель, используйте WinRT:: com_ptr (мы увидим пример в ближайшее время).If you do need a pointer, then use winrt::com_ptr (we'll see an example of that soon).
  • Для уникальных указателей используйте std:: unique_ptr и std:: make_unique.For unique pointers, use std::unique_ptr and std::make_unique.
  • Для общих указателей используйте std:: shared_ptr и std:: make_shared.For shared pointers, use std::shared_ptr and std::make_shared.

Метод App:: СетвиндовThe App::SetWindow method

После инициализацииWindows вызывает нашу реализацию IFrameworkView:: сетвиндов, передавая объект CoreWindow , представляющий главное окно игры.After Initialize, Windows calls our implementation of IFrameworkView::SetWindow, passing a CoreWindow object representing the game's main window.

В app:: сетвиндовмы подписались на события, связанные с окнами, и настроим некоторые окна и поведение при отображении.In App::SetWindow, we subscribe to window-related events, and configure some window and display behaviors. Например, мы создаем указатель мыши (через класс корекурсор ), который может использоваться как элементами управления мыши, так и сенсорным касанием.For example, we construct a mouse pointer (via the CoreCursor class), which can be used by both mouse and touch controls. Также мы передаем объект Window в объект ресурсов, зависимый от устройства.We also pass the window object to our device-dependent resources object.

Дополнительные сведения об обработке событий см. в разделе Управление потоком игр .We'll talk more about handling events in the Game flow management topic.

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:: LoadThe App::Load method

Теперь, когда главное окно задано, вызывается наша реализация IFrameworkView:: Load .Now that the main window is set, our implementation of IFrameworkView::Load is called. Загрузка — это лучшее место для предварительной выборки данных или ресурсов игр, чем Инициализация и сетвиндов.Load is a better place to pre-fetch game data or assets than Initialize and SetWindow.

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

Как видите, фактическая работа делегируется конструктору объекта гамемаин , который мы сделаем здесь.As you can see, the actual work is delegated to the constructor of the GameMain object that we make here. Класс гамемаин определен в GameMain.h и GameMain.cpp .The GameMain class is defined in GameMain.h and GameMain.cpp.

Конструктор Гамемаин:: ГамемаинThe GameMain::GameMain constructor

Конструктор гамемаин (и другие функции-члены, которые он вызывает) начинает набор асинхронных операций загрузки для создания игровых объектов, загрузки графических ресурсов и инициализации конечного автомата игры.The GameMain constructor (and the other member functions that it calls) begins a set of asynchronous loading operations to create the game objects, load graphics resources, and initialize the game's state machine. Мы также выполним все необходимые подготовительные действия перед началом игры, например задание любых начальных состояний или глобальных значений.We also do any necessary preparations before the game begins, such as setting any starting states or global values.

Windows накладывает ограничение на время, в течение которого ваша игра сможет начать обработку входных данных.Windows imposes a limit on the time your game can take before it begins processing input. Итак, использование АСИК, как мы делаем здесь, означает, что нагрузка может быстро возвращаться в фоновом режиме.So using asyc, as we do here, means that Load can return quickly while the work that it has begun continues in the background. Если загрузка занимает много времени или при наличии большого количества ресурсов, рекомендуется предоставить пользователям часто обновляемый индикатор выполнения.If loading takes a long time, or if there are lots of resources, then providing your users with a frequently updated progress bar is a good idea.

Если вы не знакомы с асинхронным программированием, см. статью Параллельные и асинхронные операции с C++/WinRT.If you're new to asynchronous programming, then see Concurrency and asynchronous operations with 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 have focus. 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.
    ...
}

Ниже приведена структура последовательности действий, которые запускает конструктор.Here's an outline of the sequence of work that's kicked off by the constructor.

  • Создайте и инициализируйте объект типа гамерендерер.Create and initialize an object of type GameRenderer. Подробнее: Платформа отрисовки I: введение в отрисовку.For more information, see Rendering framework I: Intro to rendering.
  • Создайте и инициализируйте объект типа Simple3DGame.Create and initialize an object of type Simple3DGame. Подробнее: Определение основного игрового объекта.For more information, see Define the main game object.
  • Создайте объект элемента управления пользовательского интерфейса игры и отобразите наложение сведений о игре, чтобы показать индикатор выполнения по мере загрузки файлов ресурсов.Create the game UI control object, and display game info overlay to show a progress bar as the resource files load. Подробнее: Добавление пользовательского интерфейса.For more information, see Adding a user interface.
  • Создайте объект контроллера для считывания входных данных из контроллера (сенсорный экран, мышь или беспроводной контроллер Xbox).Create a controller object to read input from the controller (touch, mouse, or Xbox wireless controller). Подробнее: Добавление элементов управления.For more information, see Adding controls.
  • Определите две прямоугольные области в нижнем левом и нижнем правом углу экрана для сенсорных элементов управления перемещения и камеры соответственно.Define two rectangular areas in the lower-left and lower-right corners of the screen for the move and camera touch controls, respectively. Проигрыватель использует левый нижний прямоугольник (определенный в вызове сетмоверект) в качестве виртуальной панели управления для перемещения камеры вперед и назад, а также параллельно с стороной.The player uses the lower-left rectangle (defined in the call to SetMoveRect) as a virtual control pad for moving the camera forward and backward, and side to side. Правый нижний прямоугольник (определенный методом сетфирерект ) используется в качестве виртуальной кнопки для запуска АММО.The lower-right rectangle (defined by the SetFireRect method) is used as a virtual button to fire the ammo.
  • Используйте соподпрограммы для разбиения загрузки ресурсов на отдельные этапы.Use coroutines to break resource loading into separate stages. Доступ к контексту устройства Direct3D ограничивается потоком, в котором был создан контекст устройства; при доступе к устройству Direct3D для создания объектов используется беспотоковая поддержка.Access to the Direct3D device context is restricted to the thread on which the device context was created; while access to the Direct3D device for object creation is free-threaded. Следовательно, соподпрограмма гамерендерер:: креатегамедевицересаурцесасинк может выполняться в отдельном потоке от задачи завершения (Гамерендерер:: финализекреатегамедевицересаурцес), которая выполняется в исходном потоке.Consequently, the GameRenderer::CreateGameDeviceResourcesAsync coroutine can run on a separate thread from the completion task (GameRenderer::FinalizeCreateGameDeviceResources), which runs on the original thread.
  • Мы используем аналогичный шаблон для загрузки ресурсов уровня с помощью Simple3DGame:: лоадлевеласинк и Simple3DGame:: финализелоадлевел.We use a similar pattern for loading level resources with Simple3DGame::LoadLevelAsync and Simple3DGame::FinalizeLoadLevel.

Дополнительные сведения о гамемаин:: инитиализегаместате см. в следующем разделе (Управление игровыми потоком).We'll see more of GameMain::InitializeGameState in the next topic (Game flow management).

Метод App:: OnActivatedThe App::OnActivated method

Далее возникает событие кореаппликатионвиев:: Activated .Next, the CoreApplicationView::Activated event is raised. Поэтому вызывается любой обработчик событий OnActivated (например, наш метод app:: OnActivated ).So any OnActivated event handler that you have (such as our App::OnActivated method) is called.

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

Единственная работа, которую мы делаем здесь, — активировать основной CoreWindow.The only work we do here is to activate the main CoreWindow. Кроме того, это можно сделать в app:: сетвиндов.Alternatively, you can choose to do that in App::SetWindow.

Метод App:: RunThe App::Run method

Инициализация, сетвиндови Загрузка задают этап.Initialize, SetWindow, and Load have set the stage. Теперь, когда игра запущена и работает, вызывается наша реализация IFrameworkView:: Run .Now that the game is up and running, our implementation of IFrameworkView::Run is called.

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

Опять же, работа делегируется гамемаин.Again, work is delegated to GameMain.

Метод Гамемаин:: RunThe GameMain::Run method

Гамемаин:: Run — это основной цикл игры; его можно найти в GameMain.cpp .GameMain::Run is the main loop of the game; you can find it in GameMain.cpp. Основная логика заключается в том, что пока окно игры остается открытым, отправляются все события, обновляете таймер, а затем готовится к просмотру и представляют результаты графического конвейера.The basic logic is that while the window for your game remains open, dispatch all events, update the timer, and then render and present the results of the graphics pipeline. Кроме того, события, используемые для перехода между состояниями игры, отправляются и обрабатываются.Also here, the events used to transition between game states are dispatched and processed.

Этот код также касается двух состояний в автомате для игровых механизмов.The code here is also concerned with two of the states in the game engine state machine.

  • Упдатингинестате::D еактиватед.UpdateEngineState::Deactivated. Это означает, что окно игры деактивировано (потеряно фокус) или привязано.This specifies that the game window is deactivated (has lost focus) or is snapped.
  • Упдатингинестате:: тусмалл.UpdateEngineState::TooSmall. Это означает, что клиентская область слишком мала для визуализации игры в.This specifies that the client area is too small to render the game in.

В любом из этих состояний игра приостанавливает обработку событий и ожидает, пока окно не потеряет фокус, не привязывается или не изменит его размер.In either of these states, the game suspends event processing, and waits for the window to focus, to unsnap, or to be resized.

Когда игра удерживает фокус, необходимо обрабатывать каждое событие в очереди сообщений по мере их поступления, поэтому требуется вызвать CoreWindowDispatch.ProcessEvents с помощью параметра ProcessAllIfPresent.When your game has focus, you must handle every event in the message queue as it arrives, and so you must call CoreWindowDispatch.ProcessEvents with the ProcessAllIfPresent option. Другие параметры могут вызвать задержки при обработке событий сообщений, что может сделать игру неотвечающей или привести к низкой реакции на поведение сенсорного экрана.Other options can cause delays in processing message events, which can make your game feel unresponsive, or result in touch behaviors that feel sluggish.

Если игра не отображается, приостановлена или прикреплена, вы не хотите, чтобы она использовала все ресурсы для отправки сообщений, которые никогда не поступают.When the game is not visible, suspended, nor snapped, you don't want it to consume any resources cycling to dispatch messages that will never arrive. В этом случае для игры необходимо использовать параметр процессонеандаллпендинг .In this case, your game must use the ProcessOneAndAllPending option. Этот параметр блокируется до тех пор, пока не будет получено событие, а затем обработает это событие (а также все остальные, поступающие в очередь процессов во время обработки первой).That option blocks until it gets an event, and then processes that event (as well as any others that arrive in the process queue during the processing of the first). Коревиндовдиспатч. ProcessEvents затем немедленно возвращается после обработки очереди.CoreWindowDispatch.ProcessEvents then immediately returns after the queue has been processed.

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:: UninitializeThe App::Uninitialize method

Когда игра завершается, вызывается наша реализация IFrameworkView:: Uninitialize .When the game ends, our implementation of IFrameworkView::Uninitialize is called. Это наша возможность выполнить очистку.This is our opportunity to perform cleanup. Закрытие окна приложения не приводит к завершению процесса приложения. Вместо этого он записывает состояние одноэлементного приложения в память.Closing the app window doesn't kill the app's process; but instead it writes the state of the app singleton to memory. Если при освобождении памяти системой потребуется нечто специальное, в том числе все специальные ресурсы, а затем поставьте код для этой очистки при неинициализации.If anything special must happen when the system reclaims this memory, including any special cleanup of resources, then put the code for that cleanup in Uninitialize.

В нашем случае Приложение:: Uninitialize — это No-Op.In our case, App::Uninitialize is a no-op.

void Uninitialize()
{
}

СоветыTips

При разработке собственной игры Разрабатывайте код запуска вокруг методов, описанных в этом разделе.When developing your own game, design your startup code around the methods described in this topic. Ниже приведен простой список основных рекомендаций для каждого метода.Here's a simple list of basic suggestions for each method.

  • Используйте инициализацию для выделения основных классов и подключения основных обработчиков событий.Use Initialize to allocate your main classes, and connect up the basic event handlers.
  • Используйте сетвиндов , чтобы подписываться на любые события, относящиеся к окну, а также для передачи главного окна в объект ресурсов, зависящих от устройства, чтобы он мог использовать это окно при создании цепочки буферов.Use SetWindow to subscribe to any window-specific events, and to pass your main window to your device-dependent resources object so that it can use that window when creating a swap chain.
  • Используйте загрузку для выполнения оставшихся настроек, а также для инициации асинхронного создания объектов и загрузки ресурсов.Use Load to handle any remaining setup, and to initiate the asynchronous creation of objects, and loading of resources. Если необходимо создать временные файлы или данные, такие как процедурно сформированные ресурсы, это также можно сделать здесь.If you need to create any temporary files or data, such as procedurally generated assets, then do that here, too.

Дальнейшие действияNext steps

В этом разделе рассматривается базовая структура игры UWP, в которой используется DirectX.This topic has covered some of the basic structure of a UWP game that uses DirectX. Рекомендуется помнить об этих методах, так как мы будем ссылаться на некоторые из них в следующих разделах.It's a good idea to keep these methods in mind, because we'll be referring back to some of them in later topics.

В следующем разделе — Управление потоком игрмы подробно рассмотрим, — как управлять состояниями игры и обработкой событий, чтобы избежать поступающих игр.In the next topic—Game flow management—we'll take an in-depth look at how to manage game states and event handling in order to keep the game flowing.