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. ;)