Определение основного игрового объекта

Замечание

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

После того как вы изложили базовую платформу примера игры и реализовали машину состояния, которая обрабатывает поведение пользователя высокого уровня и системы, вы захотите проверить правила и механики, которые превратят образец игры в игру. Рассмотрим подробные сведения о главном объекте образцовой игры и о том, как преобразовать правила игры во взаимодействие с игровым миром.

Цели

  • Узнайте, как применять основные методы разработки для реализации правил игры и механики для игры UWP DirectX.

Основной игровой объект

В примере игры Simple3DGameDXSimple3DGame является основным классом объектов игры. Экземпляр Simple3DGame создается косвенно с помощью метода App::Load.

Ниже приведены некоторые функции класса Simple3DGame.

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

    Замечание

    Обновление графики обрабатывается классом GameRenderer, который содержит методы для получения и использования ресурсов графического устройства, используемых игрой. Дополнительную информацию см. в разделе Фреймворк рендеринга I: Введение в рендеринг.

  • Служит контейнером для данных, определяющих игровой сеанс, уровень или время существования, в зависимости от того, как вы определяете игру на высоком уровне. В этом случае данные о состоянии игры в течение всего времени существования игры инициализированы один раз, когда пользователь запускает игру.

Чтобы просмотреть методы и данные, определенные этим классом, см. класс Simple3DGame ниже.

Инициализация и запуск игры

Когда игрок запускает игру, объект игры должен инициализировать состояние, создать и добавить наложение, задать переменные, отслеживающие производительность игрока, и создать экземпляры объектов, которые будут использоваться для создания уровней. В этом примере это делается при создании экземпляра GameMain GameMain в App::Load.

Объект игры Simple3DGameсоздается в конструкторе GameMain::GameMain. Затем объект инициализируется с помощью метода Simple3DGame::Initialize в процессе выполнения fire-and-forget корутины GameMain::ConstructInBackground, которая вызывается из GameMain::GameMain.

Метод Simple3DGame::Initialize

Пример игры настраивает эти компоненты в объекте игры.

  • Создается новый объект воспроизведения звука.
  • Создаются массивы графических примитивов игры, включая массивы для примитивов уровня, боеприпасов и препятствий.
  • Создается расположение для сохранения данных о состоянии игры с именем Gameи помещается в место, указанное в настройках данных приложения как ApplicationData::Current.
  • Создается таймер игры и начальное наложенное изображение в игре.
  • Новая камера создается с определенным набором параметров представления и проекции.
  • Устройство ввода (контроллер) устанавливается на тот же начальный угол наклона и поворота, что и камера, поэтому у игрока есть соответствие 1 к 1 между начальным положением контроллера и положением камеры.
  • Объект игрока создается и активируется. Мы используем объект сфера для обнаружения близости игрока к стенам и препятствиям и чтобы не позволять камере оказаться в положении, которое может испортить погружение.
  • Создается примитив мира игры.
  • Создаются цилиндрические препятствия.
  • Целевые объекты (объекты face) создаются и нумеруются.
  • Создаются сферы аммо.
  • Создаются уровни.
  • Высокая оценка загружается.
  • Загружается любое предыдущее сохраненное состояние игры.

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

Создание и загрузка уровней игры

Большая часть работы по строительству уровня выполняется в файлах Level[N].h/.cpp, находящихся в папке GameLevels образца решения. Поскольку он фокусируется на очень конкретной реализации, мы не будем охватывать их здесь. Важно, чтобы код для каждого уровня выполнялся как отдельный объект Level[N]. Если вы хотите расширить игру, можно создать объект Level[N], который принимает назначенное число в качестве параметра и случайным образом помещает препятствия и цели. Вы также можете загружать данные конфигурации уровня из файла ресурсов или даже из Интернета.

Определение игрового процесса

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

Лучшие игры мгновенно реагируют на входные данные игрока и предоставляют немедленную обратную связь. Это верно для любого типа игры: от экшенов в реальном времени, шутеров от первого лица, до вдумчивых, пошаговых стратегических игр.

Метод Simple3DGame::RunGame

Пока игровой уровень продолжается, игра находится в состоянии Dynamics.

GameMain::Update — это основной цикл обновления, который обновляет состояние приложения один раз на кадр, как показано ниже. Цикл обновления вызывает метод Simple3DGame::RunGame для обработки работы, если игра находится в состоянии Dynamics.

// 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::UpdateDynamics вызывается из метода Simple3DGame::RunGame (который вызывается из GameMain::Update) для обновления объектов, отображаемых в игровой сцене.

Цикл, такой как 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 рисует проекцию трехмерного мира, а затем рисует наложение 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 включают следующие функции.

  • инициализировать. Задает начальные значения глобальных переменных и инициализирует объекты игры. Это рассматривается в разделе Инициализация и запуск игры.
  • Загрузить игру. Инициализирует новый уровень и начинает загрузку.
  • LoadLevelAsync. Корутина, которая инициализирует уровень, а затем вызывает другую корутину, используя отрисовщик, для загрузки ресурсов уровня, специфичных для устройства. Этот метод выполняется в отдельном потоке; в результате из этого потока можно вызывать только методы ID3D11Device, в отличие от методов ID3D11DeviceContext. Все методы контекста устройства вызываются в методе FinalizeLoadLevel. Если вы не знакомы с асинхронным программированием, ознакомьтесь с параллелизмом и асинхронными операциями сC++/WinRT.
  • Завершениеуровнязагрузки. Завершает любую работу, связанную с загрузкой уровня, которую нужно выполнить в основном потоке. Сюда входят любые вызовы методов в контексте устройства Direct3D 11 (ID3D11DeviceContext).
  • Начальный уровень. Запускает игровой процесс для нового уровня.
  • PauseGame. Приостанавливает игру.
  • RunGame. Выполняет итерацию цикла игры. Он вызывается из App::Update один раз при каждой итерации цикла игры, если состояние игры Активное.
  • OnSuspending и OnResuming. Приостановка и возобновление звука игры соответственно.

Ниже приведены частные функции-члены.

  • LoadSavedState и SaveState. Загрузка и сохранение текущего состояния игры соответственно.
  • LoadHighScore и SaveHighScore. Загрузка и сохранение высокой оценки в играх соответственно.
  • ИнициализацияAmmo. Сбрасывает состояние каждого сферического объекта, используемого в качестве боеприпасов, в его первоначальное состояние в начале каждого раунда.
  • UpdateDynamics. Это важный метод, так как он обновляет все игровые объекты на основе консервированных процедур анимации, физики и управления входными данными. Это сердце интерактивности, определяющей игру. Это рассматривается в разделе Обновление игрового мира.

Другие общедоступные методы — это методы доступа к свойствам, которые возвращают информацию, специфичную для игрового процесса и наложений, в инфраструктуру приложения для отображения.

Элементы данных

Эти объекты обновляются при выполнении цикла игры.

  • объект MoveLookController. Представляет входные данные проигрывателя. Дополнительные сведения см. в разделе Добавление элементов управления.
  • объект GameRenderer. Представляет рендерер Direct3D 11, который управляет всеми объектами, специфичными для устройства, и их рендерингом. Дополнительные сведения см. в структуре рендеринга.
  • аудио объект. Управляет воспроизведением звука для игры. Дополнительные сведения см. в разделе Добавление звука.

Остальные переменные игры содержат списки примитивов и их соответствующих объемов в игре, а также данные и ограничения, специфичные для игрового процесса.

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

Мы еще не говорили о фактическом движке отрисовки — о том, как вызовы методов Render на обновленных примитивах превращаются в пиксели на вашем экране. Эти аспекты рассматриваются в двух частях:фреймворк отрисовки I: введение в отрисовку и фреймворк отрисовки II: отрисовка игр. Если вы больше заинтересованы в том, как игрок управляет обновлением состояния игры, см. статью Добавление элементов управления.