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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

См. также