Поделиться через


Методы ввода для игр

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

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

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

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

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

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

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

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

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

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

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

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

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

Еще одна вещь, о том, что список подключенных контроллеров изначально будет пустым, и принимает второй или два для заполнения. Таким образом, если вы назначаете только текущий геймпад в методе start, он будет иметь значение 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);
    }
}

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

Все устройства ввода связаны с пользователем, чтобы их удостоверение можно было связать с своим игровым процессом, достижениями, изменениями параметров и другими действиями. Пользователи могут войти в систему или выйти из системы, и это обычно для другого пользователя для входа на входное устройство, которое остается подключенным к системе после выхода предыдущего пользователя. При входе или выходе пользователя возникает событие 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.

Обнаружение сложных договоренностей кнопок

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

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

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).
}

В следующем примере определяется, нажимается ли кнопка "Геймпад" во время выпуска кнопки 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.

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

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

    • 100 % (полный)
    • 70 % (средний)
    • 40 % (низкий)
    • 10 % (критический)

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

См. также