feat: VFS 虚拟仿真实验核心框架 v2
- 加权评分系统:每步独立 ScoreWeight,满分 100,SequenceClick 按子项比例计分 - StepData 新增 DetailedContent(学习模式详细操作说明)和 SubStepCount - 伤情检查步骤扩展为 5 子项序列(视觉检查→肿胀→疼痛→生命体征→神经评估) - 夹板固定模式完整评分(选择夹板→放置衬垫→夹板固定→固定后检查) - Doc/ 文件夹归档三份文档:项目大纲、文案、评分标准 - README 更新评分权重表和夹板固定流程
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
/// <summary>
|
||||
/// 挂载在可拖拽的 3D 物体上(如绷带、夹板)。
|
||||
/// 配合 InteractionSystem 使用。
|
||||
/// </summary>
|
||||
public class DraggableObject : MonoBehaviour
|
||||
{
|
||||
[Header("拖拽状态")]
|
||||
public bool IsDraggable = true;
|
||||
|
||||
[Header("吸附目标")]
|
||||
public Transform SnapTarget;
|
||||
|
||||
[Header("事件")]
|
||||
public UnityEvent OnDragStartEvent;
|
||||
public UnityEvent OnDragEndEvent;
|
||||
public UnityEvent OnSnappedEvent; // 吸附成功
|
||||
|
||||
private Vector3 _originalPosition;
|
||||
private Quaternion _originalRotation;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_originalPosition = transform.position;
|
||||
_originalRotation = transform.rotation;
|
||||
}
|
||||
|
||||
public void OnDragStart()
|
||||
{
|
||||
OnDragStartEvent?.Invoke();
|
||||
}
|
||||
|
||||
public void OnDragEnd()
|
||||
{
|
||||
OnDragEndEvent?.Invoke();
|
||||
}
|
||||
|
||||
public void OnSnapped()
|
||||
{
|
||||
Debug.Log($"DraggableObject: {name} 已吸附到目标");
|
||||
OnSnappedEvent?.Invoke();
|
||||
IsDraggable = false; // 吸附后不可再拖动
|
||||
}
|
||||
|
||||
/// <summary>重置到初始位置</summary>
|
||||
public void ResetPosition()
|
||||
{
|
||||
transform.position = _originalPosition;
|
||||
transform.rotation = _originalRotation;
|
||||
IsDraggable = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 物体高亮效果:鼠标悬停时切换 Emission 或轮廓材质。
|
||||
/// 挂在可交互的 3D 物体上。
|
||||
/// </summary>
|
||||
public class HighlightEffect : MonoBehaviour
|
||||
{
|
||||
[Header("高亮设置")]
|
||||
[SerializeField] private Color _highlightColor = Color.yellow;
|
||||
[SerializeField] private float _highlightIntensity = 0.5f;
|
||||
|
||||
[Header("可选 - 使用 Outline 组件")]
|
||||
[SerializeField] private bool _useOutline = true;
|
||||
[SerializeField] private float _outlineWidth = 0.02f;
|
||||
|
||||
private Renderer[] _renderers;
|
||||
private Material[] _originalMaterials;
|
||||
private Material[] _highlightMaterials;
|
||||
private bool _isHighlighted;
|
||||
|
||||
[Header("考核高亮 - 始终显示")]
|
||||
public bool AlwaysHighlighted;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_renderers = GetComponentsInChildren<Renderer>();
|
||||
}
|
||||
|
||||
private void OnMouseEnter()
|
||||
{
|
||||
if (_isHighlighted) return;
|
||||
SetHighlight(true);
|
||||
}
|
||||
|
||||
private void OnMouseExit()
|
||||
{
|
||||
if (AlwaysHighlighted) return;
|
||||
SetHighlight(false);
|
||||
}
|
||||
|
||||
public void SetHighlight(bool on)
|
||||
{
|
||||
_isHighlighted = on;
|
||||
if (_renderers == null) return;
|
||||
|
||||
foreach (var r in _renderers)
|
||||
{
|
||||
if (r == null) continue;
|
||||
foreach (var mat in r.materials)
|
||||
{
|
||||
if (on)
|
||||
{
|
||||
mat.EnableKeyword("_EMISSION");
|
||||
mat.SetColor("_EmissionColor", _highlightColor * _highlightIntensity);
|
||||
}
|
||||
else
|
||||
{
|
||||
mat.DisableKeyword("_EMISSION");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>闪烁效果,用于提示用户点击</summary>
|
||||
public void Flash(float duration = 0.5f)
|
||||
{
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(FlashRoutine(duration));
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator FlashRoutine(float duration)
|
||||
{
|
||||
float half = duration / 2f;
|
||||
SetHighlight(true);
|
||||
yield return new WaitForSeconds(half);
|
||||
SetHighlight(false);
|
||||
yield return new WaitForSeconds(half);
|
||||
SetHighlight(true);
|
||||
yield return new WaitForSeconds(half);
|
||||
SetHighlight(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
/// <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 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;
|
||||
|
||||
Ray ray = _mainCamera.ScreenPointToRay(Input.mousePosition);
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, _maxRayDistance, _interactableMask))
|
||||
{
|
||||
string tag = hit.collider.tag;
|
||||
if (!string.IsNullOrEmpty(tag))
|
||||
{
|
||||
Debug.Log($"InteractionSystem: 点击到物体 Tag={tag}");
|
||||
OnObjectClicked?.Invoke(tag);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
OnAnywhereClicked?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 拖拽处理
|
||||
|
||||
private void HandleDrag()
|
||||
{
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
{
|
||||
if (UnityEngine.EventSystems.EventSystem.current != null &&
|
||||
UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
|
||||
return;
|
||||
|
||||
Ray ray = _mainCamera.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);
|
||||
Vector3 worldPos = _mainCamera.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
|
||||
}
|
||||
Reference in New Issue
Block a user