Способы ввода данных в играх

В этом разделе описываются шаблоны и методы эффективного использования устройств ввода в играх универсальная платформа Windows (UWP).

Прочитав этот раздел, вы узнаете:

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

Выбор класса устройств ввода

Существует множество различных API-интерфейсов ввода, таких как ArcadeStick, FlightStick и Gamepad. Как выбрать оптимальный API-интерфейс для своей игры?

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

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

С помощью методов класса ввода FromGameController, таких как Gamepad.FromGameController, можно проверить, насколько тщательно контролируется каждое устройство. Например, если устройство также относится к классу Gamepad, возможно, потребуется изменить пользовательский интерфейс сопоставления кнопок и предоставить игроку выбор из нескольких практичных стандартных сопоставлений. (Обратите внимание на различие: если используется только RawGameController, игроку придется вручную настраивать элементы управления геймпада.)

Кроме того, можно посмотреть код поставщика (VID) и код продукта (PID) в классе RawGameController (с помощью методов HardwareVendorId и HardwareProductId соответственно) и создать рекомендуемые сопоставления кнопок для популярных устройств. При этом игра будет по-прежнему совместима с еще неизвестными будущими устройствами, так как игрок сможет настроить сопоставления вручную.

Отслеживание подключенных контроллеров

Хотя каждый тип контроллера включает список подключенных контроллеров (например, Gamepad.Gamepads), желательно вести свой собственный список контроллеров. Подробнее см. в разделе Список геймпадов (в статье о каждом типе контроллеров есть раздел с аналогичным названием).

Однако что произойдет, если пользователь отключит свой контроллер или подключит новый? Эти события необходимо обрабатывать и соответствующим образом обновлять список. Подробнее см. в разделе Добавление и удаление геймпадов (снова-таки, в статье о каждом типе контроллеров есть раздел с аналогичным названием).

Поскольку события добавления и удаления вызываются асинхронно, при работе со списком контроллеров есть возможность получить неверные результаты. Поэтому всякий раз, когда вы обращаетесь к своему списку контроллеров, вы должны установить для него блокировку, чтобы к нему одновременно мог обращаться только один поток. Это можно сделать с помощью параллельной среды выполнения, а именно класса critical_section, который находится в <ppl.h>.

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

Чтобы это исправить, нужно предусмотреть метод, который "обновляет" главный геймпад (в игре с одним игроком; многопользовательские игры требуют более сложных решений). Этот метод затем следует вызывать в обработчиках событий добавления и удаления контроллеров или же в методе обновления.

Следующий метод просто возвращает первый геймпад в списке (или nullptr, если список пустой). Затем просто нужно не забывать делать проверку на предмет nullptr всякий раз, когда вы делаете что-либо с этим контроллером. Блокировать ли игровой процесс при отсутствии подключенного контроллера (например, приостанавливать игру) или оставлять его продолжаться без ввода — решать вам.

#include <ppl.h>

using namespace Platform::Collections;
using namespace Windows::Gaming::Input;
using namespace concurrency;

Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();

Gamepad^ GetFirstGamepad()
{
    Gamepad^ gamepad = nullptr;
    critical_section::scoped_lock{ m_lock };

    if (m_myGamepads->Size > 0)
    {
        gamepad = m_myGamepads->GetAt(0);
    }

    return gamepad;
}

Чтобы продемонстрировать все это вместе, вот пример обработки ввода с помощью геймпада:

#include <algorithm>
#include <ppl.h>

using namespace Platform::Collections;
using namespace Windows::Foundation;
using namespace Windows::Gaming::Input;
using namespace concurrency;

static Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();
static Gamepad^          m_gamepad = nullptr;
static critical_section  m_lock{};

void Start()
{
    // Register for gamepad added and removed events.
    Gamepad::GamepadAdded += ref new EventHandler<Gamepad^>(&OnGamepadAdded);
    Gamepad::GamepadRemoved += ref new EventHandler<Gamepad^>(&OnGamepadRemoved);

    // Add connected gamepads to m_myGamepads.
    for (auto gamepad : Gamepad::Gamepads)
    {
        OnGamepadAdded(nullptr, gamepad);
    }
}

void Update()
{
    // Update the current gamepad if necessary.
    if (m_gamepad == nullptr)
    {
        auto gamepad = GetFirstGamepad();

        if (m_gamepad != gamepad)
        {
            m_gamepad = gamepad;
        }
    }

    if (m_gamepad != nullptr)
    {
        // Gather gamepad reading.
    }
}

// Get the first gamepad in the list.
Gamepad^ GetFirstGamepad()
{
    Gamepad^ gamepad = nullptr;
    critical_section::scoped_lock{ m_lock };

    if (m_myGamepads->Size > 0)
    {
        gamepad = m_myGamepads->GetAt(0);
    }

    return gamepad;
}

void OnGamepadAdded(Platform::Object^ sender, Gamepad^ args)
{
    // Check if the just-added gamepad is already in m_myGamepads; if it isn't, 
    // add it.
    critical_section::scoped_lock lock{ m_lock };
    auto it = std::find(begin(m_myGamepads), end(m_myGamepads), args);

    if (it == end(m_myGamepads))
    {
        m_myGamepads->Append(args);
    }
}

void OnGamepadRemoved(Platform::Object^ sender, Gamepad^ args)
{
    // Remove the gamepad that was just disconnected from m_myGamepads.
    unsigned int indexRemoved;
    critical_section::scoped_lock lock{ m_lock };

    if (m_myGamepads->IndexOf(args, &indexRemoved))
    {
        if (m_gamepad == m_myGamepads->GetAt(indexRemoved))
        {
            m_gamepad = nullptr;
        }

        m_myGamepads->RemoveAt(indexRemoved);
    }
}

Отслеживание пользователей и их устройств

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

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

В связи с этим необходимо отслеживать вводимые пользователем данные и сопоставлять их с помощью свойства User в классе устройств (это свойство наследуется у интерфейса IGameController).

Пример приложения UserGamepadPairingUWP на GitHub демонстрирует, как отслеживать пользователей и устройства, которые они используют.

Определение положений кнопки

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

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

Gamepad gamepad;
GamepadReading newReading();
GamepadReading oldReading();

// Called at the start of the game.
void Game::Start()
{
    gamepad = Gamepad::Gamepads[0];
}

// Game::Loop represents one iteration of a typical game loop
void Game::Loop()
{
    // move previous newReading into oldReading before getting next newReading
    oldReading = newReading, newReading = gamepad.GetCurrentReading();

    // process device readings using buttonJustPressed/buttonJustReleased (see below)
}

Сначала Game::Loop перемещает существующее значение newReading (данные ввода, считанные с геймпада во время предыдущей итерации цикла) в oldReading, а затем заполняет newReading новым считанным с геймпада значением для текущей итерации. На основании полученной информации вы можете определять положения кнопок.

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

bool ButtonJustPressed(const GamepadButtons selection)
{
    bool newSelectionPressed = (selection == (newReading.Buttons & selection));
    bool oldSelectionPressed = (selection == (oldReading.Buttons & selection));

    return newSelectionPressed && !oldSelectionPressed;
}

bool ButtonJustReleased(GamepadButtons selection)
{
    bool newSelectionReleased =
        (GamepadButtons.None == (newReading.Buttons & selection));

    bool oldSelectionReleased =
        (GamepadButtons.None == (oldReading.Buttons & selection));

    return newSelectionReleased && !oldSelectionReleased;
}

Эти две функции сначала наследуют логическое состояние выбранной кнопки из newReading и oldReading, а затем выполняют бинарную логику, чтобы определить, произошло ли целевое изменение состояния кнопки. Эти функции возвращают значение true, только если новые считанные данные содержат целевое состояние (кнопка нажата или отпущена соответственно) и ранее считанные данные не содержат целевое состояние; в противном случае они возвращают значение false.

Определение сложных схем положений кнопок

Каждая кнопка на устройстве ввода предоставляет цифровые данные, указывающие на ее состояние: нажата (down) или отпущена (up). В целях обеспечения эффективности эти показания кнопок не указываются в виде отдельных логических значений. Вместо этого все они упаковываются в битовые поля, представляемые соответствующими перечислениями, например GamepadButtons (в зависимости от устройства). Для считывания данных с конкретных кнопок используется побитовая маскировка, позволяющая изолировать нужные значения. Кнопка нажата, когда установлен соответствующий бит (состояние down); в противном случае кнопка отпущена (состояние up).

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

GamepadReading reading = gamepad.GetCurrentReading();

// Determines whether gamepad button A is pressed.
if (GamepadButtons::A == (reading.Buttons & GamepadButtons::A))
{
    // The A button is pressed.
}

// Determines whether gamepad button A is released.
if (GamepadButtons::None == (reading.Buttons & GamepadButtons::A))
{
    // The A button is released (not pressed).
}

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

В следующем примере показано, как определить, нажаты ли обе кнопки геймпада "A" и "B":

if ((GamepadButtons::A | GamepadButtons::B) == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
    // The A and B buttons are both pressed.
}

В следующем примере показано, как определить, отпущены ли обе кнопки геймпада "A" и "B":

if ((GamepadButtons::None == (reading.Buttons & GamepadButtons::A | GamepadButtons::B))
{
    // The A and B buttons are both released (not pressed).
}

В следующем примере показано, как определить, нажата ли кнопка геймпада "A" одновременно с отпущенной кнопкой "B":

if (GamepadButtons::A == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
    // The A button is pressed and the B button is released (B is not pressed).
}

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

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

auto buttonArrangement = GamepadButtons::A;
auto buttonSelection = (reading.Buttons & (GamepadButtons::A | GamepadButtons::B));

if (buttonArrangement == buttonSelection)
{
    // The A button is pressed and the B button is released (B is not pressed).
}

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

Получение состояния аккумулятора

Для любого игрового устройства управления, реализующего интерфейс IGameControllerBatteryInfo, можно вызвать метод TryGetBatteryReport в экземпляре устройства управления, чтобы получить объект BatteryReport, который предоставляет информацию об аккумуляторе в устройстве. Можно получить такие свойства, как скорость зарядки (ChargeRateInMilliwatts), предполагаемую энергоемкость нового аккумулятора (DesignCapacityInMilliwattHours) и энергоемкость текущего аккумулятора при полной зарядке (FullChargeCapacityInMilliwattHours).

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

  • ChargeRateInMilliwatts и DesignCapacityInMilliwattHours всегда будут иметь значение NULL.

  • Вы можете получить процент батареи, вычислив RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours. Значения этих свойств следует игнорировать и использовать только вычисленный процент.

  • Процент в предыдущем пункте всегда будет иметь одно из следующих значений:

    • 100% (полностью заряжено)
    • 70% (средний уровень)
    • 40% (низкий уровень)
    • 10% (критический уровень)

Если ваш код выполняет какое-либо действие (например, рисует пользовательский интерфейс) на основании оставшегося уровня заряда аккумулятора, следите за тем, чтобы он соответствовал значениям выше. Например, если вы хотите предупреждать игрока о низком заряде аккумулятора в контроллере, делайте это тогда, когда заряд опустится до 10%.

См. также раздел