Привет, Хабр! Читаю Хабр с момента его появления, но всегда только читал и этой статьей решил изменить ситуацию. Меня зовут Вениамин Афанасьев и я 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, которую я делаю в одиночку.
Надеюсь, данная статья поможет тем, кто столкнулся с этой проблемой.