Привет, Хабр! Читаю Хабр с момента его появления, но всегда только читал и этой статьей решил изменить ситуацию. Меня зовут Вениамин Афанасьев и я cоло инди разработчик, работающий в Unity. Довольно часто сталкиваюсь с различными проблемами движка и способах их решения. Сегодня я расскажу вам как заставить VirtualMouse из нового Input System Unity работать.
В своей новой игре VICCP-2 Core, я решил добавить поддержку геймпада и для этого установил новый Input System. Ожидалось, что в новой системе всё будет работать из коробки и в документации была описана эта возможность. В пакете даже есть проект с примером (картинка выше).
У пакета есть 2 режима отображения курсора: Hardware и Software. И тут же возникла первая проблема, у мышки и у геймпада были разные курсоры. При включении Hardware курсора, нажатия с мышки переставали регистрироваться. В довесок при управлении с геймпада курсор выходил за пределы экрана. Зум колесика не синхронизировался. Ситуация сложилась, что вроде оно работает, но пользоваться этим невозможно.
Естественно я сразу полез в интернет искать решение. Нашел кучу форумов, роликов на ютубе где пытались решить это. Но к моему удивлению ничего не нашел, за исключением одного ролика, но данный подход меня не удовлетворил.
Далее заметил, что пакет ставит 1.3.0 версию, а документация есть уже на 1.5.1. По этому я отправился на официальный гитхаб от Unity и без труда нашел там новую версию нужного мне исходника. И там я увидел это:
Разницы у файла из 2019 и файла 2023 версии 1.5.1 нет. За 3 года, все эти "TODO" так и не были реализованы (Добро пожаловать в Unity, но сам движок отличный).
Ситуация стала безнадежной, так как изначально я хотел использовать именно оригинальный "инпут систем", чтобы избежать потом каких либо проблем. Затем, я полез на ассетстор, и нашел там отличный ассет Rewired. Тут я понял, что скорее всего его, обычно все и используют, или подобный ему. По этой причине, информации о том как заставить работать родной VirtualMouse от Unity нет. Специально описал, способы поиска решения, так как это тоже опыт и он может пригодится в других задачах. Особенно когда люди впадают в ступор и обычная ссылка на гитхаб Unity, может спасти положение, так как многие даже не подозревают о его существовании. Движок постоянно дописывается и часто уже есть готовое и исправленное. А ещё мне данная ситуация показалась забавной, ведь это официальный пакет.
Я не горел желанием покупать дорогой ассет и жаба меня так задавила, что в итоге я решил залезть в исходник и доделать эти "TODO" сам. Так же влезть в код Unity, уже своеобразный челлендж. Итак приступим:
Вырезаем весь класс из пакета и создаем новый, так же меняем имя на "VirtualMouseV". Это нужно, чтобы мы никак не изменяли оригинальный пакет и у нас не было потом проблем, например после его обновления.
public class VirtualMouseV : MonoBehaviour
Тут убираем строчку "InputSystem.DisableDevice(m_SystemMouse);". Данная строчка, как раз и отключала мышку (Зачем?):
private void TryEnableHardwareCursor() { var devices = InputSystem.devices; for (var i = 0; i < devices.Count; ++i) { var device = devices[i]; if (device.native && device is Mouse mouse) { m_SystemMouse = mouse; break; } } if (m_SystemMouse == null) { if (m_CursorGraphic != null) m_CursorGraphic.enabled = true; return; } // убрал чтобы мышка продолжала работать // InputSystem.DisableDevice(m_SystemMouse); // Sync position. if (m_VirtualMouse != null) m_SystemMouse.WarpCursorPosition(m_VirtualMouse.position.ReadValue()); // Turn off mouse cursor image. if (m_CursorGraphic != null) m_CursorGraphic.enabled = false; }
Убираем "TryFindCanvas()". Будем выставлять канвас вручную:
// поиск канваса скрыл //private void TryFindCanvas() //{ // m_Canvas = m_CursorGraphic?.GetComponentInParent<Canvas>(); //}
Делаем канвас публичным, чтобы выставить его в инспекторе (не забудьте потом это сделать). А так же добавляем переменную "isUseGamePad", она нам понадобится для синхронизации мышки и геймпада.
public Canvas m_Canvas; // Canvas that gives the motion range for the software cursor. /// <summary> /// Флаг использовался ли гейпад /// </summary> private bool isUseGamePad = false;
Приводим "UpdateMotion()", вот в такой вид:
private void UpdateMotion() { // включаем флаг что геймпад используется isUseGamePad = true; if (m_VirtualMouse == null) { // флаг что геймпад не испольуется isUseGamePad = false; return; } // Read current stick value. var stickAction = m_StickAction.action; if (stickAction == null) { // флаг что геймпад не используется isUseGamePad = false; return; } var stickValue = stickAction.ReadValue<Vector2>(); if (Mathf.Approximately(0, stickValue.x) && Mathf.Approximately(0, stickValue.y)) { // Motion has stopped. m_LastTime = default; m_LastStickValue = default; // флаг что геймпад не используется isUseGamePad = false; } else { var currentTime = InputState.currentTime; if (Mathf.Approximately(0, m_LastStickValue.x) && Mathf.Approximately(0, m_LastStickValue.y)) { // Motion has started. m_LastTime = currentTime; } // Compute delta. var deltaTime = (float)(currentTime - m_LastTime); var delta = new Vector2(m_CursorSpeed * stickValue.x * deltaTime, m_CursorSpeed * stickValue.y * deltaTime); // Update position. var currentPosition = m_VirtualMouse.position.ReadValue(); var newPosition = currentPosition + delta; ////REVIEW: for the hardware cursor, clamp to something else? // Clamp to canvas. //if (m_Canvas != null) //{ // Clamp to canvas. var pixelRect = m_Canvas.pixelRect; newPosition.x = Mathf.Clamp(newPosition.x, pixelRect.xMin, pixelRect.xMax); newPosition.y = Mathf.Clamp(newPosition.y, pixelRect.yMin, pixelRect.yMax); //} ////REVIEW: the fact we have no events on these means that actions won't have an event ID to go by; problem? InputState.Change(m_VirtualMouse.position, newPosition); InputState.Change(m_VirtualMouse.delta, delta); // обычная мышка InputState.Change(virtualMouse.position, newPosition); InputState.Change(virtualMouse.delta, delta); // Update software cursor transform, if any. if (m_CursorTransform != null && (m_CursorMode == CursorMode.SoftwareCursor || (m_CursorMode == CursorMode.HardwareCursorIfAvailable && m_SystemMouse == null))) m_CursorTransform.anchoredPosition = newPosition; m_LastStickValue = stickValue; m_LastTime = currentTime; // Update hardware cursor. m_SystemMouse?.WarpCursorPosition(newPosition); } // Update scroll wheel. var scrollAction = m_ScrollWheelAction.action; if (scrollAction != null) { var scrollValue = scrollAction.ReadValue<Vector2>(); //Debug.Log(scrollValue.x + " " + scrollValue.y); scrollValue.x *= m_ScrollSpeed; scrollValue.y *= m_ScrollSpeed; InputState.Change(m_VirtualMouse.scroll, scrollValue); // синхронизирум мышку с текущим зумом, если есть нажатия if (scrollValue.y != 0) { // обновляем скролл у мышки InputState.Change(Mouse.current.scroll, scrollValue); } } }
Дописываем "OnAfterInputUpdate()":
private void OnAfterInputUpdate() { UpdateMotion(); // обновляем позицию курсора геймпада из позиции мышки, когда геймпад не зайдействован if (isUseGamePad == false) { InputState.Change(m_VirtualMouse.position, Mouse.current.position.ReadValue()); } }
Всё, теперь у нас есть полностью рабочий VirtualMouse, в режиме hardware. Теперь используется один курсор и на мышку и на геймпад, и он не выходит за границу экрана. Так же отслеживается состояние колесика и синхронизируется с геймпадом, по этому Mouse.current.scroll.ReadValue().y будет работать и там и там одинаково. Теперь Unity воспринимает геймпад как полноценную мышь.
Так же не забудьте добавить девайсы в настройке пакета:
Пример, как нужно выставлять управление у геймпада для колесика. Вещь неочевидная и на это тоже потратил время:


Демонстрация работы допиленного компонента. Курсор в ролике двигает мышка и геймпад поочередно. При этом они работают одновременно.
Пользуясь случаем, даю ссылку на свою будущую игру VICCP 2 Core, которую я делаю в одиночку.
Надеюсь, данная статья поможет тем, кто столкнулся с этой проблемой.