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

Примечание.

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

Теперь, когда наша игра имеет свои трехмерные визуальные элементы, пришло время сосредоточиться на добавлении некоторых 2D-элементов, чтобы игра может предоставить отзыв о состоянии игры игроку. Это можно сделать, добавив простые параметры меню и компоненты отображения головы на вершине выходных данных графического конвейера 3-D.

Примечание.

Если вы не скачали последний игровой код для этого примера, перейдите в пример игры Direct3D. Этот пример является частью большой коллекции примеров функций UWP. Инструкции по скачиванию примера см. в разделе "Примеры приложений для разработки Windows".

Цель

С помощью Direct2D добавьте в игру UWP DirectX ряд графиков и поведения пользовательского интерфейса, включая:

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

Хотя в игре DirectX существует множество способов отображения элементов текста и пользовательского интерфейса, мы сосредоточимся на использовании Direct2D. Мы также будем использовать DirectWrite для текстовых элементов.

Direct2D — это набор интерфейсов API для рисования на основе пикселей, используемых для рисования примитивов и эффектов на основе пикселей. При запуске с Direct2D лучше всего держать вещи простыми. Сложные макеты и поведение интерфейса требуют времени и планирования. Если для игры требуется сложный пользовательский интерфейс, например те, которые находятся в играх имитации и стратегии, рассмотрите возможность использования XAML.

Примечание.

Сведения о разработке пользовательского интерфейса с помощью XAML в игре UWP DirectX см. в разделе "Расширение примера игры".

Direct2D специально не предназначен для пользовательских интерфейсов или макетов, таких как HTML и XAML. Он не предоставляет такие компоненты пользовательского интерфейса, как списки, поля или кнопки. Кроме того, он не предоставляет такие компоненты макета, как div, таблицы или сетки.

Для этой примера игры у нас есть два основных компонента пользовательского интерфейса.

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

Использование Direct2D для отображения головных элементов

На следующем рисунке показан экран головных голов в игре для примера. Это просто и безумно, что позволяет игроку сосредоточиться на навигации по 3D-миру и стрельбе целевых объектов. Хороший интерфейс или отображение головы никогда не должно усложнять способность игрока обрабатывать и реагировать на события в игре.

Снимок экрана: наложение игры

Наложение состоит из следующих основных примитивов.

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

Состояние наложения в игре отображается в методе GameHud::Render класса GameHud. В этом методе наложение Direct2D, представляющее наш пользовательский интерфейс, обновляется, чтобы отразить изменения в количестве попаданий, оставшемся времени и номере уровня.

Если игра инициализирована, добавьте TotalHits()и TimeRemaining()TotalShots()в буфер swprintf_s и укажите формат печати. Затем мы можем нарисовать его с помощью метода DrawText . Мы делаем то же самое для текущего индикатора уровня, рисуя пустые числа для отображения незавершенных уровней, таких как ➀, и заполненных чисел, таких как ➊, чтобы показать, что конкретный уровень завершен.

В следующем фрагменте кода описывается процесс метода GameHud::Render для

  • Создание растрового изображения с помощью **ID2D1RenderTarget::D rawBitmap **
  • Разделение областей пользовательского интерфейса на прямоугольники с помощью D2D1::RectF
  • Использование DrawText для создания текстовых элементов
void GameHud::Render(_In_ std::shared_ptr<Simple3DGame> const& game)
{
    auto d2dContext = m_deviceResources->GetD2DDeviceContext();
    auto windowBounds = m_deviceResources->GetLogicalSize();

    if (m_showTitle)
    {
        d2dContext->DrawBitmap(
            m_logoBitmap.get(),
            D2D1::RectF(
                GameUIConstants::Margin,
                GameUIConstants::Margin,
                m_logoSize.width + GameUIConstants::Margin,
                m_logoSize.height + GameUIConstants::Margin
                )
            );
        d2dContext->DrawTextLayout(
            Point2F(m_logoSize.width + 2.0f * GameUIConstants::Margin, GameUIConstants::Margin),
            m_titleHeaderLayout.get(),
            m_textBrush.get()
            );
        d2dContext->DrawTextLayout(
            Point2F(GameUIConstants::Margin, m_titleBodyVerticalOffset),
            m_titleBodyLayout.get(),
            m_textBrush.get()
            );
    }

    // Draw text for number of hits, total shots, and time remaining
    if (game != nullptr)
    {
        // This section is only used after the game state has been initialized.
        static const int bufferLength = 256;
        static wchar_t wsbuffer[bufferLength];
        int length = swprintf_s(
            wsbuffer,
            bufferLength,
            L"Hits:\t%10d\nShots:\t%10d\nTime:\t%8.1f",
            game->TotalHits(),
            game->TotalShots(),
            game->TimeRemaining()
            );

        // Draw the upper right portion of the HUD displaying total hits, shots, and time remaining
        d2dContext->DrawText(
            wsbuffer,
            length,
            m_textFormatBody.get(),
            D2D1::RectF(
                windowBounds.Width - GameUIConstants::HudRightOffset,
                GameUIConstants::HudTopOffset,
                windowBounds.Width,
                GameUIConstants::HudTopOffset + (GameUIConstants::HudBodyPointSize + GameUIConstants::Margin) * 3
                ),
            m_textBrush.get()
            );

        // Using the unicode characters starting at 0x2780 ( ➀ ) for the consecutive levels of the game.
        // For completed levels start with 0x278A ( ➊ ) (This is 0x2780 + 10).
        uint32_t levelCharacter[6];
        for (uint32_t i = 0; i < 6; i++)
        {
            levelCharacter[i] = 0x2780 + i + ((static_cast<uint32_t>(game->LevelCompleted()) == i) ? 10 : 0);
        }
        length = swprintf_s(
            wsbuffer,
            bufferLength,
            L"%lc %lc %lc %lc %lc %lc",
            levelCharacter[0],
            levelCharacter[1],
            levelCharacter[2],
            levelCharacter[3],
            levelCharacter[4],
            levelCharacter[5]
            );
        // Create a new rectangle and draw the current level info text inside
        d2dContext->DrawText(
            wsbuffer,
            length,
            m_textFormatBodySymbol.get(),
            D2D1::RectF(
                windowBounds.Width - GameUIConstants::HudRightOffset,
                GameUIConstants::HudTopOffset + (GameUIConstants::HudBodyPointSize + GameUIConstants::Margin) * 3 + GameUIConstants::Margin,
                windowBounds.Width,
                GameUIConstants::HudTopOffset + (GameUIConstants::HudBodyPointSize + GameUIConstants::Margin) * 4
                ),
            m_textBrush.get()
            );

        if (game->IsActivePlay())
        {
            // Draw the move and fire rectangles
            ...
            // Draw the crosshairs
            ...
        }
    }
}

Разбив метод дальше, этот элемент метода GameHud::Render рисует наши прямоугольники перемещения и пожара с идентификатором ID2D1RenderTarget::D rawRectangle и перекрестивает два вызова ID2D1RenderTarget::D rawLine.

// Check if game is playing
if (game->IsActivePlay())
{
    // Draw a rectangle for the touch input for the move control.
    d2dContext->DrawRectangle(
        D2D1::RectF(
            0.0f,
            windowBounds.Height - GameUIConstants::TouchRectangleSize,
            GameUIConstants::TouchRectangleSize,
            windowBounds.Height
            ),
        m_textBrush.get()
        );
    // Draw a rectangle for the touch input for the fire control.
    d2dContext->DrawRectangle(
        D2D1::RectF(
            windowBounds.Width - GameUIConstants::TouchRectangleSize,
            windowBounds.Height - GameUIConstants::TouchRectangleSize,
            windowBounds.Width,
            windowBounds.Height
            ),
        m_textBrush.get()
        );

    // Draw the cross hairs
    d2dContext->DrawLine(
        D2D1::Point2F(windowBounds.Width / 2.0f - GameUIConstants::CrossHairHalfSize,
            windowBounds.Height / 2.0f),
        D2D1::Point2F(windowBounds.Width / 2.0f + GameUIConstants::CrossHairHalfSize,
            windowBounds.Height / 2.0f),
        m_textBrush.get(),
        3.0f
        );
    d2dContext->DrawLine(
        D2D1::Point2F(windowBounds.Width / 2.0f, windowBounds.Height / 2.0f -
            GameUIConstants::CrossHairHalfSize),
        D2D1::Point2F(windowBounds.Width / 2.0f, windowBounds.Height / 2.0f +
            GameUIConstants::CrossHairHalfSize),
        m_textBrush.get(),
        3.0f
        );
}

В методе GameHud::Render мы храним логический размер окна игры в переменной windowBounds . Для этого используется GetLogicalSize метод класса DeviceResources .

auto windowBounds = m_deviceResources->GetLogicalSize();

Получение размера окна игры важно для программирования пользовательского интерфейса. Размер окна определяется в измерении под названием DIPs (независимые от устройства пиксели), где DIP определяется как 1/96 дюйма. Direct2D масштабирует единицы рисования до фактических пикселей при возникновении рисунка, используя параметры точек Windows на дюйм (DPI). Аналогичным образом при рисовании текста с помощью DirectWrite вы указываете dips, а не точки для размера шрифта. DiPs выражается как числа с плавающей запятой. 

Отображение сведений о состоянии игры

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

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

состояние и действие наложения

Наложение разбито на два раздела: состояние и действие. Раздел "Состояние " также разбивается на прямоугольники "Заголовок " и "Текст ". В разделе "Действие " только один прямоугольник. Каждый прямоугольник имеет другое назначение.

  • titleRectangle содержит текст заголовка.
  • bodyRectangle содержит текст текста.
  • actionRectangle содержит текст, который сообщает проигрывателю выполнить определенное действие.

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

  • Загрузка
  • Начальная статистика начального и высокого уровня оценки
  • Запуск уровня
  • Игра приостановлена
  • Игра закончена
  • Игра выиграла

Часть действия обновляется с помощью метода GameInfoOverlay::SetAction, что позволяет задать текст действия одним из следующих элементов.

  • "Коснитесь, чтобы снова играть..."
  • "Загрузка уровня, подождите ..."
  • "Коснитесь, чтобы продолжить..."
  • нет

Примечание.

Оба этих метода будут рассмотрены далее в разделе "Представление состояния игры".

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

Инициализация и рисование наложения

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

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

GameInfoOverlay::GameInfoOverlay

Конструктор GameInfoOverlay::GameInfoOverlay инициализирует наложение, сохраняя область растрового изображения, которую мы будем использовать для отображения сведений для игрока. Конструктор получает фабрику из объекта ID2D1Device, переданного ему, который он использует для создания объекта ID2D1DeviceContext, к которому сам объект наложения может нарисовать. IDWriteFactory::CreateTextFormat

GameInfoOverlay::CreateDeviceDependentResources

GameInfoOverlay::CreateDeviceDependentResources — это наш метод для создания кистей, которые будут использоваться для рисования текста. Для этого мы получаем объект ID2D1DeviceContext2 , который обеспечивает создание и рисование геометрии, а также функциональные возможности, такие как отрисовка рукописных и градиентных сетк. Затем мы создадим ряд цветных кистей с помощью ID2D1SolidColorBrush для рисования следующих элементов пользовательского интерфейса.

  • Черная кисть для прямоугольника фона
  • Белая кисть для текста состояния
  • Оранжевая кисть для текста действия

DeviceResources::SetDpi

Метод DeviceResources::SetDpi задает точки на дюйм окна. Этот метод вызывается при изменении DPI и должен быть изменен, что происходит при изменении размера окна игры. После обновления DPI этот метод также вызываетDeviceResources::CreateWindowSizeDependentResources , чтобы убедиться, что необходимые ресурсы повторно создаются при изменении размера окна.

GameInfoOverlay::CreateWindowsSizeDependentResources

Метод GameInfoOverlay::CreateWindowsSizeDependentResources заключается в том, что происходит все наше рисование. Ниже приведена структура шагов метода.

  • Три прямоугольника создаются для раздела текста пользовательского интерфейса для текста "Заголовок", "Текст" и "Действие ".

    m_titleRectangle = D2D1::RectF(
        GameInfoOverlayConstant::SideMargin,
        GameInfoOverlayConstant::TopMargin,
        overlaySize.width - GameInfoOverlayConstant::SideMargin,
        GameInfoOverlayConstant::TopMargin + GameInfoOverlayConstant::TitleHeight
        );
    m_actionRectangle = D2D1::RectF(
        GameInfoOverlayConstant::SideMargin,
        overlaySize.height - (GameInfoOverlayConstant::ActionHeight + GameInfoOverlayConstant::BottomMargin),
        overlaySize.width - GameInfoOverlayConstant::SideMargin,
        overlaySize.height - GameInfoOverlayConstant::BottomMargin
        );
    m_bodyRectangle = D2D1::RectF(
        GameInfoOverlayConstant::SideMargin,
        m_titleRectangle.bottom + GameInfoOverlayConstant::Separator,
        overlaySize.width - GameInfoOverlayConstant::SideMargin,
        m_actionRectangle.top - GameInfoOverlayConstant::Separator
        );
    
  • Битовая карта создается с учетом m_levelBitmapтекущего DPI с помощью CreateBitmap.

  • m_levelBitmapустанавливается как целевой объект отрисовки 2D с помощью ID2D1DeviceContext::SetTarget.

  • Растровое изображение очищается с каждым пикселем, сделанным черным с помощью ID2D1RenderTarget::Clear.

  • Идентификатор 2D1RenderTarget::BeginDraw вызывается для запуска рисования.

  • DrawText вызывается для рисования текста, хранящегося в m_titleString, m_bodyStringи m_actionString в соответствующем прямоугольнике с помощью соответствующего идентификатора ID2D1SolidColorBrush.

  • Идентификатор 2D1RenderTarget::EndDraw вызывается для остановки всех операций рисования в m_levelBitmap.

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

  • Повторите процесс для рисования m_levelBitmapm_tooSmallBitmapдля , на этот раз только рисование строки Paused в тексте.

Теперь все, что нам нужно, шесть методов для заполнения текста наших шести состояний наложения!

Представление состояния игры

Каждый из шести состояний наложения в игре имеет соответствующий метод в объекте GameInfoOverlay . Эти методы рисуют вариант наложения для передачи явных сведений игроку о самой игре. Эта связь представлена строкой Title и Body . Так как пример уже настроил ресурсы и макет для этой информации при инициализации и с помощью метода GameInfoOverlay::CreateDeviceDependentResources , он должен предоставлять только строки, относящиеся к наложению.

Часть состояния наложения устанавливается с вызовом одного из следующих методов.

Состояние игры Метод набора состояний Поля состояния
Загрузка GameInfoOverlay::SetGameLoading Заголовок
загрузки ресурсов
текст
добавочно печатает "." для обозначения действия загрузки.
Начальная статистика начального и высокого уровня оценки GameInfoOverlay::SetGameStats Заголовок
высокий уровень
тела
завершен #
Общее количество очков #
Общее количество выстрелов #
Запуск уровня GameInfoOverlay::SetLevelStart Описание цели уровня заголовка
#
Body
Level.
Игра приостановлена GameInfoOverlay::SetPause Названиеигры приостановленное
тело
нет
Игра закончена GameInfoOverlay::SetGameOver Название
игры над
уровнями тела
завершено #
Total Points #
Total Shots #
Levels Completed #
High Score #
Игра выиграла GameInfoOverlay::SetGameOver Заголовок
вы выиграли!
Уровни тела
завершены #
Общее количество очков #
Общее количество выстрелов #
Уровни завершены #
Высокая оценка #

С помощью метода GameInfoOverlay::CreateWindowSizeDependentResources образец объявил три прямоугольные области, соответствующие определенным регионам наложения.

Учитывая эти области, давайте рассмотрим один из методов, относящихся к состоянию, GameInfoOverlay::SetGameStats, и посмотрим, как нарисовывается наложение.

void GameInfoOverlay::SetGameStats(int maxLevel, int hitCount, int shotCount)
{
    int length;

    auto d2dContext = m_deviceResources->GetD2DDeviceContext();

    d2dContext->SetTarget(m_levelBitmap.get());
    d2dContext->BeginDraw();
    d2dContext->SetTransform(D2D1::Matrix3x2F::Identity());
    d2dContext->FillRectangle(&m_titleRectangle, m_backgroundBrush.get());
    d2dContext->FillRectangle(&m_bodyRectangle, m_backgroundBrush.get());
    m_titleString = L"High Score";

    d2dContext->DrawText(
        m_titleString.c_str(),
        m_titleString.size(),
        m_textFormatTitle.get(),
        m_titleRectangle,
        m_textBrush.get()
        );
    length = swprintf_s(
        wsbuffer,
        bufferLength,
        L"Levels Completed %d\nTotal Points %d\nTotal Shots %d",
        maxLevel,
        hitCount,
        shotCount
        );
    m_bodyString = std::wstring(wsbuffer, length);
    d2dContext->DrawText(
        m_bodyString.c_str(),
        m_bodyString.size(),
        m_textFormatBody.get(),
        m_bodyRectangle,
        m_textBrush.get()
        );

    // We ignore D2DERR_RECREATE_TARGET here. This error indicates that the device
    // is lost. It will be handled during the next call to Present.
    HRESULT hr = d2dContext->EndDraw();
    if (hr != D2DERR_RECREATE_TARGET)
    {
        // The D2DERR_RECREATE_TARGET indicates there has been a problem with the underlying
        // D3D device. All subsequent rendering will be ignored until the device is recreated.
        // This error will be propagated and the appropriate D3D error will be returned from the
        // swapchain->Present(...) call. At that point, the sample will recreate the device
        // and all associated resources. As a result, the D2DERR_RECREATE_TARGET doesn't
        // need to be handled here.
        winrt::check_hresult(hr);
    }
}

Используя контекст устройства Direct2D, инициализированный объектом GameInfoOverlay , этот метод заполняет прямоугольники заголовка и тела черным с помощью фоновой кисти. Он рисует текст для строки "Высокая оценка" в прямоугольник заголовка и строку, содержащую сведения о состоянии игры в прямоугольник тела с помощью кисти белого текста.

Прямоугольник действия обновляется последующим вызовом GameInfoOverlay::SetAction из метода объекта GameMain, который предоставляет сведения о состоянии игры, необходимые GameInfoOverlay::SetAction, чтобы определить правильное сообщение для игрока, например "Коснитесь, чтобы продолжить".

Наложение для любого заданного состояния выбирается в методе GameMain::SetGameInfoOverlay следующим образом:

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

    case GameInfoOverlayState::GameStats:
        m_uiControl->SetGameStats(
            m_game->HighScore().levelCompleted + 1,
            m_game->HighScore().totalHits,
            m_game->HighScore().totalShots
            );
        break;

    case GameInfoOverlayState::LevelStart:
        m_uiControl->SetLevelStart(
            m_game->LevelCompleted() + 1,
            m_game->CurrentLevel()->Objective(),
            m_game->CurrentLevel()->TimeLimit(),
            m_game->BonusTime()
            );
        break;

    case GameInfoOverlayState::GameOverCompleted:
        m_uiControl->SetGameOver(
            true,
            m_game->LevelCompleted() + 1,
            m_game->TotalHits(),
            m_game->TotalShots(),
            m_game->HighScore().totalHits
            );
        break;

    case GameInfoOverlayState::GameOverExpired:
        m_uiControl->SetGameOver(
            false,
            m_game->LevelCompleted(),
            m_game->TotalHits(),
            m_game->TotalShots(),
            m_game->HighScore().totalHits
            );
        break;

    case GameInfoOverlayState::Pause:
        m_uiControl->SetPause(
            m_game->LevelCompleted() + 1,
            m_game->TotalHits(),
            m_game->TotalShots(),
            m_game->TimeRemaining()
            );
        break;
    }
}

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

Следующие шаги

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