Files
VFSUnity/Assets/Scripts/Interaction/InteractionSystem.cs
T
2026-07-17 05:42:52 +08:00

244 lines
7.6 KiB
C#

using UnityEngine;
using UnityEngine.Events;
using System.Collections;
/// <summary>
/// 核心交互系统:射线检测 + 点击/拖拽处理。
/// 挂在主相机或专用 GameObject 上。
/// </summary>
public class InteractionSystem : MonoBehaviour
{
[Header("射线配置")]
[SerializeField] private Camera _mainCamera;
[SerializeField] private LayerMask _interactableMask = ~0;
[SerializeField] private float _maxRayDistance = 100f;
[Header("事件 - 由 StepManager 绑定")]
public UnityEvent<string> OnObjectClicked; // 点击到 Tag 时触发,传 Tag
public UnityEvent OnAnywhereClicked; // 点击空白处触发
[Header("拖拽配置")]
[SerializeField] private float _dragPlaneDistance = 2f;
[SerializeField] private float _snapDistance = 0.3f;
private GameObject _draggedObject;
private Vector3 _dragOffset;
private float _dragZ;
private bool _isDragging;
private Coroutine _pendingAnywhereClick;
// 考核模式序列点击
private int _sequenceClickCount;
private string[] _expectedSequence;
private void Awake()
{
if (_mainCamera == null)
_mainCamera = Camera.main;
}
private void Update()
{
HandleClick();
HandleDrag();
}
#region 点击处理
private void HandleClick()
{
if (!Input.GetMouseButtonDown(0)) return;
if (UnityEngine.EventSystems.EventSystem.current != null &&
UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
return;
if (IsCustomFractureExamActive()) return;
if (InteractionClickGuard.WasHandledThisFrame()) return;
if (IsSplintPracticeActive()) return;
Camera rayCamera = GetActiveCamera();
if (rayCamera == null) return;
Ray ray = rayCamera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, _maxRayDistance, _interactableMask))
{
if (HasDirectClickHandler(hit.collider))
return;
string tag = hit.collider.tag;
if (!string.IsNullOrEmpty(tag))
{
Debug.Log($"InteractionSystem: 点击到物体 Tag={tag}");
OnObjectClicked?.Invoke(tag);
return;
}
}
QueueAnywhereClick();
}
private void QueueAnywhereClick()
{
if (_pendingAnywhereClick != null) StopCoroutine(_pendingAnywhereClick);
_pendingAnywhereClick = StartCoroutine(InvokeAnywhereClickNextFrame());
}
private IEnumerator InvokeAnywhereClickNextFrame()
{
yield return null;
_pendingAnywhereClick = null;
if (InteractionClickGuard.WasHandledRecently()) yield break;
if (UnityEngine.EventSystems.EventSystem.current != null &&
UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
yield break;
OnAnywhereClicked?.Invoke();
}
private Camera GetActiveCamera()
{
if (_mainCamera != null && _mainCamera.enabled && _mainCamera.gameObject.activeInHierarchy)
return _mainCamera;
foreach (var camera in Camera.allCameras)
{
if (camera != null && camera.enabled && camera.gameObject.activeInHierarchy)
return camera;
}
return Camera.main;
}
private bool HasDirectClickHandler(Collider collider)
{
if (collider == null) return false;
return collider.GetComponentInParent<Point2ClickHandler>() != null ||
collider.GetComponentInParent<TouchPositionHandler>() != null ||
collider.GetComponentInParent<ShoseClickHandler>() != null ||
collider.GetComponentInParent<PingClickHandler>() != null ||
collider.GetComponentInParent<ChanraoTuibuHandler>() != null ||
collider.GetComponentInParent<SplintObjectClickHandler>() != null;
}
private bool IsSplintPracticeActive()
{
var flow = FindObjectOfType<SplintPracticeFlowManager>();
return flow != null && flow.IsActive;
}
private bool IsCustomFractureExamActive()
{
return GameManager.Instance != null &&
GameManager.Instance.CurrentExperimentType == ExperimentType.FractureFixation &&
GameManager.Instance.CurrentMode == ExperimentMode.Exam;
}
#endregion
#region 拖拽处理
private void HandleDrag()
{
if (Input.GetMouseButtonDown(0))
{
if (UnityEngine.EventSystems.EventSystem.current != null &&
UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
return;
Camera rayCamera = GetActiveCamera();
if (rayCamera == null) return;
Ray ray = rayCamera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, _maxRayDistance, _interactableMask))
{
var draggable = hit.collider.GetComponent<DraggableObject>();
if (draggable != null && draggable.IsDraggable)
{
_draggedObject = hit.collider.gameObject;
_dragZ = _mainCamera.WorldToScreenPoint(_draggedObject.transform.position).z;
_isDragging = true;
draggable.OnDragStart();
}
}
}
if (Input.GetMouseButton(0) && _isDragging && _draggedObject != null)
{
Vector3 screenPos = new Vector3(Input.mousePosition.x, Input.mousePosition.y, _dragZ);
Camera rayCamera = GetActiveCamera();
if (rayCamera == null) return;
Vector3 worldPos = rayCamera.ScreenToWorldPoint(screenPos);
_draggedObject.transform.position = worldPos;
}
if (Input.GetMouseButtonUp(0) && _isDragging && _draggedObject != null)
{
var draggable = _draggedObject.GetComponent<DraggableObject>();
if (draggable != null && draggable.SnapTarget != null)
{
float dist = Vector3.Distance(_draggedObject.transform.position, draggable.SnapTarget.position);
if (dist <= _snapDistance)
{
_draggedObject.transform.position = draggable.SnapTarget.position;
_draggedObject.transform.rotation = draggable.SnapTarget.rotation;
draggable.OnSnapped();
}
}
draggable?.OnDragEnd();
_draggedObject = null;
_isDragging = false;
}
}
#endregion
#region 考核序列点击
public void BeginSequenceCheck(string[] expectedTags)
{
_expectedSequence = expectedTags;
_sequenceClickCount = 0;
}
public bool TrySequenceClick(string clickedTag, out bool isComplete)
{
isComplete = false;
if (_expectedSequence == null || _sequenceClickCount >= _expectedSequence.Length)
return false;
if (clickedTag == _expectedSequence[_sequenceClickCount])
{
_sequenceClickCount++;
if (_sequenceClickCount >= _expectedSequence.Length)
{
isComplete = true;
_expectedSequence = null;
return true;
}
return true;
}
return false;
}
/// <summary>获取当前序列已正确命中的步数(用于 proportion 计分)</summary>
public int GetSequenceHitCount()
{
return _sequenceClickCount;
}
#endregion
#region 公共方法
public void SetInteractionEnabled(bool enabled)
{
this.enabled = enabled;
}
#endregion
}