Пакет Input System в установщике
Пакет Input System в установщике

Привет, Хабр! Читаю Хабр с момента его появления, но всегда только читал и этой статьей решил изменить ситуацию. Меня зовут Вениамин Афанасьев и я cоло инди разработчик, работающий в Unity. Довольно часто сталкиваюсь с различными проблемами движка и способах их решения. Сегодня я расскажу вам как заставить VirtualMouse из нового Input System Unity работать.

В своей новой игре VICCP-2 Core, я решил добавить поддержку геймпада и для этого установил новый Input System. Ожидалось, что в новой системе всё будет работать из коробки и в документации была описана эта возможность. В пакете даже есть проект с примером (картинка выше).

Компонент Virtual Mouse
Компонент Virtual Mouse

У пакета есть 2 режима отображения курсора: Hardware и Software. И тут же возникла первая проблема, у мышки и у геймпада были разные курсоры. При включении Hardware курсора, нажатия с мышки переставали регистрироваться. В довесок при управлении с геймпада курсор выходил за пределы экрана. Зум колесика не синхронизировался. Ситуация сложилась, что вроде оно работает, но пользоваться этим невозможно.

Естественно я сразу полез в интернет искать решение. Нашел кучу форумов, роликов на ютубе где пытались решить это. Но к моему удивлению ничего не нашел, за исключением одного ролика, но данный подход меня не удовлетворил.

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

TO DO 2023
TO DO 2023

Разницы у файла из 2019 и файла 2023 версии 1.5.1 нет. За 3 года, все эти "TODO" так и не были реализованы (Добро пожаловать в Unity, но сам движок отличный).

TO DO 2019
TO DO 2019

Ситуация стала безнадежной, так как изначально я хотел использовать именно оригинальный "инпут систем", чтобы избежать потом каких либо проблем. Затем, я полез на ассетстор, и нашел там отличный ассет Rewired. Тут я понял, что скорее всего его, обычно все и используют, или подобный ему. По этой причине, информации о том как заставить работать родной VirtualMouse от Unity нет. Специально описал, способы поиска решения, так как это тоже опыт и он может пригодится в других задачах. Особенно когда люди впадают в ступор и обычная ссылка на гитхаб Unity, может спасти положение, так как многие даже не подозревают о его существовании. Движок постоянно дописывается и часто уже есть готовое и исправленное. А ещё мне данная ситуация показалась забавной, ведь это официальный пакет.

Я не горел желанием покупать дорогой ассет и жаба меня так задавила, что в итоге я решил залезть в исходник и доделать эти "TODO" сам. Так же влезть в код Unity, уже своеобразный челлендж. Итак приступим:

  1. Вырезаем весь класс из пакета и создаем новый, так же меняем имя на "VirtualMouseV". Это нужно, чтобы мы никак не изменяли оригинальный пакет и у нас не было потом проблем, например после его обновления.

public class VirtualMouseV : MonoBehaviour
  1. Тут убираем строчку "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;
        }
  1. Убираем "TryFindCanvas()". Будем выставлять канвас вручную:

// поиск канваса скрыл
        //private void TryFindCanvas()
        //{
        //    m_Canvas = m_CursorGraphic?.GetComponentInParent<Canvas>();
        //}
  1. Делаем канвас публичным, чтобы выставить его в инспекторе (не забудьте потом это сделать). А так же добавляем переменную "isUseGamePad", она нам понадобится для синхронизации мышки и геймпада.

        public Canvas m_Canvas; // Canvas that gives the motion range for the software cursor.
        
        /// <summary>
        /// Флаг использовался ли гейпад
        /// </summary>
        private bool isUseGamePad = false;
  1. Приводим "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);
                }
            }

            
        }
  1. Дописываем "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 воспринимает геймпад как полноценную мышь.

Так же не забудьте добавить девайсы в настройке пакета:

Настройки Input System Package
Настройки Input System Package

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

VirtualMouseV
VirtualMouseV
VirtualMouseV
VirtualMouseV
VirtualMouseV
VirtualMouseV

Демонстрация работы допиленного компонента. Курсор в ролике двигает мышка и геймпад поочередно. При этом они работают одновременно.

Пользуясь случаем, даю ссылку на свою будущую игру VICCP 2 Core, которую я делаю в одиночку.

Надеюсь, данная статья поможет тем, кто столкнулся с этой проблемой.

Комментарии (1)