InputBusSystem

Namespace: Vortex.Unity.InputBusSystem Сборка: ru.vortex.unity.inputbussystem

Назначение

Управление вводом на основе Unity Input System с поддержкой карт ввода (Input Action Maps), подписки на экшены по принципу LIFO и прямой привязки клавиш.

Возможности:

  • Централизованная маршрутизация сигналов ввода через статический контроллер
  • Управление картами ввода с автоматической активацией/деактивацией по наличию подписчиков
  • LIFO-маршрутизация — сигнал получает только последний зарегистрированный подписчик
  • Гарантия парности performed/canceled — отмена доставляется только получателю нажатия
  • Привязка произвольных клавиш и комбинаций к UI-событиям без прямого взаимодействия с Input System API

Вне ответственности:

  • Ребиндинг клавиш в рантайме
  • Визуальный редактор карт ввода (используется стандартный Unity Input Actions Asset)
  • Аналоговый ввод в маршрутизации — шина роутит событиями только дискретные действия (Button); аналоговые значения читаются polling'ом через GetAction (см. Использование)

Зависимости

Зависимость Назначение
Unity Input System InputSystem.actions, InputAction, InputActionMap
Vortex.Core.AppSystem App.GetState(), App.OnStateChanged, App.OnExit
Vortex.Core.SettingsSystem Settings.Data().DebugMode — диагностическое логирование
Vortex.Core.Extensions AddNew, AddOnce — хелперы коллекций
Vortex.Unity.EditorTools [ValueSelector], [ClassLabel]
Vortex.Unity.UI.Misc AdvancedButton — опциональная интеграция

Использование

Подписка на экшен из кода

InputController.AddActionUser("Player/Jump", this, OnJumpPerformed, OnJumpCanceled);

// Отписка
InputController.RemoveActionUser("Player/Jump", this);

this используется как ключ идентификации. Повторный AddActionUser с тем же объектом не создаст дубликат.

Polling-чтение значения (GetAction)

Для аналогового ввода (стики, триггеры, курсорная дельта), где нужна не callback-подписка, а само значение по требованию, GetAction("Карта/Экшен") отдаёт InputAction напрямую — дальше ReadValue<T>() / IsPressed() в своём Update:

var move = InputController.GetAction("Player/Move");
var delta = move?.ReadValue<Vector2>() ?? Vector2.zero;

null — ключ не найден. Это escape-hatch мимо LIFO-маршрутизации: значение читает сам вызывающий, шина в polling не участвует (в эдиторе метод при необходимости сам поднимает Init).

Подписка через Inspector (InputActionHandler)

Компонент InputActionHandler на GameObject:

  • inputAction — экшен из dropdown (составной id Карта/Экшен, сгруппирован по картам в папки)
  • button — опционально, ссылка на AdvancedButton
  • onPressed / onReleased — Unity Events

Позиция указателя (InputPointerHandler)

Компонент InputPointerHandler подписывается на экшен по правилам шины (LIFO) и на каждое его событие отдаёт наружу текущую позицию указателя одним UnityEvent<Vector2> (onPoint, screen-координаты через Pointer.current). В отличие от InputActionHandler (два события press/release) — один метод со значением вектора; что делать с точкой и когда начинать/заканчивать ловлю решает подписчик (например, включением/выключением объекта по press-экшену).

  • inputAction — экшен, событие которого = момент отдать позицию (напр. CursorPosition)
  • onPoint — UnityEvent<Vector2>, вызывается на каждое событие экшена с текущей позицией

Подписка отложена до AppStates.Running (как в InputActionHandler), автоотписка в OnDisable/OnDestroy.

Активация карты (InputMapHandler)

Компонент InputMapHandler на GameObject:

  • inputMap — карта из dropdown

Карта активна, пока хотя бы один InputMapHandler для неё включён в сцене.

Прямая привязка клавиш (KeyboardHandler)

Компонент KeyboardHandler на GameObject:

  • buttonCode — массив отдельных клавиш (любая вызовет срабатывание)
  • buttonsCombinations — массив комбинаций (последняя клавиша — активирующая, остальные — модификаторы)
  • button, onPressed, onReleased — аналогично InputActionHandler

Архитектура

InputBusSystem/
├── InputController.cs          # Статический контроллер: индексация, маршрутизация
├── InputSubscriber.cs          # Модель подписчика с equality по Owner
├── Handlers/
│   ├── InputActionHandler.cs   # Подписка на экшен через InputController
│   ├── InputPointerHandler.cs  # На каждое событие экшена отдаёт позицию указателя (screen)
│   ├── InputMapHandler.cs      # Активация карты ввода
│   └── KeyboardHandler.cs      # Прямая привязка клавиш/комбинаций
└── Debug/
    ├── Model/SettingsModelExtInput.cs    # partial SettingsModel + InputDebugMode
    └── Presets/DebugSettingsExtInput.cs  # partial DebugSettings + toggle

Поток данных

Unity Input System
    ↓ performed / canceled
InputController (маршрутизация по LIFO)
    ↓ callback
InputActionHandler / подписчик
    ↓ UnityEvent
UI / игровая логика

KeyboardHandler работает автономно — создаёт собственный InputAction в обход InputController.


Ключевые концепции

Составной id экшена (Карта/Экшен)

Экшен индексируется по составному id Карта/Экшен (через /), а не по голому имени. Это:

  • снимает конфликт одноимённых экшенов в разных картах (id различаются, Init не падает на дубликате);
  • раскладывает дропдаун по папкам-картам — ValueSelector через SearchablePopup группирует значения по /.

Философия уникальности имён сохраняется: уникальна теперь связка «карта/экшен», а не голое имя экшена.

LIFO-маршрутизация

При нескольких подписчиках на один экшен performed доставляется только последнему (subscribers[^1]). Позволяет перехватывать ввод в модальных окнах и оверлеях без отписки предыдущих слоёв.

Парность performed/canceled

Объект, получивший performed, запоминается в CatchPerformed. При canceled сигнал доставляется только ему. Если верхний подписчик сменился — canceled отбрасывается.

Карты с подсчётом пользователей

Карта активируется при первом AddMapUser и деактивируется при уходе последнего пользователя (RemoveMapUser). Предотвращает конфликт между картами без ручного управления.

Комбинации клавиш

KeyboardHandler поддерживает до 3 модификаторов через композитные биндинги Unity (OneModifier, TwoModifiers, ThreeModifiers). Последняя клавиша в массиве — активирующая, остальные — модификаторы.

Отложенная подписка

Хэндлеры проверяют App.GetState() < AppStates.Running и откладывают регистрацию до готовности приложения через App.OnStateChanged.


Контракт

Вход

  • Unity Input Actions Asset с определёнными картами и экшенами
  • AppStates.Running — состояние, при котором хэндлеры регистрируются

Выход

  • UnityEvent onPressed / onReleased — в InputActionHandler и KeyboardHandler
  • AdvancedButton.Press() / Release() — опциональная интеграция
  • Активация/деактивация InputActionMap — в InputMapHandler

Гарантии

  • Один performed → максимум один canceled для того же подписчика
  • Карта деактивируется только при нулевом количестве пользователей
  • Подписчик не дублируется в списке (AddOnce с equality по Owner)
  • Хэндлеры отписываются в OnDisable / OnDestroy

Ограничения

  • Маршрутизация — только дискретный ввод (Button); аналоговые значения событиями не передаются, но доступны polling'ом через GetAction
  • InputSubscriber.Equals сравнивает по Owner — один объект не может иметь разные колбэки на один экшен
  • KeyboardHandler поддерживает максимум 3 модификатора; 4+ игнорируются с предупреждением
  • Нет механизма приоритетов — только порядок добавления (LIFO)

API

InputController (static)

// Карты ввода
static string[] GetMaps()
static void AddMapUser(string mapId, object inputMapUser)
static void RemoveMapUser(string mapId, object inputMapUser)

// Экшены ввода
static string[] GetActions()
static void AddActionUser(string actionInputId, object actionInputUser,
    Action onPerformedCallback, Action onCanceledCallback)
static void RemoveActionUser(string actionInputId, object actionInputUser)

// Прямой доступ к InputAction для polling-чтения значения (ReadValue<T>()/IsPressed())
static InputAction GetAction(string actionInputId)   // null — ключ не найден

InputSubscriber

// Equality определяется по Owner
public readonly object Owner;
public Action OnPerformed { get; }
public Action OnCanceled { get; }

Критические требования

  1. Input Actions Asset должен быть назначен — InputController.Init() читает InputSystem.actions. При отсутствии словари пусты, хэндлеры выбросят KeyNotFoundException.
  2. Уникальны имя карты и связка Карта/Экшен. Экшены индексируются составным id, поэтому одноимённые экшены в РАЗНЫХ картах допустимы (id различаются). Дубликат имени карты или полностью совпадающего Карта/Экшен вызовет ArgumentException при инициализации (в норме Unity такого не создаёт).
  3. KeyboardHandler не проходит через InputController — его экшены не участвуют в LIFO-маршрутизации.
  4. KeyboardHandler требует физической клавиатуры — при Keyboard.current == null логирует предупреждение.

Debug

Диагностическое логирование InputController управляется через Settings.Data().InputDebugMode; KeyboardHandler логирует под общим Settings.Data().DebugMode. Настраивается в ассете DebugSettings (toggle InputDebugMode).

При включении логируются: регистрация/отписка подписчиков, доставка performed/canceled, активация/деактивация карт.


Граничные случаи

Ситуация Поведение
Несуществующее имя экшена/карты KeyNotFoundException (кроме RemoveActionUser)
RemoveActionUser для незарегистрированного объекта или несуществующего экшена Тихий пропуск — в т.ч. отписка на App.OnExit после очистки шины
Подписчик performed удалён до canceled canceled отбрасывается
Нет подписчиков при performed Тихий пропуск
Keyboard.current == null Предупреждение; нажатия игнорируются
Комбинация с 4+ модификаторами Предупреждение; комбинация пропущена
OnEnable до AppStates.Running Подписка отложена до смены состояния
Input Actions Asset не назначен Словари пусты; KeyNotFoundException при регистрации