499 lines
16 KiB
C#
499 lines
16 KiB
C#
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
|
|
namespace AibisDream.Framework
|
|
{
|
|
/// <summary>
|
|
/// 世界坐标下的滚动组件,类似ScrollRect但兼容SpriteRenderer
|
|
/// </summary>
|
|
[RequireComponent(typeof(Collider2D))]
|
|
public class WorldScrollRect : MonoBehaviour, IInteraction
|
|
{
|
|
[Header("滚动目标")]
|
|
[SerializeField] private Transform targetTransform;
|
|
|
|
[Header("滚动方向")]
|
|
[SerializeField] private bool horizontal = true;
|
|
[SerializeField] private bool vertical = true;
|
|
|
|
[Header("边界设置")]
|
|
[SerializeField] private bool useAutoBounds = true;
|
|
[SerializeField] private Bounds contentBounds;
|
|
[SerializeField] private Bounds viewportBounds;
|
|
|
|
[Header("惯性滚动")]
|
|
[SerializeField] private bool inertia = true;
|
|
[SerializeField] private float decelerationRate = 0.135f;
|
|
[SerializeField] private float velocityThreshold = 0.1f;
|
|
|
|
[Header("弹性边界")]
|
|
[SerializeField] private bool elasticity = true;
|
|
[SerializeField] private float elasticityStrength = 0.1f;
|
|
|
|
[Header("鼠标滚轮")]
|
|
[SerializeField] private bool scrollWheelEnabled = true;
|
|
[SerializeField] private float scrollSensitivity = 1f;
|
|
|
|
[Header("交互设置")]
|
|
[SerializeField] private bool isActive = true;
|
|
[SerializeField] private bool isAvailable = true;
|
|
|
|
private EventTriggerEx _eventTrigger;
|
|
private Collider2D _collider;
|
|
private Vector3 _initialTargetPosition;
|
|
private Vector3 _lastMouseWorldPos;
|
|
private Vector3 _velocity;
|
|
private bool _isDragging;
|
|
private Vector3[] _velocityHistory = new Vector3[5];
|
|
private int _velocityHistoryIndex;
|
|
private float _lastDragTime;
|
|
|
|
// IInteraction接口实现
|
|
public bool IsActive => isActive;
|
|
public bool IsAvailable => isAvailable;
|
|
public GameObject GetGameObject() => gameObject;
|
|
|
|
private void Awake()
|
|
{
|
|
_eventTrigger = GetComponent<EventTriggerEx>();
|
|
_collider = GetComponent<Collider2D>();
|
|
|
|
if (targetTransform == null)
|
|
{
|
|
targetTransform = transform;
|
|
}
|
|
|
|
_initialTargetPosition = targetTransform.position;
|
|
|
|
// 注册拖拽事件
|
|
_eventTrigger.Register(EventTriggerType.BeginDrag, OnBeginDrag);
|
|
_eventTrigger.Register(EventTriggerType.Drag, OnDrag);
|
|
_eventTrigger.Register(EventTriggerType.EndDrag, OnEndDrag);
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
if (useAutoBounds)
|
|
{
|
|
CalculateBounds();
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!isActive || !isAvailable) return;
|
|
|
|
// 处理鼠标滚轮
|
|
if (scrollWheelEnabled)
|
|
{
|
|
HandleScrollWheel();
|
|
}
|
|
|
|
// 处理惯性滚动
|
|
if (inertia && !_isDragging && _velocity.magnitude > velocityThreshold)
|
|
{
|
|
ApplyInertia();
|
|
}
|
|
|
|
// 处理弹性边界
|
|
if (elasticity)
|
|
{
|
|
ApplyElasticity();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 计算内容边界和视口边界
|
|
/// </summary>
|
|
private void CalculateBounds()
|
|
{
|
|
// 计算视口边界(从Collider2D获取)
|
|
if (_collider != null)
|
|
{
|
|
viewportBounds = _collider.bounds;
|
|
}
|
|
else
|
|
{
|
|
viewportBounds = new Bounds(transform.position, Vector3.one * 5f);
|
|
}
|
|
|
|
// 计算内容边界(从targetTransform的子对象获取)
|
|
if (targetTransform != null)
|
|
{
|
|
Bounds bounds = new Bounds();
|
|
bool first = true;
|
|
|
|
// 检查targetTransform本身是否有Renderer
|
|
Renderer selfRenderer = targetTransform.GetComponent<Renderer>();
|
|
if (selfRenderer != null)
|
|
{
|
|
bounds = selfRenderer.bounds;
|
|
first = false;
|
|
}
|
|
|
|
// 遍历所有子对象
|
|
foreach (Transform child in targetTransform)
|
|
{
|
|
Renderer renderer = child.GetComponent<Renderer>();
|
|
if (renderer != null)
|
|
{
|
|
if (first)
|
|
{
|
|
bounds = renderer.bounds;
|
|
first = false;
|
|
}
|
|
else
|
|
{
|
|
bounds.Encapsulate(renderer.bounds);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 如果没有Renderer,使用Transform位置
|
|
if (first)
|
|
{
|
|
bounds = new Bounds(child.position, Vector3.zero);
|
|
first = false;
|
|
}
|
|
else
|
|
{
|
|
bounds.Encapsulate(child.position);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!first)
|
|
{
|
|
contentBounds = bounds;
|
|
}
|
|
else
|
|
{
|
|
// 如果没有任何Renderer,使用targetTransform的位置
|
|
contentBounds = new Bounds(targetTransform.position, Vector3.one * 2f);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
contentBounds = new Bounds(transform.position, Vector3.one * 2f);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 开始拖拽
|
|
/// </summary>
|
|
private void OnBeginDrag(BaseEventData data)
|
|
{
|
|
if (!IsActive || !IsAvailable) return;
|
|
if (data is not PointerEventData pointerData) return;
|
|
|
|
_isDragging = true;
|
|
_velocity = Vector3.zero;
|
|
_lastMouseWorldPos = CameraKit.GetMouseWorldPos(pointerData.position);
|
|
_lastDragTime = Time.time;
|
|
_velocityHistoryIndex = 0;
|
|
|
|
// 清除速度历史
|
|
for (int i = 0; i < _velocityHistory.Length; i++)
|
|
{
|
|
_velocityHistory[i] = Vector3.zero;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 拖拽中
|
|
/// </summary>
|
|
private void OnDrag(BaseEventData data)
|
|
{
|
|
if (!IsActive || !IsAvailable) return;
|
|
if (data is not PointerEventData pointerData) return;
|
|
|
|
Vector3 currentMouseWorldPos = CameraKit.GetMouseWorldPos(pointerData.position);
|
|
Vector3 delta = currentMouseWorldPos - _lastMouseWorldPos;
|
|
|
|
// 根据滚动方向限制delta
|
|
if (!horizontal) delta.x = 0f;
|
|
if (!vertical) delta.y = 0f;
|
|
delta.z = 0f;
|
|
|
|
// 更新目标位置
|
|
Vector3 newPosition = targetTransform.position + delta;
|
|
targetTransform.position = ClampPosition(newPosition);
|
|
|
|
// 记录速度历史
|
|
float deltaTime = Time.time - _lastDragTime;
|
|
if (deltaTime > 0f)
|
|
{
|
|
Vector3 velocity = delta / deltaTime;
|
|
_velocityHistory[_velocityHistoryIndex] = velocity;
|
|
_velocityHistoryIndex = (_velocityHistoryIndex + 1) % _velocityHistory.Length;
|
|
}
|
|
|
|
_lastMouseWorldPos = currentMouseWorldPos;
|
|
_lastDragTime = Time.time;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 结束拖拽
|
|
/// </summary>
|
|
private void OnEndDrag(BaseEventData data)
|
|
{
|
|
_isDragging = false;
|
|
|
|
// 计算平均速度
|
|
Vector3 totalVelocity = Vector3.zero;
|
|
int count = 0;
|
|
for (int i = 0; i < _velocityHistory.Length; i++)
|
|
{
|
|
if (_velocityHistory[i].magnitude > 0f)
|
|
{
|
|
totalVelocity += _velocityHistory[i];
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if (count > 0)
|
|
{
|
|
_velocity = totalVelocity / count;
|
|
}
|
|
else
|
|
{
|
|
_velocity = Vector3.zero;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 应用惯性滚动
|
|
/// </summary>
|
|
private void ApplyInertia()
|
|
{
|
|
// 应用衰减
|
|
_velocity *= Mathf.Pow(decelerationRate, Time.deltaTime);
|
|
|
|
// 根据滚动方向限制速度
|
|
if (!horizontal) _velocity.x = 0f;
|
|
if (!vertical) _velocity.y = 0f;
|
|
_velocity.z = 0f;
|
|
|
|
// 更新位置
|
|
Vector3 newPosition = targetTransform.position + _velocity * Time.deltaTime;
|
|
targetTransform.position = ClampPosition(newPosition);
|
|
|
|
// 如果速度太小,停止惯性
|
|
if (_velocity.magnitude < velocityThreshold)
|
|
{
|
|
_velocity = Vector3.zero;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 应用弹性边界
|
|
/// </summary>
|
|
private void ApplyElasticity()
|
|
{
|
|
if (_isDragging || _velocity.magnitude > velocityThreshold) return;
|
|
|
|
Vector3 currentPos = targetTransform.position;
|
|
Vector3 clampedPos = ClampPosition(currentPos);
|
|
Vector3 offset = currentPos - clampedPos;
|
|
|
|
if (offset.magnitude > 0.01f)
|
|
{
|
|
// 应用弹性回弹
|
|
Vector3 elasticForce = -offset * elasticityStrength;
|
|
Vector3 newPosition = currentPos + elasticForce * Time.deltaTime;
|
|
targetTransform.position = ClampPosition(newPosition);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 处理鼠标滚轮
|
|
/// </summary>
|
|
private void HandleScrollWheel()
|
|
{
|
|
Vector2 scrollDelta = Input.mouseScrollDelta;
|
|
if (scrollDelta.magnitude == 0f) return;
|
|
|
|
// 检查鼠标是否在视口内
|
|
Vector3 mouseWorldPos = CameraKit.GetMouseWorldPos(Input.mousePosition);
|
|
if (!viewportBounds.Contains(mouseWorldPos)) return;
|
|
|
|
Vector3 scrollDelta3D = new Vector3(
|
|
horizontal ? scrollDelta.x * scrollSensitivity : 0f,
|
|
vertical ? scrollDelta.y * scrollSensitivity : 0f,
|
|
0f
|
|
);
|
|
|
|
Vector3 newPosition = targetTransform.position + scrollDelta3D;
|
|
targetTransform.position = ClampPosition(newPosition);
|
|
|
|
// 停止惯性滚动
|
|
_velocity = Vector3.zero;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 限制位置在边界内
|
|
/// </summary>
|
|
private Vector3 ClampPosition(Vector3 position)
|
|
{
|
|
if (!useAutoBounds && contentBounds.size.magnitude < 0.01f)
|
|
{
|
|
return position;
|
|
}
|
|
|
|
// 计算targetTransform的偏移量
|
|
Vector3 offset = position - _initialTargetPosition;
|
|
|
|
// 计算移动后的内容边界
|
|
Bounds movedContentBounds = contentBounds;
|
|
movedContentBounds.center += offset;
|
|
|
|
Vector3 contentMin = movedContentBounds.min;
|
|
Vector3 contentMax = movedContentBounds.max;
|
|
Vector3 viewportMin = viewportBounds.min;
|
|
Vector3 viewportMax = viewportBounds.max;
|
|
|
|
Vector3 clampedOffset = offset;
|
|
|
|
// 水平方向限制
|
|
if (horizontal)
|
|
{
|
|
float contentWidth = contentMax.x - contentMin.x;
|
|
float viewportWidth = viewportMax.x - viewportMin.x;
|
|
|
|
if (contentWidth > viewportWidth)
|
|
{
|
|
// 内容大于视口,限制内容边界在视口内
|
|
// 内容左边界不能超过视口左边界
|
|
if (contentMin.x > viewportMin.x)
|
|
{
|
|
clampedOffset.x -= (contentMin.x - viewportMin.x);
|
|
}
|
|
// 内容右边界不能超过视口右边界
|
|
if (contentMax.x < viewportMax.x)
|
|
{
|
|
clampedOffset.x -= (contentMax.x - viewportMax.x);
|
|
}
|
|
}
|
|
// 如果内容小于视口,允许自由移动(不限制)
|
|
}
|
|
else
|
|
{
|
|
clampedOffset.x = 0f;
|
|
}
|
|
|
|
// 垂直方向限制
|
|
if (vertical)
|
|
{
|
|
float contentHeight = contentMax.y - contentMin.y;
|
|
float viewportHeight = viewportMax.y - viewportMin.y;
|
|
|
|
if (contentHeight > viewportHeight)
|
|
{
|
|
// 内容大于视口,限制内容边界在视口内
|
|
// 内容下边界不能超过视口下边界
|
|
if (contentMin.y > viewportMin.y)
|
|
{
|
|
clampedOffset.y -= (contentMin.y - viewportMin.y);
|
|
}
|
|
// 内容上边界不能超过视口上边界
|
|
if (contentMax.y < viewportMax.y)
|
|
{
|
|
clampedOffset.y -= (contentMax.y - viewportMax.y);
|
|
}
|
|
}
|
|
// 如果内容小于视口,允许自由移动(不限制)
|
|
}
|
|
else
|
|
{
|
|
clampedOffset.y = 0f;
|
|
}
|
|
|
|
clampedOffset.z = 0f;
|
|
return _initialTargetPosition + clampedOffset;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置目标Transform
|
|
/// </summary>
|
|
public void SetTargetTransform(Transform target)
|
|
{
|
|
targetTransform = target;
|
|
if (target != null)
|
|
{
|
|
_initialTargetPosition = target.position;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置内容边界
|
|
/// </summary>
|
|
public void SetContentBounds(Bounds bounds)
|
|
{
|
|
contentBounds = bounds;
|
|
useAutoBounds = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置视口边界
|
|
/// </summary>
|
|
public void SetViewportBounds(Bounds bounds)
|
|
{
|
|
viewportBounds = bounds;
|
|
useAutoBounds = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重新计算边界
|
|
/// </summary>
|
|
public void RecalculateBounds()
|
|
{
|
|
CalculateBounds();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 停止滚动
|
|
/// </summary>
|
|
public void StopMovement()
|
|
{
|
|
_velocity = Vector3.zero;
|
|
_isDragging = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重置到初始位置
|
|
/// </summary>
|
|
public void ResetPosition()
|
|
{
|
|
StopMovement();
|
|
if (targetTransform != null)
|
|
{
|
|
targetTransform.position = _initialTargetPosition;
|
|
}
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (_eventTrigger != null)
|
|
{
|
|
_eventTrigger.UnRegister(EventTriggerType.BeginDrag, OnBeginDrag);
|
|
_eventTrigger.UnRegister(EventTriggerType.Drag, OnDrag);
|
|
_eventTrigger.UnRegister(EventTriggerType.EndDrag, OnEndDrag);
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
// 绘制视口边界
|
|
Gizmos.color = Color.green;
|
|
Gizmos.DrawWireCube(viewportBounds.center, viewportBounds.size);
|
|
|
|
// 绘制内容边界
|
|
Gizmos.color = Color.yellow;
|
|
Gizmos.DrawWireCube(contentBounds.center, contentBounds.size);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|