refactor(peipei-eye): 视觉模块平移改用 Cinemachine 锚点
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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<EyeSystem>();
|
||||
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<EyeTarget>(); // 更新当前目标
|
||||
|
||||
// 处理鼠标点击事件 - 只有在交互可用且不在对话冷却期时才处理
|
||||
currentTarget = hit.collider.GetComponent<EyeTarget>();
|
||||
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<BoxCollider2D>();
|
||||
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<RectTransform>();
|
||||
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<RectTransform>();
|
||||
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 对话冷却处理
|
||||
|
||||
/// <summary>
|
||||
/// 注册对话事件
|
||||
/// </summary>
|
||||
|
||||
private void RegisterDialogEvents()
|
||||
{
|
||||
// 监听对话结束事件
|
||||
EnumEventSystem.Global.Register(InteractionEventEnum.DialogEnd, OnDialogueComplete);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对话结束时的处理
|
||||
/// </summary>
|
||||
|
||||
private void OnDialogueComplete()
|
||||
{
|
||||
_lastDialogEndTime = Time.time;
|
||||
_isDialogCooldownActive = true;
|
||||
Debug.Log($"[交互冷却] 对话结束,开始 {_dialogEndCooldown} 秒冷却期");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新对话冷却状态
|
||||
/// </summary>
|
||||
|
||||
private void UpdateDialogCooldown()
|
||||
{
|
||||
if (_isDialogCooldownActive && Time.time - _lastDialogEndTime >= _dialogEndCooldown)
|
||||
{
|
||||
_isDialogCooldownActive = false;
|
||||
Debug.Log("[交互冷却] 冷却期结束,交互恢复正常");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 销毁时取消事件注册
|
||||
/// </summary>
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
focusTween?.Kill();
|
||||
EnumEventSystem.Global.UnRegister(InteractionEventEnum.DialogEnd, OnDialogueComplete);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IInteraction 接口实现
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否激活,激活就正常显示鼠标,否则就显示禁用标志
|
||||
/// </summary>
|
||||
|
||||
public bool IsActive => _isInteractionActive && !_isDialogCooldownActive;
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否可用,可用就正常显示鼠标,否则就保持原有标志不变
|
||||
/// </summary>
|
||||
public bool IsAvailable => _isInteractionAvailable && !_isDialogCooldownActive;
|
||||
|
||||
/// <summary>
|
||||
/// 返回GameObject
|
||||
/// </summary>
|
||||
|
||||
public GameObject GetGameObject()
|
||||
{
|
||||
return gameObject;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user