Контроллеры: действия и мёртвые зоны
WASD и стик через одни действия без потери аналоговой величины.
Для движения задайте действия Input Map, а затем читайте результат независимо от физической кнопки. Прямой опрос номера оси нужен для диагностики или отдельной схемы управления, а не вместо всех игровых действий.
Какой метод выбрать
| Задача | API |
|---|---|
| Ходьба по двум осям со стика или клавиш | Input.GetVector(left, right, up, down). |
| Одно направление с отрицательным и положительным значением | Input.GetAxis(negative, positive). |
| Аналоговая величина одного действия | Input.GetActionStrength(action). |
| Удержание кнопки | Input.IsActionPressed(action). |
| Одно действие при нажатии | Input.IsActionJustPressed(action). |
| Проверить необработанное значение физической оси | Input.GetJoyAxis(device, axis). |
Движение мыши и отклонение стика не имеют одинаковых единиц. Мышь сообщает приращение за событие, стик обычно задаёт направление и интенсивность, из которых вы вычисляете скорость поворота. Не применяйте к ним одну формулу с одинаковым delta.
Назначьте устройство
Input.GetConnectedJoypads() возвращает текущие идентификаторы устройств. Не считайте, что первый подключённый контроллер всегда имеет ID 0 или что его ID — постоянный идентификатор игрока. Храните назначение игрок → устройство и обновляйте его по Input.JoyConnectionChanged; после отключения предложите новое назначение и остановите зависящие от него действия.
Событие Input Map может быть привязано к конкретному устройству или ко всем устройствам. В локальной игре на нескольких контроллерах общая маска устройств не разделяет игроков. В примере ниже выбрано одно устройство; клавиатурные привязки общие и сами по себе не создают многопользовательский ввод.
Готовая обвязка для WASD и левого стика
Этот файл создаёт четыре новых действия для выбранного устройства, читает круговую мёртвую зону и удаляет созданные действия через Dispose(). Существующие действия с таким префиксом отклоняются, а не перезаписываются.
using System;
using Reckfell;
public sealed class MovementActions : IDisposable
{
private readonly string[] _names;
private bool _disposed;
public MovementActions(string prefix, int device)
{
if (string.IsNullOrWhiteSpace(prefix) || device < 0) throw new ArgumentOutOfRangeException();
_names = new[] { prefix + "_left", prefix + "_right", prefix + "_up", prefix + "_down" };
foreach (string name in _names)
if (InputMap.HasAction(name)) throw new InvalidOperationException("Action already exists: " + name);
Bind(_names[0], Key.A, JoyAxis.LeftX, -1, device);
Bind(_names[1], Key.D, JoyAxis.LeftX, 1, device);
Bind(_names[2], Key.W, JoyAxis.LeftY, -1, device);
Bind(_names[3], Key.S, JoyAxis.LeftY, 1, device);
}
private static void Bind(string name, Key key, JoyAxis axis, float sign, int device)
{
InputMap.AddAction(name, 0.2f);
InputMap.ActionAddEvent(name, new InputEventKey { PhysicalKeycode = key });
InputMap.ActionAddEvent(name, new InputEventJoypadMotion { Device = device, Axis = axis, AxisValue = sign });
}
public Vector2 Read(float deadzone = 0.2f)
{
if (_disposed) throw new ObjectDisposedException(nameof(MovementActions));
if (!float.IsFinite(deadzone) || deadzone < 0 || deadzone >= 1) throw new ArgumentOutOfRangeException(nameof(deadzone));
return Input.GetVector(_names[0], _names[1], _names[2], _names[3], deadzone);
}
public void Dispose()
{
if (_disposed) return;
foreach (string name in _names) InputMap.EraseAction(name);
_disposed = true;
}
}Создайте MovementActions один раз, например в _Ready(), с уникальным префиксом и ID выбранного подключённого устройства. В физическом цикле читайте Read() и задавайте CharacterBody2D.Velocity = direction * speed, затем вызывайте MoveAndSlide(). Не создавайте набор действий каждый кадр. Освобождайте его при окончательном завершении использования; после Dispose() чтение запрещено. Привязки, добавленные кодом, не становятся автоматически сохранёнными настройками проекта.
Для собственного Input Map используйте те же четыре направления. Сохраняйте небольшое аналоговое отклонение: безусловный Normalized() после GetVector() превратит любую ненулевую величину в полную скорость.
Мёртвая зона: точный смысл
GetVector() берёт необработанные силы четырёх действий, строит вектор и применяет круговую мёртвую зону к его длине. За зоной величина перенормируется к диапазону 0–1; диагональ ограничивается длиной 1. Если параметр отрицательный, используется среднее значение мёртвых зон четырёх действий. Это отличается от двух независимых GetAxis().
В выполненной проверке с зоной 0,2:
- Отклонение X=0,1 дало ноль, хотя raw-сила оставалась 0,1.
- X=0,6 дало X=0,5 после перенормировки.
- (0,3; 0,4) дало (0,225; 0,3): длина уменьшилась с 0,5 до 0,375.
- (1; 1) ограничилось единичной длиной; WASD использует те же действия.
Не подбирайте порог по этим числам вслепую. В текущем InputMap.AddAction() значение по умолчанию 0,2; уже сохранённое действие может иметь другое значение. Настраивайте его под реальное устройство и оставляйте пользователю регулировку.
Если ввод неправильный
Нет контроллера в списке — сначала проверьте подключение на уровне ОС. Устройство есть, но действие не меняется — проверьте ID, ось, знак и привязку Input Map. Другой игрок двигает персонажа — проверьте фильтр устройства. Персонаж ползёт без касания — измерьте raw-отклонение и настройте мёртвую зону. При отпускании движение остаётся — проверьте обновление состояния и собственный накопленный вектор.
Кнопка геймпада не обязана давать клавиатурные echo-события: повтор меню реализуйте таймером с начальной задержкой. Для перепривязки используйте ввод и переназначение; для маршрута события через интерфейс — InputEvent.
Выполнены синтетические события осей и клавиш, круговая зона, диагональ, фильтр устройства и очистка. Физические контроллеры, вибрация, датчики, отключение устройства, фокус ОС и другие платформы здесь не проверялись. Наличие API не гарантирует поддержку функции любым устройством.