From fbbf5a1fe75f78aa424c0c6a86830e520b6997e6 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 24 Mar 2026 20:45:43 +0900 Subject: [PATCH 1/2] Add click/release animation to mouse cursor during input replay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InputReplayer updated SimulateMouseUiOverlayState but never triggered the expand (1.5x→1x shrink) or dissipate (fade-out) cursor animations that SimulateMouseUiTool plays on click/release. This made the cursor appear static during replay, unlike the interactive tool experience. - Add frame-based self-animation to SimulateMouseUiOverlay (LateUpdate-driven) - Add animation request facade to SimulateMouseUiOverlayState so InputReplayer triggers animations without traversing the OverlayCanvasFactory object chain - Extract shared animation constants into SimulateMouseUiAnimationConstants - SetCursorScale/SetAlpha cancel self-animation to avoid conflicts with SimulateMouseUiTool's async-driven animation --- .../Api/McpTools/ReplayInput/InputReplayer.cs | 2 + .../SimulateMouseUi/SimulateMouseUiTool.cs | 6 +- .../SimulateMouseUiAnimationConstants.cs | 9 +++ .../SimulateMouseUiAnimationConstants.cs.meta | 11 ++++ .../SimulateMouseUi/SimulateMouseUiOverlay.cs | 56 +++++++++++++++++++ .../SimulateMouseUiOverlayState.cs | 36 ++++++++++++ 6 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiAnimationConstants.cs create mode 100644 Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiAnimationConstants.cs.meta diff --git a/Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs b/Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs index 424636809..ab7995ca8 100644 --- a/Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs +++ b/Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs @@ -466,6 +466,7 @@ private static void ApplyUiEvents() OnUiPointerDown(screenPos, eventSystem); SimulateMouseUiOverlayState.Update( MouseAction.Click, inputPos, null, _currentPressTarget?.name, gameViewSize); + SimulateMouseUiOverlayState.RequestExpandAnimation(); } else if (leftHeld && (_currentPressTarget != null || _currentDragTarget != null)) { @@ -497,6 +498,7 @@ private static void ApplyUiEvents() if (justReleased) { OnUiPointerUp(screenPos, eventSystem); + SimulateMouseUiOverlayState.RequestDissipateAnimation(); SimulateMouseUiOverlayState.Clear(); } } diff --git a/Packages/src/Editor/Api/McpTools/SimulateMouseUi/SimulateMouseUiTool.cs b/Packages/src/Editor/Api/McpTools/SimulateMouseUi/SimulateMouseUiTool.cs index d85b3ede4..c1bf14565 100644 --- a/Packages/src/Editor/Api/McpTools/SimulateMouseUi/SimulateMouseUiTool.cs +++ b/Packages/src/Editor/Api/McpTools/SimulateMouseUi/SimulateMouseUiTool.cs @@ -15,9 +15,9 @@ public class SimulateMouseUiTool : AbstractUnityTool "simulate-mouse-ui"; - private const float EXPAND_DURATION = 0.1f; - private const float EXPAND_START_SCALE = 1.5f; - private const float DISSIPATE_DURATION = 0.1f; + private const float EXPAND_DURATION = SimulateMouseUiAnimationConstants.EXPAND_DURATION; + private const float EXPAND_START_SCALE = SimulateMouseUiAnimationConstants.EXPAND_START_SCALE; + private const float DISSIPATE_DURATION = SimulateMouseUiAnimationConstants.DISSIPATE_DURATION; protected override async Task ExecuteAsync( SimulateMouseUiSchema parameters, diff --git a/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiAnimationConstants.cs b/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiAnimationConstants.cs new file mode 100644 index 000000000..d23eea5ca --- /dev/null +++ b/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiAnimationConstants.cs @@ -0,0 +1,9 @@ +namespace io.github.hatayama.uLoopMCP +{ + public static class SimulateMouseUiAnimationConstants + { + public const float EXPAND_DURATION = 0.1f; + public const float EXPAND_START_SCALE = 1.5f; + public const float DISSIPATE_DURATION = 0.1f; + } +} diff --git a/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiAnimationConstants.cs.meta b/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiAnimationConstants.cs.meta new file mode 100644 index 000000000..fcfabd5d7 --- /dev/null +++ b/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiAnimationConstants.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b2a42b98d6124b0a828dd71a039d401 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlay.cs b/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlay.cs index 4582df688..53439b328 100644 --- a/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlay.cs +++ b/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlay.cs @@ -27,6 +27,14 @@ public class SimulateMouseUiOverlay : MonoBehaviour private readonly List _pathSegments = new List(); private readonly List _waypointMarkers = new List(); + private const float SELF_EXPAND_DURATION = SimulateMouseUiAnimationConstants.EXPAND_DURATION; + private const float SELF_EXPAND_START_SCALE = SimulateMouseUiAnimationConstants.EXPAND_START_SCALE; + private const float SELF_DISSIPATE_DURATION = SimulateMouseUiAnimationConstants.DISSIPATE_DURATION; + + private enum SelfAnimState { None, Expanding, Dissipating } + private SelfAnimState _selfAnimState = SelfAnimState.None; + private float _selfAnimStartTime; + private void Awake() { Debug.Assert(_canvasGroup != null, "_canvasGroup must be assigned in prefab"); @@ -43,6 +51,23 @@ private void Awake() private void LateUpdate() { + ConsumePendingAnimationRequests(); + + // Dissipate runs independently of overlay state (state is already cleared on release) + if (_selfAnimState == SelfAnimState.Dissipating) + { + float elapsed = Time.realtimeSinceStartup - _selfAnimStartTime; + float t = Mathf.Clamp01(elapsed / SELF_DISSIPATE_DURATION); + _cursorGroup.localScale = Vector3.one * Mathf.Lerp(1f, 0f, t); + _canvasGroup.alpha = Mathf.Lerp(1f, 0f, t); + if (t >= 1f) + { + _selfAnimState = SelfAnimState.None; + _canvasGroup.alpha = 0f; + } + return; + } + if (!SimulateMouseUiOverlayState.IsActive) { _canvasGroup.alpha = 0; @@ -57,19 +82,50 @@ private void LateUpdate() _cursorGroup.localScale = Vector3.one; } + if (_selfAnimState == SelfAnimState.Expanding) + { + float elapsed = Time.realtimeSinceStartup - _selfAnimStartTime; + float t = Mathf.Clamp01(elapsed / SELF_EXPAND_DURATION); + _cursorGroup.localScale = Vector3.one * Mathf.Lerp(SELF_EXPAND_START_SCALE, 1f, t); + if (t >= 1f) + { + _selfAnimState = SelfAnimState.None; + } + } + UpdateCursorPosition(); UpdateCursorMode(); UpdateDragPath(); } + private void ConsumePendingAnimationRequests() + { + if (SimulateMouseUiOverlayState.ConsumePendingExpandAnimation()) + { + _canvasGroup.alpha = 1f; + _cursorGroup.localScale = Vector3.one * SELF_EXPAND_START_SCALE; + _selfAnimState = SelfAnimState.Expanding; + _selfAnimStartTime = Time.realtimeSinceStartup; + } + + if (SimulateMouseUiOverlayState.ConsumePendingDissipateAnimation()) + { + _selfAnimState = SelfAnimState.Dissipating; + _selfAnimStartTime = Time.realtimeSinceStartup; + } + } + public void SetCursorScale(float scale) { _cursorGroup.localScale = Vector3.one * scale; + // SimulateMouseUiTool drives animation externally; cancel any self-driven animation + _selfAnimState = SelfAnimState.None; } public void SetAlpha(float alpha) { _canvasGroup.alpha = alpha; + _selfAnimState = SelfAnimState.None; } // sim coordinate: simX = screen pixel X, simY = EditorScreen.height - canvasY (top-left origin) diff --git a/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlayState.cs b/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlayState.cs index 6cffc907e..797f3e0e2 100644 --- a/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlayState.cs +++ b/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlayState.cs @@ -17,6 +17,10 @@ public static class SimulateMouseUiOverlayState public static float LongPressElapsed { get; private set; } + // Animation request flags survive Clear() — they are consumed by the overlay in LateUpdate + private static bool _pendingExpandAnimation; + private static bool _pendingDissipateAnimation; + private const int MAX_DRAG_WAYPOINTS = 4; private static readonly List _dragWaypoints = new List(); @@ -67,6 +71,38 @@ public static void AddWaypoint(Vector2 position) _dragWaypoints.Add(position); } + public static void RequestExpandAnimation() + { + _pendingExpandAnimation = true; + } + + public static void RequestDissipateAnimation() + { + _pendingDissipateAnimation = true; + } + + public static bool ConsumePendingExpandAnimation() + { + if (!_pendingExpandAnimation) + { + return false; + } + + _pendingExpandAnimation = false; + return true; + } + + public static bool ConsumePendingDissipateAnimation() + { + if (!_pendingDissipateAnimation) + { + return false; + } + + _pendingDissipateAnimation = false; + return true; + } + public static void Clear() { IsActive = false; From 86e49db01ad43c61c17e3edfa940e884205878f9 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 24 Mar 2026 20:58:42 +0900 Subject: [PATCH 2/2] Suppress idle overlay reactivation after mouse release during input replay RequestDissipateAnimation() on mouse-up was immediately cancelled by the next idle frame calling SimulateMouseUiOverlayState.Update(), making the release animation invisible. Add _suppressIdleUiOverlay flag to skip idle overlay updates until the pointer actually moves to a new position. --- .../Api/McpTools/ReplayInput/InputReplayer.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs b/Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs index ab7995ca8..8f816dfd5 100644 --- a/Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs +++ b/Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs @@ -39,6 +39,8 @@ internal static class InputReplayer // Mouse.current state, so UI interactions must go through ExecuteEvents directly. private static bool _hasMousePosition; private static bool _prevLeftButtonHeld; + private static Vector2? _previousReplayMousePosition; + private static bool _suppressIdleUiOverlay; private static PointerEventData? _pointerData; private static GameObject? _currentPressTarget; private static GameObject? _currentDragTarget; @@ -452,6 +454,10 @@ private static void ApplyUiEvents() } Vector2 screenPos = _replayMousePosition.Value; + bool mouseMoved = !_previousReplayMousePosition.HasValue + || _previousReplayMousePosition.Value != screenPos; + _previousReplayMousePosition = screenPos; + bool leftHeld = _replayHeldButtons.Contains(MouseButton.Left); bool justPressed = leftHeld && !_prevLeftButtonHeld; bool justReleased = !leftHeld && _prevLeftButtonHeld; @@ -462,6 +468,7 @@ private static void ApplyUiEvents() if (justPressed) { + _suppressIdleUiOverlay = false; _pressTime = Time.realtimeSinceStartup; OnUiPointerDown(screenPos, eventSystem); SimulateMouseUiOverlayState.Update( @@ -489,8 +496,11 @@ private static void ApplyUiEvents() } } } - else + else if (!_suppressIdleUiOverlay || mouseMoved) { + // Keeping the overlay hidden until the pointer actually moves prevents release fade-out + // from being cancelled by the next idle frame at the same position. + _suppressIdleUiOverlay = false; SimulateMouseUiOverlayState.Update( MouseAction.Click, inputPos, null, null, gameViewSize); } @@ -498,6 +508,7 @@ private static void ApplyUiEvents() if (justReleased) { OnUiPointerUp(screenPos, eventSystem); + _suppressIdleUiOverlay = true; SimulateMouseUiOverlayState.RequestDissipateAnimation(); SimulateMouseUiOverlayState.Clear(); } @@ -644,7 +655,9 @@ private static bool DetectMousePositionEvents(InputRecordingData data) private static void ResetUiReplayState() { _replayMousePosition = null; + _previousReplayMousePosition = null; _prevLeftButtonHeld = false; + _suppressIdleUiOverlay = false; _pointerData = null; _currentPressTarget = null; _currentDragTarget = null;