using AibisDream.Framework; using DG.Tweening; using System.Collections; using TMPro; using UnityEngine; using UnityEngine.Serialization; namespace AibisDream { public class MouseFollowAndZoom : MonoBehaviour, IInteraction { public GameObject targetIndicator; public TMP_Text targetNameText; public Vector2 sceneBoundsMin; public Vector2 sceneBoundsMax; [Header("Cinemachine Pan")] [Tooltip("平移目标;留空则平移本物体。Eye 模块应指向 Eye DeepCamera 虚拟相机 Transform。")] [SerializeField] private Transform panTransform; [Tooltip("虚拟相机可移动的世界区域(与 ViewCamera / EyeSystem 聚焦边界一致)。")] [SerializeField] private Vector2 worldRegionMin = new(30.2f, -5f); [SerializeField] private Vector2 worldRegionMax = new(50.2f, 5f); [Header("Input")] [SerializeField] private float mouseScreenOffsetX; [FormerlySerializedAs("viewCamera")] [SerializeField] private Camera legacyInputCamera; public float parallaxEffectMultiplier = 0.1f; public Vector2 maxOffset = new Vector2(2f, 2f); [Header("Interaction")] [SerializeField] private bool _isInteractionActive = true; [SerializeField] private bool _isInteractionAvailable = true; [SerializeField] private float _dialogEndCooldown = 0.1f; private EyeSystem eyemanager; private EyeTarget currentTarget; private bool isActive; private bool isLocked; private float _lastDialogEndTime = -1f; private bool _isDialogCooldownActive; private Vector3 initialPosition; private Vector3 currentOffset; private Tween focusTween; private Transform PanTransform => panTransform != null ? panTransform : transform; private bool UsesCinemachinePan => panTransform != null; public Vector3 ViewPosition { get { Camera cameraForInput = GetInputCamera(); return cameraForInput != null ? cameraForInput.transform.position : PanTransform.position; } } public void SetIsActive(bool value) { isActive = value; } public void SetIsLock(bool value) { isLocked = value; } public void SetInteractionActive(bool value) { _isInteractionActive = value; } public void SetInteractionAvailable(bool value) { _isInteractionAvailable = value; } public void ResetPanOrigin() { focusTween?.Kill(); currentOffset = Vector3.zero; initialPosition = PanTransform.position; } public Tween MoveFocusTo(Transform target, float duration, Vector2 minBounds, Vector2 maxBounds, Vector2 viewportSize) { if (target == null) { return null; } Vector3 focusPosition = ClampFocusPosition(target.position, minBounds, maxBounds, viewportSize); Vector3 targetPanPosition; if (UsesCinemachinePan) { targetPanPosition = ClampFocusPosition( new Vector3(focusPosition.x, focusPosition.y, PanTransform.position.z), minBounds, maxBounds, viewportSize); } else { Vector3 viewPosition = ViewPosition; viewPosition.z = focusPosition.z; targetPanPosition = PanTransform.position + viewPosition - focusPosition; targetPanPosition.z = PanTransform.position.z; } initialPosition = targetPanPosition; focusTween?.Kill(); if (duration <= 0f) { PanTransform.position = initialPosition + currentOffset; focusTween = null; return null; } focusTween = PanTransform.DOMove(initialPosition + currentOffset, duration).SetEase(Ease.InOutSine); return focusTween; } public IEnumerator WaitForFocusMove(float fallbackDuration = 0f) { if (focusTween != null && focusTween.IsActive()) { yield return focusTween.WaitForCompletion(); yield break; } if (fallbackDuration > 0f) { yield return new WaitForSeconds(fallbackDuration); } } private void Start() { initialPosition = PanTransform.position; targetIndicator.SetActive(false); eyemanager = FindObjectOfType(); if (eyemanager == null) { Debug.LogError("[MouseFollowAndZoom] EyeSystem 未找到。", this); } RegisterDialogEvents(); } private void Update() { if (!isActive) { targetIndicator.SetActive(false); return; } UpdateDialogCooldown(); HandleTargetIndicator(); if (isLocked) { return; } HandleMouseMovement(); UpdateCurrentTarget(); } private void HandleMouseMovement() { Camera cameraForInput = GetInputCamera(); if (cameraForInput == null) { return; } Vector3 mousePosition = GetMouseScreenPosition(); Vector3 worldMousePosition = cameraForInput.ScreenToWorldPoint(mousePosition); worldMousePosition.z = 0; // 视差锚点用 initialPosition,避免相机跟随平移后 delta 被抵消 Vector3 parallaxAnchor = UsesCinemachinePan ? initialPosition : ViewPosition; parallaxAnchor.z = 0; Vector3 delta = UsesCinemachinePan ? worldMousePosition - parallaxAnchor : parallaxAnchor - worldMousePosition; Vector3 targetOffset = delta * parallaxEffectMultiplier; targetOffset.x = Mathf.Clamp(targetOffset.x, -maxOffset.x, maxOffset.x); targetOffset.y = Mathf.Clamp(targetOffset.y, -maxOffset.y, maxOffset.y); currentOffset = Vector3.Lerp(currentOffset, targetOffset, 0.01f); Vector3 targetPosition = initialPosition + currentOffset; if (UsesCinemachinePan) { Vector2 parallaxSlack = maxOffset * parallaxEffectMultiplier; targetPosition = ClampFocusPositionWithSlack( targetPosition, worldRegionMin, worldRegionMax, GetViewportSize(), parallaxSlack); } else { targetPosition.x = Mathf.Clamp(targetPosition.x, sceneBoundsMin.x, sceneBoundsMax.x); targetPosition.y = Mathf.Clamp(targetPosition.y, sceneBoundsMin.y, sceneBoundsMax.y); } targetPosition.z = PanTransform.position.z; PanTransform.position = Vector3.Lerp(PanTransform.position, targetPosition, 0.1f); } private void UpdateCurrentTarget() { Camera cameraForInput = GetInputCamera(); if (cameraForInput == null) { currentTarget = null; return; } Vector3 worldPosition = cameraForInput.ScreenToWorldPoint(GetMouseScreenPosition()); worldPosition.z = 0; RaycastHit2D hit = Physics2D.Raycast(worldPosition, Vector2.zero); if (hit.collider != null && hit.collider.CompareTag("EyeTarget")) { currentTarget = hit.collider.GetComponent(); if (Input.GetMouseButtonDown(0) && _isInteractionActive && _isInteractionAvailable && !_isDialogCooldownActive) { if (currentTarget != null) { eyemanager.SetTarget(currentTarget); } else { Debug.LogWarning("[MouseFollowAndZoom] Collider 缺少 EyeTarget 组件。", hit.collider); } } } else { currentTarget = null; } } private Vector3 GetMouseScreenPosition() { Vector3 mousePosition = Input.mousePosition; mousePosition.x -= mouseScreenOffsetX; mousePosition.z = GetInputCamera() != null ? GetInputCamera().nearClipPlane : mousePosition.z; return mousePosition; } private void HandleTargetIndicator() { if (currentTarget == null || !_isInteractionActive) { targetIndicator.SetActive(false); return; } BoxCollider2D boxCollider = currentTarget.GetComponent(); if (boxCollider == null) { return; } Vector3 boxCenter = boxCollider.bounds.center; Vector3 boxSize = boxCollider.bounds.size; RectTransform indicatorRectTransform = targetIndicator.GetComponent(); indicatorRectTransform.position = boxCenter; indicatorRectTransform.sizeDelta = new Vector2(boxSize.x, boxSize.y); Color indicatorColor = currentTarget.GetIndicatorColor(); targetNameText.text = currentTarget.name + " " + "R" + indicatorColor.r.ToString("F1") + " G" + indicatorColor.g.ToString("F1") + " B" + indicatorColor.b.ToString("F1"); targetIndicator.SetActive(true); } public void SetIndicatorTarget(EyeTarget target) { currentTarget = target; } private Camera GetInputCamera() { if (UsesCinemachinePan && CameraKit.Instance != null) { return CameraKit.Instance.GetCurrentCamera(); } if (legacyInputCamera != null) { return legacyInputCamera; } return Camera.main; } private Vector2 GetViewportSize() { Camera cam = GetInputCamera(); if (cam == null || !cam.orthographic) { return Vector2.zero; } float height = cam.orthographicSize * 2f; return new Vector2(height * cam.aspect, height); } private static Vector3 ClampFocusPositionWithSlack( Vector3 targetPosition, Vector2 minBounds, Vector2 maxBounds, Vector2 viewportSize, Vector2 slack) { if (maxBounds.x > minBounds.x) { float halfWidth = Mathf.Max(0f, viewportSize.x * 0.5f); targetPosition.x = Mathf.Clamp( targetPosition.x, minBounds.x + halfWidth - slack.x, maxBounds.x - halfWidth + slack.x); } if (maxBounds.y > minBounds.y) { float halfHeight = Mathf.Max(0f, viewportSize.y * 0.5f); targetPosition.y = Mathf.Clamp( targetPosition.y, minBounds.y + halfHeight - slack.y, maxBounds.y - halfHeight + slack.y); } return targetPosition; } private static Vector3 ClampFocusPosition(Vector3 targetPosition, Vector2 minBounds, Vector2 maxBounds, Vector2 viewportSize) { if (maxBounds.x > minBounds.x) { float halfWidth = Mathf.Max(0f, viewportSize.x * 0.5f); targetPosition.x = Mathf.Clamp(targetPosition.x, minBounds.x + halfWidth, maxBounds.x - halfWidth); } if (maxBounds.y > minBounds.y) { float halfHeight = Mathf.Max(0f, viewportSize.y * 0.5f); targetPosition.y = Mathf.Clamp(targetPosition.y, minBounds.y + halfHeight, maxBounds.y - halfHeight); } return targetPosition; } private void OnDrawGizmos() { Gizmos.color = Color.red; Vector2 min = UsesCinemachinePan ? worldRegionMin : sceneBoundsMin; Vector2 max = UsesCinemachinePan ? worldRegionMax : sceneBoundsMax; Vector3 bottomLeft = new Vector3(min.x, min.y, 0); Vector3 topRight = new Vector3(max.x, max.y, 0); Gizmos.DrawLine(bottomLeft, new Vector3(bottomLeft.x, topRight.y, 0)); Gizmos.DrawLine(bottomLeft, new Vector3(topRight.x, bottomLeft.y, 0)); Gizmos.DrawLine(topRight, new Vector3(bottomLeft.x, topRight.y, 0)); Gizmos.DrawLine(topRight, new Vector3(topRight.x, bottomLeft.y, 0)); } private void RegisterDialogEvents() { EnumEventSystem.Global.Register(InteractionEventEnum.DialogEnd, OnDialogueComplete); } private void OnDialogueComplete() { _lastDialogEndTime = Time.time; _isDialogCooldownActive = true; } private void UpdateDialogCooldown() { if (_isDialogCooldownActive && Time.time - _lastDialogEndTime >= _dialogEndCooldown) { _isDialogCooldownActive = false; } } private void OnDestroy() { focusTween?.Kill(); EnumEventSystem.Global.UnRegister(InteractionEventEnum.DialogEnd, OnDialogueComplete); } public bool IsActive => _isInteractionActive && !_isDialogCooldownActive; public bool IsAvailable => _isInteractionAvailable && !_isDialogCooldownActive; public GameObject GetGameObject() { return gameObject; } } }