Управление потоком игры

Примечание

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

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

Игровые состояния, используемые для управления потоком игры

Мы используем состояния игры, чтобы управлять процессом игры.

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

  • Игра не была запущена, или игра находится между уровнями (высокий балл равен нулю).
  • Игровой цикл выполняется и находится в середине уровня.
  • Игровой цикл не выполняется из-за завершения игры (высокая оценка имеет ненулевое значение).

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

Управление состоянием игры

Таким образом, во время инициализации игры необходимо поддерживать холодный запуск игры, а также возобновление игры после остановки в полете. Пример Simple3DGameDX всегда сохраняет состояние игры, чтобы создать впечатление, что он никогда не останавливался.

В ответ на событие приостановки игровой процесс приостанавливается, но ресурсы игры по-прежнему находятся в памяти. Аналогичным образом обрабатывается событие возобновления, чтобы убедиться, что образец игры забирается в состояние, в которое она находилась на момент приостановки или завершения. В зависимости от состояния игроку предоставляются разные возможности.

  • Если игра возобновляется в середине уровня, то она будет приостановлена, и наложение предлагает возможность продолжить.
  • Если игра возобновляется в состоянии завершения игры, отображаются высокие баллы и возможность играть в новую игру.
  • Наконец, если игра возобновляется до начала уровня, то наложение представляет пользователю параметр запуска.

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

В этом примере инициализация состояний игры выполняется в GameMain::InitializeGameState (структура этого метода показана в следующем разделе).

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

  • Инициализация начинается с узла Start при проверке текущего состояния игры. Код игры см. в разделе GameMain::InitializeGameState в следующем разделе.

Главный конечный автомат для игры

Метод GameMain::InitializeGameState

Метод GameMain::InitializeGameState вызывается косвенно через конструктор класса GameMain, который является результатом создания экземпляра GameMain в App::Load.

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) : ...
{
    m_deviceResources->RegisterDeviceNotify(this);
    ...
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    ...
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();
    ...
}

void GameMain::InitializeGameState()
{
    // Set up the initial state machine for handling Game playing state.
    if (m_game->GameActive() && m_game->LevelActive())
    {
        // The last time the game terminated it was in the middle
        // of a level.
        // We are waiting for the user to continue the game.
        ...
    }
    else if (!m_game->GameActive() && (m_game->HighScore().totalHits > 0))
    {
        // The last time the game terminated the game had been completed.
        // Show the high score.
        // We are waiting for the user to acknowledge the high score and start a new game.
        // The level resources for the first level will be loaded later.
        ...
    }
    else
    {
        // This is either the first time the game has run or
        // the last time the game terminated the level was completed.
        // We are waiting for the user to begin the next level.
        ...
    }
    m_uiControl->ShowGameInfoOverlay();
}

Обновление игрового движка

Метод App::Run вызывает GameMain::Run. В GameMain::Run — это базовый конечный автомат для обработки всех основных действий, которые может предпринять пользователь. Самый высокий уровень этого конечного автомата связан с загрузкой игры, игрой на определенном уровне или продолжением уровня после приостановки игры (системой или пользователем).

В примере игры есть 3 основных состояния (представленные перечислением UpdateEngineState ), в которых может находиться игра.

  1. UpdateEngineState::WaitingForResources. Игровой цикл повторяется из-за невозможности перехода до тех пор, пока не появится доступ к ресурсам (в данном случае — к графическим). После завершения задач асинхронной загрузки ресурсов мы обновляем состояние на UpdateEngineState::ResourcesLoaded. Обычно это происходит между уровнями, когда уровень загружает новые ресурсы с диска, с игрового сервера или из облачной серверной части. В примере игры мы имитируем это поведение, так как в этом примере не требуются дополнительные ресурсы для каждого уровня.
  2. UpdateEngineState::WaitingForPress. Игровой цикл повторяется в ожидании конкретного ввода от пользователя. Эти входные данные — это действие игрока для загрузки игры, запуска уровня или продолжения уровня. Пример кода ссылается на эти вложенные состояния с помощью перечисления PressResultState .
  3. UpdateEngineState::D ynamics. Игровой цикл выполняется с участием пользователя. При нахождении пользователя в игровом процессе игра проверяет 3 состояния, в которые она может перейти:
  • GameState::TimeExpired. Истечение срока действия для уровня.
  • GameState::LevelComplete. Завершение уровня игроком.
  • GameState::GameComplete. Прохождение игроком всех уровней.

Игра — это просто конечный автомат, содержащий несколько небольших конечных автоматов. Каждое конкретное состояние должно быть определено очень конкретными критериями. Переходы из одного состояния в другое должны основываться на дискретных пользовательских данных или системных действиях (например, загрузке графических ресурсов).

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

Давайте рассмотрим код для цикла обновления.

Метод GameMain::Update

Это структура конечного автомата, используемая для обновления игрового движка.

void GameMain::Update()
{
    // The controller object has its own update loop.
    m_controller->Update(); 

    switch (m_updateState)
    {
    case UpdateEngineState::WaitingForResources:
        ...
        break;

    case UpdateEngineState::ResourcesLoaded:
        ...
        break;

    case UpdateEngineState::WaitingForPress:
        if (m_controller->IsPressComplete())
        {
            ...
        }
        break;

    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)
            {
            case GameState::TimeExpired:
                ...
                break;

            case GameState::LevelComplete:
                ...
                break;

            case GameState::GameComplete:
                ...
                break;
            }
        }

        if (m_updateState == UpdateEngineState::WaitingForPress)
        {
            // Transitioning state, so enable waiting for the press event.
            m_controller->WaitForPress(
                m_renderer->GameInfoOverlayUpperLeft(),
                m_renderer->GameInfoOverlayLowerRight());
        }
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            // Transitioning state, so shut down the input controller
            // until resources are loaded.
            m_controller->Active(false);
        }
        break;
    }
}

Обновление пользовательского интерфейса

Чтобы состояние могло меняться в зависимости от действий игрока и в соответствии с правилами игры, его необходимо информировать о состоянии системы. Многие игры, включая этот пример игры, обычно используют элементы пользовательского интерфейса для представления этой информации игроку. Пользовательский интерфейс содержит представления состояния игры и другие сведения, относящиеся к игре, такие как оценка, боеприпасы или количество оставшихся шансов. Пользовательский интерфейс также называется наложением, так как он отрисовывается отдельно от графического конвейера main и размещается поверх трехмерной проекции.

Некоторые сведения о пользовательском интерфейсе также представлены в виде hud,чтобы позволить пользователю видеть эту информацию, не отрывая глаз полностью от main игровой области. В примере игры мы создадим это наложение с помощью API Direct2D. Кроме того, можно создать это наложение с помощью XAML, который мы обсудим в разделе Расширение примера игры.

Пользовательский интерфейс имеет два компонента.

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

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

Наложение может быть одним из этих шести экранов в зависимости от состояния игры.

  1. Экран хода загрузки ресурсов в начале игры.
  2. Экран статистики игрового процесса.
  3. Экран начального сообщения уровня.
  4. Игровой экран, когда все уровни завершены без истечения времени.
  5. Игровой экран, когда время истекает.
  6. Экран меню "Приостановка".

Отделение пользовательского интерфейса от графического конвейера игры позволяет работать с ним независимо от обработчика графики игры и значительно снижает сложность кода игры.

Ниже показано, как в примере игры структуру конечного автомата наложения.

void GameMain::SetGameInfoOverlay(GameInfoOverlayState state)
{
    m_gameInfoOverlayState = state;
    switch (state)
    {
    case GameInfoOverlayState::Loading:
        m_uiControl->SetGameLoading(m_loadingCount);
        break;

    case GameInfoOverlayState::GameStats:
        ...
        break;

    case GameInfoOverlayState::LevelStart:
        ...
        break;

    case GameInfoOverlayState::GameOverCompleted:
        ...
        break;

    case GameInfoOverlayState::GameOverExpired:
        ...
        break;

    case GameInfoOverlayState::Pause:
        ...
        break;
    }
}

Обработка событий

Как мы видели в разделе Определение платформы приложений UWP в игре , многие методы поставщика представлений класса App регистрируют обработчики событий. Эти методы должны правильно обрабатывать эти важные события, прежде чем мы добавим механику игры или начнем разработку графики.

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

Это обработчики событий, используемые в этом примере, и события, которые они обрабатывают.

Обработчик событий Описание
OnActivated Обрабатывает CoreApplicationView::Activated. Игровое приложение было выведено на передний план, поэтому активировано главное окно.
OnDpiChanged Обрабатывает Graphics::Display::DisplayInformation::DpiChanged. Разрешение экрана было изменено, и игровое приложение соответствующим образом перераспределяет ресурсы.
Примечание. Координаты CoreWindow находятся в независимых от устройства пикселях (DIP) для Direct2D. В результате необходимо передать в Direct2D уведомление об изменениях разрешения для правильного отображения любых двумерных ресурсов или примитивов.
OnOrientationChanged Обрабатывает Graphics::Display::DisplayInformation::OrientationChanged. Ориентация дисплея изменилась, и графические элементы необходимо обновить.
OnDisplayContentsInvalidated Обрабатывает Graphics::Display::DisplayInformation::DisplayContentsInvalidated. Изображение на экране следует обновить, и ваша игра должна быть повторно отрисована.
OnResuming Обрабатывает CoreApplication::Resuming. Игровое приложение восстанавливает игру из состояния приостановки.
OnSuspending Обрабатывает CoreApplication::Suspending. Игровое приложение сохраняет свое состояние на диск. У него есть 5 секунд, чтобы сохранить состояние в хранилище.
OnVisibilityChanged Обрабатывает CoreWindow::VisibilityChanged. Игровое приложение изменило видимость и стало видимым или невидимым из-за того, что отобразилось другое приложение.
OnWindowActivationChanged Обрабатывает CoreWindow::Activated. Главное окно игрового приложения было деактивировано или активировано, поэтому оно должно удалить фокус и приостановить игру или восстановить фокус. В обоих случаях наложение показывает, что игра приостановлена.
OnWindowClosed Обрабатывает CoreWindow::Closed. Игровое приложение закрывает главное окно и приостанавливает игру.
OnWindowSizeChanged Обрабатывает CoreWindow::SizeChanged. Игровое приложение перераспределяет графические ресурсы и наложение, чтобы подстроиться под изменение размера, а затем обновляет целевой объект прорисовки.

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

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

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