Canvas UI Object As Virtual Cursor?

Hello all,

Apologies if this is in the wrong topic, but this sort of overlaps with scripting, Input, and UI, but thought this was the best place.

I am trying to create a virtual cursor in a canvas as I am rendering the canvas to a RenderTexture for other purposes and need to be able to interact with the UI elements in a way similar to a computer (it’s an interactable element).

I found a repo that does this perfectly in Unity 5, but seems to have been largely broken at some point along the line to 2021 (the version my project is using), and can’t for the life of me work out how it’s broken.

No errors are thrown, no warnings, no nothing.

I was wondering if anyone has been able to build a system similar to this

As a prime example, what this user was searching for is what I am after, but this also seems to no longer work either, and all other examples I can find are using the new input system. Unfortunately I am using a custom controls sub-system built off the legacy input so the new built-in Gamepad Mouse Cursor would also not be a go.

Basically, I just want to be able to use the pivot point of a UI Image object (or rather it’s RectTransform) as the mouse position, and register it as a click location when the actual physical mouse is clicked. I already have the Image object moving around in the Canvas but can’t seem to work out how to pass this as a replacement cursor.

Try looking into UGUI documentation for all kinds of interfaces which are used to register mouse pointer or touches etc. They work perfectly, I’ve used it literally a couple of weeks ago in a mobile project for a sliding “virtual joystick”.

Here is the main link, and the scripting reference.

In your case, you want something like IPointerMoveHandler
To use it you just implement this interface like so

using UnityEngine;
using UnityEngine.UI;

public class MyPointerCapturingClass : MonoBehaviour, IPointerMoveHandler { }

Now you implement the actual interface by adding this one callback method

void OnPointerMove(PointerEventData eventData) {
  // where you have access to eventData
}

PointerEventData has a lot of stuff you can grab and take into consideration.
Now the question remains whether you can put this directly on the canvas object.

I’ve used it recently in a context where I stretched an invisible raw image over my game, and this is where the interaction takes place, so this is where I put the script as well. I think it should work with just the canvas, although you should experiment on your own, see what suits you best.

To use this systematically is a little cumbersome (similar to how physics collisions and triggers are registered), and so if you want to connect this with another part of your logic/engine I recommend you to try implement events (either Unity’s if you want them in the inspector, or native C#) so that you can propagate the data you actually need where you need it.

For example

delegate void CursorHandler(Vector2 position, int button);
// ^ this is just a meta-declaration for a delegate, can be anywhere, even nested, like class or interface

public event CursorHandler OnCursorChange; // this is part of the class

void OnPointerMove(PointerEventData eventData) {
  OnCursorChange?.Invoke(eventData.position, (int)eventData.button);
}

Then in your user code, you just hook this up by doing

using UnityEngine;

public class MyUserBehaviour : MonoBehaviour {

  [SerializeField] MyPointerCapturingClass _capture;

  void OnEnable() => _capture.OnCursorChange += myPointerHandler;
  void OnDisable() => _capture.OnCursorChange -= myPointerHandler;

  void myPointerHandler(Vector2 position, int button) {
    Debug.Log($"position is {position} button is {button}");
  }

}

I’ve copied the current Standalone Input Module and modified it to allow the cursor’s position to be the virtual cursor position rather than the mouse, and I’ve verified this is working using the previewGUI, however the PointerEnter entry never yields any results on any UI elements, nor do any interactive elements respond to interactions.

I’ve been over my code a few times, looking at both the default Standalone and my own and can’t work out what I’m doing wrong. It’s still surprising to me there is no simple way to do this considering how many games have this feature.

Am I missing something obvious?

public class VirtualInputModule : PointerInputModule
{

    public RectTransform VirtualCursor;
    public Camera CanvasCamera;

    private float m_PrevActionTime;
    private Vector2 m_LastMoveVector;
    private int m_ConsecutiveMoveCount = 0;

    private Vector2 m_LastMousePosition;
    private Vector2 m_MousePosition;

    private GameObject m_CurrentFocusedGameObject;

    private PointerEventData m_InputPointerEvent;

    /*protected StandaloneInputModule()
    {
    }*/

    [Obsolete("Mode is no longer needed on input module as it handles both mouse and keyboard simultaneously.", false)]
    public enum InputMode
    {
        Mouse,
        Buttons
    }

    [Obsolete("Mode is no longer needed on input module as it handles both mouse and keyboard simultaneously.", false)]
    public InputMode inputMode
    {
        get { return InputMode.Mouse; }
    }

    [SerializeField]
    private string m_HorizontalAxis = "Horizontal";

    /// <summary>
    /// Name of the vertical axis for movement (if axis events are used).
    /// </summary>
    [SerializeField]
    private string m_VerticalAxis = "Vertical";

    /// <summary>
    /// Name of the submit button.
    /// </summary>
    [SerializeField]
    private string m_SubmitButton = "Submit";

    /// <summary>
    /// Name of the submit button.
    /// </summary>
    [SerializeField]
    private string m_CancelButton = "Cancel";

    [SerializeField]
    private float m_InputActionsPerSecond = 10;

    [SerializeField]
    private float m_RepeatDelay = 0.5f;

    [SerializeField]
    [FormerlySerializedAs("m_AllowActivationOnMobileDevice")]
    [HideInInspector]
    private bool m_ForceModuleActive;

    [Obsolete("allowActivationOnMobileDevice has been deprecated. Use forceModuleActive instead (UnityUpgradable) -> forceModuleActive")]
    public bool allowActivationOnMobileDevice
    {
        get { return m_ForceModuleActive; }
        set { m_ForceModuleActive = value; }
    }

    /// <summary>
    /// Force this module to be active.
    /// </summary>
    /// <remarks>
    /// If there is no module active with higher priority (ordered in the inspector) this module will be forced active even if valid enabling conditions are not met.
    /// </remarks>

    [Obsolete("forceModuleActive has been deprecated. There is no need to force the module awake as StandaloneInputModule works for all platforms")]
    public bool forceModuleActive
    {
        get { return m_ForceModuleActive; }
        set { m_ForceModuleActive = value; }
    }

    /// <summary>
    /// Number of keyboard / controller inputs allowed per second.
    /// </summary>
    public float inputActionsPerSecond
    {
        get { return m_InputActionsPerSecond; }
        set { m_InputActionsPerSecond = value; }
    }

    /// <summary>
    /// Delay in seconds before the input actions per second repeat rate takes effect.
    /// </summary>
    /// <remarks>
    /// If the same direction is sustained, the inputActionsPerSecond property can be used to control the rate at which events are fired. However, it can be desirable that the first repetition is delayed, so the user doesn't get repeated actions by accident.
    /// </remarks>
    public float repeatDelay
    {
        get { return m_RepeatDelay; }
        set { m_RepeatDelay = value; }
    }

    /// <summary>
    /// Name of the horizontal axis for movement (if axis events are used).
    /// </summary>
    public string horizontalAxis
    {
        get { return m_HorizontalAxis; }
        set { m_HorizontalAxis = value; }
    }

    /// <summary>
    /// Name of the vertical axis for movement (if axis events are used).
    /// </summary>
    public string verticalAxis
    {
        get { return m_VerticalAxis; }
        set { m_VerticalAxis = value; }
    }

    /// <summary>
    /// Maximum number of input events handled per second.
    /// </summary>
    public string submitButton
    {
        get { return m_SubmitButton; }
        set { m_SubmitButton = value; }
    }

    /// <summary>
    /// Input manager name for the 'cancel' button.
    /// </summary>
    public string cancelButton
    {
        get { return m_CancelButton; }
        set { m_CancelButton = value; }
    }

    private bool ShouldIgnoreEventsOnNoFocus()
    {
#if UNITY_EDITOR
        return !UnityEditor.EditorApplication.isRemoteConnected;
#else
            return true;
#endif
    }

    public override void UpdateModule()
    {
        if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
        {
            if (m_InputPointerEvent != null && m_InputPointerEvent.pointerDrag != null && m_InputPointerEvent.dragging)
            {
                ReleaseMouse(m_InputPointerEvent, m_InputPointerEvent.pointerCurrentRaycast.gameObject);
            }

            m_InputPointerEvent = null;

            return;
        }

        m_LastMousePosition = m_MousePosition;
        m_MousePosition = VirtualCursor.anchoredPosition;
    }

    private void ReleaseMouse(PointerEventData pointerEvent, GameObject currentOverGo)
    {
        ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

        var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

        // PointerClick and Drop events
        if (pointerEvent.pointerClick == pointerClickHandler && pointerEvent.eligibleForClick)
        {
            ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);
        }
        if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
        {
            ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
        }

        pointerEvent.eligibleForClick = false;
        pointerEvent.pointerPress = null;
        pointerEvent.rawPointerPress = null;
        pointerEvent.pointerClick = null;

        if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
            ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);

        pointerEvent.dragging = false;
        pointerEvent.pointerDrag = null;

        // redo pointer enter / exit to refresh state
        // so that if we moused over something that ignored it before
        // due to having pressed on something else
        // it now gets it.
        if (currentOverGo != pointerEvent.pointerEnter)
        {
            HandlePointerExitAndEnter(pointerEvent, null);
            HandlePointerExitAndEnter(pointerEvent, currentOverGo);
        }

        m_InputPointerEvent = pointerEvent;
    }

    public override bool ShouldActivateModule()
    {
        if (!base.ShouldActivateModule())
            return false;

        var shouldActivate = m_ForceModuleActive;
        shouldActivate |= input.GetButtonDown(m_SubmitButton);
        shouldActivate |= input.GetButtonDown(m_CancelButton);
        shouldActivate |= !Mathf.Approximately(input.GetAxisRaw(m_HorizontalAxis), 0.0f);
        shouldActivate |= !Mathf.Approximately(input.GetAxisRaw(m_VerticalAxis), 0.0f);
        shouldActivate |= (m_MousePosition - m_LastMousePosition).sqrMagnitude > 0.0f;
        shouldActivate |= input.GetMouseButtonDown(0);

        if (input.touchCount > 0)
            shouldActivate = true;

        return shouldActivate;
    }

    /// <summary>
    /// See BaseInputModule.
    /// </summary>
    public override void ActivateModule()
    {
        if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
            return;

        base.ActivateModule();
        m_MousePosition = input.mousePosition;
        m_LastMousePosition = input.mousePosition;

        var toSelect = eventSystem.currentSelectedGameObject;
        if (toSelect == null)
            toSelect = eventSystem.firstSelectedGameObject;

        eventSystem.SetSelectedGameObject(toSelect, GetBaseEventData());
    }

    /// <summary>
    /// See BaseInputModule.
    /// </summary>
    public override void DeactivateModule()
    {
        base.DeactivateModule();
        ClearSelection();
    }

    public override void Process()
    {
        if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
            return;

        bool usedEvent = SendUpdateEventToSelectedObject();

        // case 1004066 - touch / mouse events should be processed before navigation events in case
        // they change the current selected gameobject and the submit button is a touch / mouse button.

        // touch needs to take precedence because of the mouse emulation layer
        if (!ProcessTouchEvents() && input.mousePresent)
            ProcessMouseEvent();

        if (eventSystem.sendNavigationEvents)
        {
            if (!usedEvent)
                usedEvent |= SendMoveEventToSelectedObject();

            if (!usedEvent)
                SendSubmitEventToSelectedObject();
        }
    }

    private bool ProcessTouchEvents()
    {
        for (int i = 0; i < input.touchCount; ++i)
        {
            Touch touch = input.GetTouch(i);

            if (touch.type == TouchType.Indirect)
                continue;

            bool released;
            bool pressed;
            var pointer = GetTouchPointerEventData(touch, out pressed, out released);

            ProcessTouchPress(pointer, pressed, released);

            if (!released)
            {
                ProcessMove(pointer);
                ProcessDrag(pointer);
            }
            else
                RemovePointerData(pointer);
        }
        return input.touchCount > 0;
    }

    /// <summary>
    /// This method is called by Unity whenever a touch event is processed. Override this method with a custom implementation to process touch events yourself.
    /// </summary>
    /// <param name="pointerEvent">Event data relating to the touch event, such as position and ID to be passed to the touch event destination object.</param>
    /// <param name="pressed">This is true for the first frame of a touch event, and false thereafter. This can therefore be used to determine the instant a touch event occurred.</param>
    /// <param name="released">This is true only for the last frame of a touch event.</param>
    /// <remarks>
    /// This method can be overridden in derived classes to change how touch press events are handled.
    /// </remarks>
    protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released)
    {
        var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

        // PointerDown notification
        if (pressed)
        {
            pointerEvent.eligibleForClick = true;
            pointerEvent.delta = Vector2.zero;
            pointerEvent.dragging = false;
            pointerEvent.useDragThreshold = true;
            pointerEvent.pressPosition = pointerEvent.position;
            pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;

            DeselectIfSelectionChanged(currentOverGo, pointerEvent);

            if (pointerEvent.pointerEnter != currentOverGo)
            {
                // send a pointer enter to the touched element if it isn't the one to select...
                HandlePointerExitAndEnter(pointerEvent, currentOverGo);
                pointerEvent.pointerEnter = currentOverGo;
            }

            // search for the control that will receive the press
            // if we can't find a press handler set the press
            // handler to be what would receive a click.
            var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);

            var newClick = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

            // didnt find a press handler... search for a click handler
            if (newPressed == null)
                newPressed = newClick;

            // Debug.Log("Pressed: " + newPressed);

            float time = Time.unscaledTime;

            if (newPressed == pointerEvent.lastPress)
            {
                var diffTime = time - pointerEvent.clickTime;
                if (diffTime < 0.3f)
                    ++pointerEvent.clickCount;
                else
                    pointerEvent.clickCount = 1;

                pointerEvent.clickTime = time;
            }
            else
            {
                pointerEvent.clickCount = 1;
            }

            pointerEvent.pointerPress = newPressed;
            pointerEvent.rawPointerPress = currentOverGo;
            pointerEvent.pointerClick = newClick;

            pointerEvent.clickTime = time;

            // Save the drag handler as well
            pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);

            if (pointerEvent.pointerDrag != null)
                ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
        }

        // PointerUp notification
        if (released)
        {
            // Debug.Log("Executing pressup on: " + pointer.pointerPress);
            ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

            // Debug.Log("KeyCode: " + pointer.eventData.keyCode);

            // see if we mouse up on the same element that we clicked on...
            var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

            // PointerClick and Drop events
            if (pointerEvent.pointerClick == pointerClickHandler && pointerEvent.eligibleForClick)
            {
                ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);
            }

            if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
            {
                ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
            }

            pointerEvent.eligibleForClick = false;
            pointerEvent.pointerPress = null;
            pointerEvent.rawPointerPress = null;
            pointerEvent.pointerClick = null;

            if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
                ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);

            pointerEvent.dragging = false;
            pointerEvent.pointerDrag = null;

            // send exit events as we need to simulate this on touch up on touch device
            ExecuteEvents.ExecuteHierarchy(pointerEvent.pointerEnter, pointerEvent, ExecuteEvents.pointerExitHandler);
            pointerEvent.pointerEnter = null;
        }

        m_InputPointerEvent = pointerEvent;
    }

    /// <summary>
    /// Calculate and send a submit event to the current selected object.
    /// </summary>
    /// <returns>If the submit event was used by the selected object.</returns>
    protected bool SendSubmitEventToSelectedObject()
    {
        if (eventSystem.currentSelectedGameObject == null)
            return false;

        var data = GetBaseEventData();
        if (input.GetButtonDown(m_SubmitButton))
            ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.submitHandler);

        if (input.GetButtonDown(m_CancelButton))
            ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.cancelHandler);
        return data.used;
    }

    private Vector2 GetRawMoveVector()
    {
        Vector2 move = Vector2.zero;
        move.x = input.GetAxisRaw(m_HorizontalAxis);
        move.y = input.GetAxisRaw(m_VerticalAxis);

        if (input.GetButtonDown(m_HorizontalAxis))
        {
            if (move.x < 0)
                move.x = -1f;
            if (move.x > 0)
                move.x = 1f;
        }
        if (input.GetButtonDown(m_VerticalAxis))
        {
            if (move.y < 0)
                move.y = -1f;
            if (move.y > 0)
                move.y = 1f;
        }
        return move;
    }

    /// <summary>
    /// Calculate and send a move event to the current selected object.
    /// </summary>
    /// <returns>If the move event was used by the selected object.</returns>
    protected bool SendMoveEventToSelectedObject()
    {
        float time = Time.unscaledTime;

        Vector2 movement = GetRawMoveVector();
        if (Mathf.Approximately(movement.x, 0f) && Mathf.Approximately(movement.y, 0f))
        {
            m_ConsecutiveMoveCount = 0;
            return false;
        }

        bool similarDir = (Vector2.Dot(movement, m_LastMoveVector) > 0);

        // If direction didn't change at least 90 degrees, wait for delay before allowing consequtive event.
        if (similarDir && m_ConsecutiveMoveCount == 1)
        {
            if (time <= m_PrevActionTime + m_RepeatDelay)
                return false;
        }
        // If direction changed at least 90 degree, or we already had the delay, repeat at repeat rate.
        else
        {
            if (time <= m_PrevActionTime + 1f / m_InputActionsPerSecond)
                return false;
        }

        var axisEventData = GetAxisEventData(movement.x, movement.y, 0.6f);

        if (axisEventData.moveDir != MoveDirection.None)
        {
            ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, axisEventData, ExecuteEvents.moveHandler);
            if (!similarDir)
                m_ConsecutiveMoveCount = 0;
            m_ConsecutiveMoveCount++;
            m_PrevActionTime = time;
            m_LastMoveVector = movement;
        }
        else
        {
            m_ConsecutiveMoveCount = 0;
        }

        return axisEventData.used;
    }

    protected void ProcessMouseEvent()
    {
        ProcessMouseEvent(0);
    }

    [Obsolete("This method is no longer checked, overriding it with return true does nothing!")]
    protected virtual bool ForceAutoSelect()
    {
        return false;
    }

    /// <summary>
    /// Process all mouse events.
    /// </summary>
    protected void ProcessMouseEvent(int id)
    {
        var mouseData = GetMousePointerEventData(id);
        var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;

        m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject;

        // Process the first mouse button fully
        ProcessMousePress(leftButtonData);
        ProcessMove(leftButtonData.buttonData);
        ProcessDrag(leftButtonData.buttonData);

        // Now process right / middle clicks
        ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData);
        ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData);
        ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData);
        ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData);

        if (!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f))
        {
            var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);
            ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);
        }
    }

    protected bool SendUpdateEventToSelectedObject()
    {
        if (eventSystem.currentSelectedGameObject == null)
            return false;

        var data = GetBaseEventData();
        ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.updateSelectedHandler);
        return data.used;
    }

    /// <summary>
    /// Calculate and process any mouse button state changes.
    /// </summary>
    protected void ProcessMousePress(MouseButtonEventData data)
    {
        var pointerEvent = data.buttonData;
        var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

        // PointerDown notification
        if (data.PressedThisFrame())
        {
            pointerEvent.eligibleForClick = true;
            pointerEvent.delta = Vector2.zero;
            pointerEvent.dragging = false;
            pointerEvent.useDragThreshold = true;
            pointerEvent.pressPosition = pointerEvent.position;
            pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;

            DeselectIfSelectionChanged(currentOverGo, pointerEvent);

            // search for the control that will receive the press
            // if we can't find a press handler set the press
            // handler to be what would receive a click.
            var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);
            var newClick = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

            // didnt find a press handler... search for a click handler
            if (newPressed == null)
                newPressed = newClick;

            // Debug.Log("Pressed: " + newPressed);

            float time = Time.unscaledTime;

            if (newPressed == pointerEvent.lastPress)
            {
                var diffTime = time - pointerEvent.clickTime;
                if (diffTime < 0.3f)
                    ++pointerEvent.clickCount;
                else
                    pointerEvent.clickCount = 1;

                pointerEvent.clickTime = time;
            }
            else
            {
                pointerEvent.clickCount = 1;
            }

            pointerEvent.pointerPress = newPressed;
            pointerEvent.rawPointerPress = currentOverGo;
            pointerEvent.pointerClick = newClick;

            pointerEvent.clickTime = time;

            // Save the drag handler as well
            pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);

            if (pointerEvent.pointerDrag != null)
                ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);

            m_InputPointerEvent = pointerEvent;
        }

        // PointerUp notification
        if (data.ReleasedThisFrame())
        {
            ReleaseMouse(pointerEvent, currentOverGo);
        }
    }

    private readonly MouseState m_MouseState = new MouseState();

    protected override MouseState GetMousePointerEventData(int id)
    {
        // Populate the left button...
        PointerEventData leftData;
        var created = GetPointerData(kMouseLeftId, out leftData, true);

        leftData.Reset();

        var screenSpaceCursorPosition = RectTransformUtility.WorldToScreenPoint(CanvasCamera,
            VirtualCursor.position);

        if (created)
            leftData.position = screenSpaceCursorPosition;

        Vector2 pos = screenSpaceCursorPosition;
        leftData.delta = pos - leftData.position;
        leftData.position = pos;
        leftData.scrollDelta = Input.mouseScrollDelta;
        leftData.button = PointerEventData.InputButton.Left;
        eventSystem.RaycastAll(leftData, m_RaycastResultCache);
        var raycast = FindFirstRaycast(m_RaycastResultCache);
        leftData.pointerCurrentRaycast = raycast;
        m_RaycastResultCache.Clear();

        // copy the apropriate data into right and middle slots
        PointerEventData rightData;
        GetPointerData(kMouseRightId, out rightData, true);
        CopyFromTo(leftData, rightData);
        rightData.button = PointerEventData.InputButton.Right;

        PointerEventData middleData;
        GetPointerData(kMouseMiddleId, out middleData, true);
        CopyFromTo(leftData, middleData);
        middleData.button = PointerEventData.InputButton.Middle;

        m_MouseState.SetButtonState(PointerEventData.InputButton.Left, StateForMouseButton(0), leftData);
        m_MouseState.SetButtonState(PointerEventData.InputButton.Right, StateForMouseButton(1), rightData);
        m_MouseState.SetButtonState(PointerEventData.InputButton.Middle, StateForMouseButton(2), middleData);

        return m_MouseState;
    }

    protected GameObject GetCurrentFocusedGameObject()
    {
        return m_CurrentFocusedGameObject;
    }
}

I’m not sure why you need a custom input module, perhaps I’m not understanding what you need. I gave you a working example in my previous post and it is kind of expected that you provide some feedback. I also have very little experience with UI Toolkit, if that’s what you’re using, so maybe someone else can share more info on that.

Now I’m looking at the repo, and it seems to me that, other than just wanting a software cursor, you also want to simulate input? Sure, introducing a custom input module should work, but you can also introduce your own mediator pattern to an existing input, and then everything reacts to that. I presume you want the whole UI system to react to this simulated environment on its own? In that case supplanting a module of your own makes sense.

Have you tried VirtualMouseInput?

Oh I’m not using the UI Toolkit, just the regular UI stuff.

Your post was talking about implementing the pointer input interface, examples I found online talked about modifying the actual input module as this is what handles the cursor position, unless I’m misunderstanding how you’re overriding the position?

I can’t see in your post or the links where the actual position is modified. I assume I still need to do this in order to use a canvas image’s anchor point as the location.

I’ve looked into the new input system, unfortunately everything in my project is using the old input system due to some legacy dependency on a custom control system I built back in the Unity 5 days and have just been building on since.

The first thing I tried was simply running both systems together but it caused a lot of conflicts and issues with input in general so I decided that probably wasn’t the way to go.

Hence why I’ve been trying to work out how to implement this myself, it would take far longer to port everything over to the new input system than it would be to get this working.

I though you just wanted to read the mouse position over canvas in order to provide a software cursor. However if you want to wire this pointer back to event system so that it natively reacts, then check out the second post (edit: sure, it’s the new input system, my bad). I must admit I never needed this, so I can’t help you more (from the top of my head). Can you explain your use case in more detail?

Basically I have an interactive element in my level (it’s a first-person mystery game) where the player can “dock” with a computer and use it.

Technically speaking, this “computer” is a Canvas hidden under the map set to Screen Space - Camera and rendered to a RenderTexture which is used for the computer monitor. There are interactable elements within this Canvas (a few buttons and a Scroll View for this one). I have an Image with a cursor sprite with the pivot set to the “click location” I want to use for interacting with the elements.

As a result of this, the mouse position would obviously not work for this due to the use of a RenderTexture (used so I can apply shader effects to the “monitor”).

Basically, I was hoping to use this cursor image as a replacement cursor to interact with the buttons and scroll view (I can always remove the scroll view if dragging is a no-go). The GitHub repo I linked to was something I’d used way back in the Unity 5 days, but it appears the pointer interactions have been changed somewhere along the line and no longer works in later versions.

Super edge case I know.