diff --git a/Assets/Scripts/FixSystem/Effect/MouseFollowEffect.cs b/Assets/Scripts/FixSystem/Effect/MouseFollowEffect.cs index 3f9280817..fe72e41a9 100644 --- a/Assets/Scripts/FixSystem/Effect/MouseFollowEffect.cs +++ b/Assets/Scripts/FixSystem/Effect/MouseFollowEffect.cs @@ -1,39 +1,61 @@ -using UnityEngine; -using TMPro; using AibisDream.Framework; +using DG.Tweening; +using TMPro; +using UnityEngine; +using UnityEngine.Serialization; namespace AibisDream { public class MouseFollowAndZoom : MonoBehaviour, IInteraction { - public GameObject targetIndicator; // UI 目标框 - public TMP_Text targetNameText; // 显示目标名称的文本 - public Vector2 sceneBoundsMin; // 场景边界的左下角 - public Vector2 sceneBoundsMax; // 场景边界的右上角 - private EyeSystem eyemanager; - private EyeTarget currentTarget; // 当前目标 - private bool isActive = false; - private bool isLocked = false; - - // 交互状态管理 - [Header("交互状态")] - [SerializeField] private bool _isInteractionActive = true; // 是否激活交互 - [SerializeField] private bool _isInteractionAvailable = true; // 是否可用交互 - [SerializeField] private float _dialogEndCooldown = 0.1f; // 对话结束后的冷却时间 - - // 内部状态 - private float _lastDialogEndTime = -1f; // 上次对话结束的时间 - private bool _isDialogCooldownActive = false; // 是否在对话冷却期间 + 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 Camera viewCamera; 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 float viewOffset = 0; + 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; @@ -43,152 +65,212 @@ namespace AibisDream { isLocked = value; } - - // 交互状态控制方法 + public void SetInteractionActive(bool value) { _isInteractionActive = value; } - + public void SetInteractionAvailable(bool value) { _isInteractionAvailable = value; } - - void Start() + public void ResetPanOrigin() { - initialPosition = transform.position; - targetIndicator.SetActive(false); // 初始时隐藏目标框 + focusTween?.Kill(); + currentOffset = Vector3.zero; + initialPosition = PanTransform.position; + } + + public void MoveFocusTo(Transform target, float duration, Vector2 minBounds, Vector2 maxBounds, Vector2 viewportSize) + { + if (target == null) + { + return; + } + + 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; + return; + } + + focusTween = PanTransform.DOMove(initialPosition + currentOffset, duration).SetEase(Ease.InOutSine); + } + + private void Start() + { + initialPosition = PanTransform.position; + targetIndicator.SetActive(false); eyemanager = FindObjectOfType(); if (eyemanager == null) { - Debug.LogError("EyeManager instance not found in the scene."); + Debug.LogError("[MouseFollowAndZoom] EyeSystem 未找到。", this); } - - // 注册对话事件以处理对话结束后的冷却 + RegisterDialogEvents(); } - void Update() + private void Update() { if (!isActive) { targetIndicator.SetActive(false); return; } - - // 检查对话结束冷却 - UpdateDialogCooldown(); - HandleTargetIndicator(); // 显示指示器 - if (isLocked) return; + UpdateDialogCooldown(); + HandleTargetIndicator(); + + if (isLocked) + { + return; + } + HandleMouseMovement(); - UpdateCurrentTarget(); // 更新当前目标 + UpdateCurrentTarget(); } - private Vector3 currentOffset; // 当前的偏移量 - - void HandleMouseMovement() + private void HandleMouseMovement() { - // 获取鼠标位置 - Vector3 mousePosition = Input.mousePosition; - mousePosition.x -= 40f; // 为x坐标加上40.3 - Vector3 worldMousePosition = viewCamera.ScreenToWorldPoint(mousePosition); - worldMousePosition.z = 0; // 确保 z 为 0 + Camera cameraForInput = GetInputCamera(); + if (cameraForInput == null) + { + return; + } - // 计算目标偏移量并反向 - Vector3 targetOffset = (viewCamera.transform.position - worldMousePosition) * parallaxEffectMultiplier; + 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); - // 平滑处理偏移量 - float smoothFactor = 0.01f; // 平滑系数,值越小平滑效果越强,响应越慢 - currentOffset = Vector3.Lerp(currentOffset, targetOffset, smoothFactor); + 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.x = Mathf.Clamp(targetPosition.x, sceneBoundsMin.x, sceneBoundsMax.x); - targetPosition.y = Mathf.Clamp(targetPosition.y, sceneBoundsMin.y, sceneBoundsMax.y); + targetPosition.z = PanTransform.position.z; - // 使用插值(Lerp)平滑更新位置 - float followSpeed = 0.1f; // 跟随速度,值越小延迟越大 - Vector3 smoothedPosition = Vector3.Lerp(transform.position, targetPosition, followSpeed); - - // 应用平滑后的位置 - transform.position = smoothedPosition; + PanTransform.position = Vector3.Lerp(PanTransform.position, targetPosition, 0.1f); } - void UpdateCurrentTarget() + private void UpdateCurrentTarget() { - // 获取鼠标位置 - Vector3 mousePosition = Input.mousePosition; - mousePosition.x -= 40f; // 为x坐标加上40.3 - Vector3 worldPosition = Camera.main.ScreenToWorldPoint(mousePosition); - RaycastHit2D hit = - Physics2D.Raycast(worldPosition + viewCamera.transform.position - Camera.main.transform.position, - Vector2.zero); + 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(); // 更新当前目标 - - // 处理鼠标点击事件 - 只有在交互可用且不在对话冷却期时才处理 + currentTarget = hit.collider.GetComponent(); if (Input.GetMouseButtonDown(0) && _isInteractionActive && _isInteractionAvailable && !_isDialogCooldownActive) { - Debug.Log("点击目标触发了"); if (currentTarget != null) { - eyemanager.SetTarget(currentTarget); // 设置目标 + eyemanager.SetTarget(currentTarget); } else { - Debug.LogWarning("The collider does not have an EyeTarget component."); + Debug.LogWarning("[MouseFollowAndZoom] Collider 缺少 EyeTarget 组件。", hit.collider); } } } else { - currentTarget = null; // 如果没有目标,设置为 null + currentTarget = null; } } - void HandleTargetIndicator() + 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); // 隐藏指示器 + targetIndicator.SetActive(false); return; } - // 获取目标的 BoxCollider2D BoxCollider2D boxCollider = currentTarget.GetComponent(); - if (boxCollider != null) + if (boxCollider == null) { - Vector3 boxCenter = boxCollider.bounds.center; - Vector3 boxSize = boxCollider.bounds.size; - - // 转换为世界空间坐标 - Vector3 worldCenter = boxCenter; - Vector3 worldSize = boxSize; - - // 设置目标框的位置 - RectTransform indicatorRectTransform = targetIndicator.GetComponent(); - indicatorRectTransform.position = worldCenter; - indicatorRectTransform.sizeDelta = new Vector2(worldSize.x, worldSize.y); - - // 设置目标名称 - targetNameText.text = currentTarget.name + " " + - "R" + currentTarget._color.r.ToString("F1") + - " G" + currentTarget._color.g.ToString("F1") + - " B" + currentTarget._color.b.ToString("F1"); - - // 显示指示器 - targetIndicator.SetActive(true); + 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); + + targetNameText.text = currentTarget.name + " " + + "R" + currentTarget._color.r.ToString("F1") + + " G" + currentTarget._color.g.ToString("F1") + + " B" + currentTarget._color.b.ToString("F1"); + + targetIndicator.SetActive(true); } public void SetIndicatorTarget(EyeTarget target) @@ -196,85 +278,123 @@ namespace AibisDream currentTarget = target; } - void OnDrawGizmos() + 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 颜色 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); - // 绘制场景边界 - Vector3 bottomLeft = new Vector3(sceneBoundsMin.x, sceneBoundsMin.y, 0); - Vector3 topRight = new Vector3(sceneBoundsMax.x, sceneBoundsMax.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)); } - - #region 对话冷却处理 - - /// - /// 注册对话事件 - /// + private void RegisterDialogEvents() { - // 监听对话结束事件 EnumEventSystem.Global.Register(InteractionEventEnum.DialogEnd, OnDialogueComplete); } - - /// - /// 对话结束时的处理 - /// + private void OnDialogueComplete() { _lastDialogEndTime = Time.time; _isDialogCooldownActive = true; - Debug.Log($"[交互冷却] 对话结束,开始 {_dialogEndCooldown} 秒冷却期"); } - - /// - /// 更新对话冷却状态 - /// + private void UpdateDialogCooldown() { if (_isDialogCooldownActive && Time.time - _lastDialogEndTime >= _dialogEndCooldown) { _isDialogCooldownActive = false; - Debug.Log("[交互冷却] 冷却期结束,交互恢复正常"); } } - - /// - /// 销毁时取消事件注册 - /// + private void OnDestroy() { + focusTween?.Kill(); EnumEventSystem.Global.UnRegister(InteractionEventEnum.DialogEnd, OnDialogueComplete); } - - #endregion - - #region IInteraction 接口实现 - - /// - /// 判断是否激活,激活就正常显示鼠标,否则就显示禁用标志 - /// + public bool IsActive => _isInteractionActive && !_isDialogCooldownActive; - - /// - /// 判断是否可用,可用就正常显示鼠标,否则就保持原有标志不变 - /// public bool IsAvailable => _isInteractionAvailable && !_isDialogCooldownActive; - - /// - /// 返回GameObject - /// + public GameObject GetGameObject() { return gameObject; } - - #endregion } -} \ No newline at end of file +}