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


Геймпад и вибрация

На этой странице описываются основы программирования для игровых панелей с помощью [Windows.Gaming.Input.Gamepad][игровой панели] и связанных API-интерфейсов для универсальная платформа Windows (UWP).

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

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

Обзор геймпадов

Игровые панели, такие как беспроводной контроллер Xbox и беспроводной контроллер Xbox S, являются устройствами ввода общего назначения. Это стандартное устройство ввода в Xbox One и распространенный выбор для игроков Windows, когда они не предпочитают клавиатуру и мышь. Игровые панели поддерживаются в приложениях Windows 10 или Windows 11 и Xbox UWP через пространство имен Windows.Gaming.Input .

Геймпады Xbox One оснащены боковой площадкой (или D-pad); Кнопки A, B, X, Y, View и Menu; слева и справа от пальца, бамперов и триггеров; и в общей сложности четыре двигателя вибрации. Оба отпечатка обеспечивают двойное аналоговое чтение в осях X и Y, а также выступает в качестве кнопки при нажатии внутрь. Каждый триггер предоставляет аналоговое чтение, представляющее, насколько далеко оно оттягивается.

Примечание.

Windows.Gaming.Input.Gamepad также поддерживает геймпады Xbox 360, которые имеют тот же макет управления, что и стандартные игровые панели Xbox One.

Вибрация и импульсные триггеры

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

Примечание.

Геймпады Xbox 360 не оснащены триггерами импульса.

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

Мертвая зона отпечатка

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

Более крупные взаимоблокировки предлагают простую стратегию разделения намеренного ввода от непреднамеренных входных данных.

Дополнительные сведения см. в статье "Чтение отпечатков".

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

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

В качестве контроллера навигации пользовательского интерфейса геймпады сопоставляют необходимый набор команд навигации с левой кнопкой мыши, D-pad, View, Menu, A и B .

Команда навигации Входные данные геймпада
Up Левый палец вверх / D-pad up
Down Левый палец вниз / D-pad down
Left Левый палец слева / D-pad слева
Right Левый палец справа / D-pad right
Представления Кнопка "Просмотреть"
Меню Кнопка меню
Accept Кнопка
Отменить Кнопка B

Кроме того, геймпады сопоставляют все необязательные наборы команд навигации с оставшимися входными данными.

Команда навигации Входные данные геймпада
Page Up Левый триггер
Page Down Правый триггер
Страница слева Левый бампер
Страница справа Правый бампер
Прокрутка вверх Правый палец вверх
Прокрутите вниз Правый палец вниз
Прокрутка влево Правый палец слева
Прокрутка вправо Правый палец справа
Контекст 1 Кнопка X
Контекст 2 Кнопка Y
Контекст 3 Нажатие левого отпечатка
Контекст 4 Нажатие правого отпечатка

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

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

Список геймпадов

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

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

auto myGamepads = ref new Vector<Gamepad^>();
critical_section myLock{};

for (auto gamepad : Gamepad::Gamepads)
{
    // Check if the gamepad is already in myGamepads; if it isn't, add it.
    critical_section::scoped_lock lock{ myLock };
    auto it = std::find(begin(myGamepads), end(myGamepads), gamepad);

    if (it == end(myGamepads))
    {
        // This code assumes that you're interested in all gamepads.
        myGamepads->Append(gamepad);
    }
}
private readonly object myLock = new object();
private List<Gamepad> myGamepads = new List<Gamepad>();
private Gamepad mainGamepad;

private void GetGamepads()
{
    lock (myLock)
    {
        foreach (var gamepad in Gamepad.Gamepads)
        {
            // Check if the gamepad is already in myGamepads; if it isn't, add it.
            bool gamepadInList = myGamepads.Contains(gamepad);

            if (!gamepadInList)
            {
                // This code assumes that you're interested in all gamepads.
                myGamepads.Add(gamepad);
            }
        }
    }   
}

Добавление и удаление геймпадов

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

В следующем примере начинается отслеживание добавленной игровой панели.

Gamepad::GamepadAdded += ref new EventHandler<Gamepad^>(Platform::Object^, Gamepad^ args)
{
    // Check if the just-added gamepad is already in myGamepads; if it isn't, add
    // it.
    critical_section::scoped_lock lock{ myLock };
    auto it = std::find(begin(myGamepads), end(myGamepads), args);

    if (it == end(myGamepads))
    {
        // This code assumes that you're interested in all new gamepads.
        myGamepads->Append(args);
    }
}
Gamepad.GamepadAdded += (object sender, Gamepad e) =>
{
    // Check if the just-added gamepad is already in myGamepads; if it isn't, add
    // it.
    lock (myLock)
    {
        bool gamepadInList = myGamepads.Contains(e);

        if (!gamepadInList)
        {
            myGamepads.Add(e);
        }
    }
};

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

Gamepad::GamepadRemoved += ref new EventHandler<Gamepad^>(Platform::Object^, Gamepad^ args)
{
    unsigned int indexRemoved;
    critical_section::scoped_lock lock{ myLock };

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

        myGamepads->RemoveAt(indexRemoved);
    }
}
Gamepad.GamepadRemoved += (object sender, Gamepad e) =>
{
    lock (myLock)
    {
        int indexRemoved = myGamepads.IndexOf(e);

        if (indexRemoved > -1)
        {
            if (mainGamepad == myGamepads[indexRemoved])
            {
                mainGamepad = null;
            }

            myGamepads.RemoveAt(indexRemoved);
        }
    }
};

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

Пользователи и гарнитуры

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

Чтение геймпадов

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

Опрос геймпада

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

Вы опрашиваете геймпад, вызвав GetCurrentReading. Эта функция возвращает GamepadReading, которая содержит состояние игровой панели.

В следующем примере опрашивает геймпад для текущего состояния.

auto gamepad = myGamepads[0];

GamepadReading reading = gamepad->GetCurrentReading();
Gamepad gamepad = myGamepads[0];

GamepadReading reading = gamepad.GetCurrentReading();

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

Чтение отпечатков

Каждый палец обеспечивает аналоговое чтение между -1,0 и +1,0 в осях X и Y. В оси X значение -1,0 соответствует левой позиции отпечатка; Значение +1.0 соответствует правой позиции. В оси Y значение -1,0 соответствует нижней позиции наибольшего отпечатка; Значение +1.0 соответствует верхней позиции. В обоих осях значение составляет примерно 0,0, если палка находится в центре, но это нормально для точного значения, даже между последующими считываниями; Стратегии устранения этого варианта рассматриваются далее в этом разделе.

Значение оси X левого отпечатка считывается из LeftThumbstickX свойства структуры GamepadReading; значение оси Y считывается из LeftThumbstickY свойства. Значение оси X правого отпечатка считывается из RightThumbstickX свойства. Значение оси Y считывается из RightThumbstickY свойства.

float leftStickX = reading.LeftThumbstickX;   // returns a value between -1.0 and +1.0
float leftStickY = reading.LeftThumbstickY;   // returns a value between -1.0 and +1.0
float rightStickX = reading.RightThumbstickX; // returns a value between -1.0 and +1.0
float rightStickY = reading.RightThumbstickY; // returns a value between -1.0 and +1.0
double leftStickX = reading.LeftThumbstickX;   // returns a value between -1.0 and +1.0
double leftStickY = reading.LeftThumbstickY;   // returns a value between -1.0 and +1.0
double rightStickX = reading.RightThumbstickX; // returns a value between -1.0 and +1.0
double rightStickY = reading.RightThumbstickY; // returns a value between -1.0 and +1.0

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

В следующем примере показана базовая радиальная мертвая зона с помощью теоремы Pythagorean.

float leftStickX = reading.LeftThumbstickX;   // returns a value between -1.0 and +1.0
float leftStickY = reading.LeftThumbstickY;   // returns a value between -1.0 and +1.0

// choose a deadzone -- readings inside this radius are ignored.
const float deadzoneRadius = 0.1;
const float deadzoneSquared = deadzoneRadius * deadzoneRadius;

// Pythagorean theorem -- for a right triangle, hypotenuse^2 = (opposite side)^2 + (adjacent side)^2
auto oppositeSquared = leftStickY * leftStickY;
auto adjacentSquared = leftStickX * leftStickX;

// accept and process input if true; otherwise, reject and ignore it.
if ((oppositeSquared + adjacentSquared) > deadzoneSquared)
{
    // input accepted, process it
}
double leftStickX = reading.LeftThumbstickX;   // returns a value between -1.0 and +1.0
double leftStickY = reading.LeftThumbstickY;   // returns a value between -1.0 and +1.0

// choose a deadzone -- readings inside this radius are ignored.
const double deadzoneRadius = 0.1;
const double deadzoneSquared = deadzoneRadius * deadzoneRadius;

// Pythagorean theorem -- for a right triangle, hypotenuse^2 = (opposite side)^2 + (adjacent side)^2
double oppositeSquared = leftStickY * leftStickY;
double adjacentSquared = leftStickX * leftStickX;

// accept and process input if true; otherwise, reject and ignore it.
if ((oppositeSquared + adjacentSquared) > deadzoneSquared)
{
    // input accepted, process it
}

Каждый палец также действует как кнопка при нажатии внутрь; Дополнительные сведения о чтении этих входных данных см. в разделе "Чтение кнопок".

Чтение триггеров

Триггеры представлены в виде значений с плавающей запятой от 0,0 (полностью выпущено) до 1.0 (полностью подавлено). Значение левого триггера считывается из LeftTrigger свойства структуры GamepadReading; значение правого триггера считывается из RightTrigger свойства.

float leftTrigger  = reading.LeftTrigger;  // returns a value between 0.0 and 1.0
float rightTrigger = reading.RightTrigger; // returns a value between 0.0 and 1.0
double leftTrigger = reading.LeftTrigger;  // returns a value between 0.0 and 1.0
double rightTrigger = reading.RightTrigger; // returns a value between 0.0 and 1.0

Чтение кнопок

Каждая из кнопок геймпада — четыре направления D-pad, левых и правых бамперов, клавиши A, B, X, Y, View и Menu— это цифровое чтение, указывающее, нажимается ли оно (вниз) или освобождается (вверх). Для повышения эффективности чтение кнопок не представлено в виде отдельных логических значений; вместо этого они все упакованы в одно битовое поле, представленное перечислением GamepadButtons .

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

В следующем примере определяется, нажимается ли кнопка A.

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

В следующем примере определяется, выпущена ли кнопка A.

if (GamepadButtons::None == (reading.Buttons & GamepadButtons::A))
{
    // button A is released
}
if (GamepadButtons.None == (reading.Buttons & GamepadButtons.A))
{
    // button A is released
}

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

Запуск примера ввода геймпадов

Пример GamepadUWP (github) демонстрирует, как подключиться к геймпаду и прочитать его состояние.

Обзор триггеров вибрации и импульса

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

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

Использование вибрации и импульса

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

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

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

// get the first gamepad
Gamepad^ gamepad = Gamepad::Gamepads->GetAt(0);

// create an instance of GamepadVibration
GamepadVibration vibration;

// ... set vibration levels on vibration struct here

// copy the GamepadVibration struct to the gamepad
gamepad.Vibration = vibration;
// get the first gamepad
Gamepad gamepad = Gamepad.Gamepads[0];

// create an instance of GamepadVibration
GamepadVibration vibration = new GamepadVibration();

// ... set vibration levels on vibration struct here

// copy the GamepadVibration struct to the gamepad
gamepad.Vibration = vibration;

Использование вибрационных двигателей

Слева и правые вибрационные моторы принимают значения с плавающей запятой между 0,0 (без вибрации) и 1,0 (наиболее интенсивные вибрации). Интенсивность левого двигателя задается свойством структуры GamepadVibration; интенсивность правого двигателя задается LeftMotor свойствомRightMotor.

В следующем примере устанавливается интенсивность как двигателей вибрации, так и активируется вибрация геймпада.

GamepadVibration vibration;
vibration.LeftMotor = 0.80;  // sets the intensity of the left motor to 80%
vibration.RightMotor = 0.25; // sets the intensity of the right motor to 25%
gamepad.Vibration = vibration;
GamepadVibration vibration = new GamepadVibration();
vibration.LeftMotor = 0.80;  // sets the intensity of the left motor to 80%
vibration.RightMotor = 0.25; // sets the intensity of the right motor to 25%
mainGamepad.Vibration = vibration;

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

Использование триггеров импульса

Каждый двигатель триггера импульса принимает значение с плавающей запятой от 0,0 (без вибрации) до 1,0 (наиболее интенсивные вибрации). Интенсивность левого триггера задается свойством структуры GamepadVibration. Интенсивность правого триггера задается LeftTrigger свойствомRightTrigger.

Следующий пример задает интенсивность обоих триггеров импульса и активирует их.

GamepadVibration vibration;
vibration.LeftTrigger = 0.75;  // sets the intensity of the left trigger to 75%
vibration.RightTrigger = 0.50; // sets the intensity of the right trigger to 50%
gamepad.Vibration = vibration;
GamepadVibration vibration = new GamepadVibration();
vibration.LeftTrigger = 0.75;  // sets the intensity of the left trigger to 75%
vibration.RightTrigger = 0.50; // sets the intensity of the right trigger to 50%
mainGamepad.Vibration = vibration;

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

Запуск образца вибрации геймпада

Пример GamepadVibrationUWP (github) демонстрирует, как двигатель вибрации геймпада и триггеры импульса используются для создания различных эффектов.

См. также