1
\$\begingroup\$

This question came from some of my students. I wanted to share it here so others can find & improve on the solutions we've found so far.

In particular, my answer below doesn't behave as well as I'd like in windowed / editor mode - UI buttons no longer respond to hovering the cursor. I welcome alternative answers that solve this more completely.


We're making a visual novel game using the Fungus asset for Unity.

We want to add a twist where some dialogue options are easier or harder to press, because they attract or repulse your mouse cursor.

Unfortunately, we haven't been able to identify a cross-platform way to manipulate the mouse's position, nor a way to create a "Fake" mouse cursor image that can still interact with Fungus's UI buttons.

How can we pull this off?

\$\endgroup\$
2
  • \$\begingroup\$ I can't code this right away but you could replace the mouse with an in game object, casting rays from that object that will act as mouse clicks and implementing the mouse movement as a physics interaction with the object (this will introduce some delay to the mouse movement or it might feel like you have negative mouse acceleration on). Then all you need to do is put a 2D point effector to wherever you want. (Or, just implement negative mouse acceleration and increase it the closer you get to some options) \$\endgroup\$ Commented Jan 15, 2018 at 11:54
  • \$\begingroup\$ @JohnHamilton Check the answer below — that's very similar to what we've already done, but doesn't solve the lingering issues with hover events in non-fullscreen mode. \$\endgroup\$
    – DMGregory
    Commented Jan 15, 2018 at 13:53

1 Answer 1

3
\$\begingroup\$

We can take complete control of how Unity handles cursor events by providing our own InputModule.

Start by finding the EventSystem in your scene, and removing any existing InputModules from it, and adding a custom one instead (based on a sample by OpticalOverride). This replaces the native mouse cursor with our own fake cursor for the purpose of all UI events. In particular, clicks from the fake cursor will trigger Fungus dialogue options under it.

using UnityEngine;
using UnityEngine.EventSystems;

public class FalseCursorInputModule : StandaloneInputModule {

    public RectTransform cursorVisual;
    public float cursorSpeed = 2f;

    Vector2 m_cursorPos;
    private readonly MouseState m_MouseState = new MouseState();

    RectTransform m_canvas;    

    protected override void Start() {
        OnApplicationFocus(Application.isFocused);
        m_canvas = (RectTransform)cursorVisual.parent;
        m_cursorPos = new Vector2(Screen.width, Screen.height) / 2f;
    }

    // Lock & hide the real mouse cursor.
    private void OnApplicationFocus(bool focus) {
        if (Screen.fullScreen) {
            // In full-screen, this works perfectly.
            // Out of full screen, click events don't work.
            Cursor.lockState = CursorLockMode.Confined;
            Cursor.visible = false;
            return;
        } else {
            // This works as a fallback, but we don't get hover events.
            Cursor.lockState = CursorLockMode.Locked;
        }
    }

    // Intercept mouse events and use them to drive our virtual cursor instead.
    protected override MouseState GetMousePointerEventData(int id = 0) {
        MouseState m = new MouseState();

        // Populate the left button...
        PointerEventData leftData;
        var created = GetPointerData(kMouseLeftId, out leftData, true);
        leftData.Reset();
        if (created)
            leftData.position = m_cursorPos;

        // Add the relative movement of the mouse to our virtual cursor position.
        Vector2 delta = new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"));
        m_cursorPos = m_cursorPos + cursorSpeed * delta;

        // Translate to canvas space for handling repulsion & positioning visuals.
        Vector2 scale = new Vector2(m_canvas.rect.width / Screen.width, m_canvas.rect.height / Screen.height);

        Vector2 placedPosition = Vector2.Scale(m_cursorPos, scale);

        foreach (var repulsor in FindObjectsOfType<CursorRepulsor>())
            placedPosition = repulsor.Repulse(placedPosition);

        placedPosition.x = Mathf.Clamp(placedPosition.x, 0, m_canvas.rect.width);
        placedPosition.y = Mathf.Clamp(placedPosition.y, 0, m_canvas.rect.height);

        cursorVisual.anchoredPosition = placedPosition;

        // Translate back to screen space for UI raycasting.
        m_cursorPos = placedPosition;
        m_cursorPos.x /= scale.x;
        m_cursorPos.y /= scale.y;

        // Finish setting up left mouse event data.
        leftData.delta = m_cursorPos - leftData.position;
        leftData.position = m_cursorPos;
        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;
    }
}

Your CursorVisual should be an image on a canvas set to the same size as you've configured Fungus to use, with a sortOrder of 2 so it sits above any menu graphics. The image should be anchored to the bottom-left to make the anchoredPosition code above work as expected.

You can add additional objects to this canvas to be your repulsion / attraction loci. Here's one way we might write that script:

public class CursorRepulsor : MonoBehaviour {
    [Tooltip("Farthest distance at which the repulsor has an effect")];
    public float limitRadius = 300;
    [Tooltip("Distance at which the repulsor achieves its maximum effect")];
    public float peakRadius = 100;

    [Tooltip("Strength of the push - use a negative number to attract")];
    public float strength = 5;

    public Vector2 Repulse(Vector2 cursorPosition) {
        Vector2 me = ((RectTransform)transform).anchoredPosition;
        Vector2 away = cursorPosition - me;

        float distance = away.magnitude;
        if(distance > limitRadius)
            return cursorPosition;

        float intensity = Mathf.Clamp01((limitRadius - distance) / (limitRadius - peakRadius));

        float repulse = strength * intensity;

        return cursorPosition + away * repulse / distance;
    }
}

Placing these repulsors in the appropriate locations for your Fungus scene is left as an exercise for the reader / someone with more Fungus experience than I have thus far. ;)

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .