diff --git a/Assets/Scripts/Interaction/PingClickHandler.cs b/Assets/Scripts/Interaction/PingClickHandler.cs index 9b774a3..88e9375 100644 --- a/Assets/Scripts/Interaction/PingClickHandler.cs +++ b/Assets/Scripts/Interaction/PingClickHandler.cs @@ -13,16 +13,46 @@ public class PingClickHandler : MonoBehaviour // 实例变量,记录自己被启用(或重新闪烁)的那一帧 private int _enableFrame = -1; + // 添加一个全局静态标志位,记录“语音是否已经被播放过了”,防止重复播放 + private static bool _hasPlayedFinishVoice = false; + public bool CanBeClicked() { return _isBlinking; } void OnEnable() { _enableFrame = Time.frameCount; + // 每次重新启用时(如果是第一轮的第一步),重置语音播放标志位 + // 通过判断是否是第一轮的第一个 ping,或者直接在第一轮就重置 + if (name == "ping.004" && !_modelShown) + { + _hasPlayedFinishVoice = false; + } + _modelRenderer = GetComponent(); if (_modelRenderer == null) _modelRenderer = GetComponentInChildren(); foreach (var r in GetComponentsInChildren(true)) r.enabled = false; + var colliders = GetComponents(); + foreach (var c in colliders) + { + if (c is BoxCollider || c is CapsuleCollider) + { + Destroy(c); + } + } + + var sphere = GetComponent(); + if (sphere == null) sphere = gameObject.AddComponent(); + + float maxScale = Mathf.Max(Mathf.Abs(transform.lossyScale.x), Mathf.Abs(transform.lossyScale.y), Mathf.Abs(transform.lossyScale.z)); + if (maxScale < 0.001f) maxScale = 1f; + + sphere.radius = 0.15f / maxScale; + sphere.center = Vector3.zero; + sphere.isTrigger = false; + sphere.enabled = true; + CreateOutlineMesh(); _modelShown = false; _isBlinking = true; @@ -59,22 +89,16 @@ public class PingClickHandler : MonoBehaviour void Update() { - // 只检测正在闪烁的 ping if (!_isBlinking) return; if (!Input.GetMouseButtonDown(0)) return; - // 防御 1:防止刚被激活的同一帧内立刻触发(比如点击鞋子的一瞬间 ping.004 被激活,此时鼠标左键按下状态依然为 true) if (Time.frameCount == _enableFrame) return; - - // 防御 2:防止同一帧内有多个 ping 同时触发(执行顺序导致的状态延迟问题) if (Time.frameCount == _lastClickedFrame) return; - // 如果点在 UI 上,不处理 if (UnityEngine.EventSystems.EventSystem.current != null && UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject()) return; - // 找到当前渲染画面的相机 Camera activeCamera = null; foreach (var c in Camera.allCameras) { @@ -85,14 +109,12 @@ public class PingClickHandler : MonoBehaviour } if (activeCamera == null) return; - // 计算自己到鼠标的屏幕距离 Vector3 screenPos = activeCamera.WorldToScreenPoint(transform.position); - if (screenPos.z < 0) return; // 在相机背后 + if (screenPos.z < 0) return; Vector2 mousePos = Input.mousePosition; float myDist = Vector2.Distance(mousePos, new Vector2(screenPos.x, screenPos.y)); - // 检查是否有其他正在闪烁的 ping 比我更近 var allHandlers = FindObjectsOfType(); foreach (var h in allHandlers) { @@ -104,14 +126,13 @@ public class PingClickHandler : MonoBehaviour if (otherScreen.z < 0) continue; float otherDist = Vector2.Distance(mousePos, new Vector2(otherScreen.x, otherScreen.y)); - if (otherDist < myDist) return; // 别人比我更近,让别人处理 + if (otherDist < myDist) return; } - // 我是离鼠标最近的闪烁中的 ping! - if (myDist < 120f) // 稍微放宽一点阈值,120像素内都算点中 + if (myDist < 120f) { Debug.Log($"[PingClick] 屏幕坐标命中: {name} (距离={myDist:F1}px)"); - _lastClickedFrame = Time.frameCount; // 锁定当前帧,其他 ping 不准再触发 + _lastClickedFrame = Time.frameCount; _isBlinking = false; PerformClick(); } @@ -166,23 +187,29 @@ public class PingClickHandler : MonoBehaviour if (p.gameObject.activeInHierarchy && (p._isBlinking || p._modelShown)) remaining++; } - // 自己刚刚 HideSelf 了,如果没有其他活跃的 ping 了 + if (remaining <= 1) { - Debug.Log("[PingClick] 所有的 ping 都已经绑成 chanrao 了!播放完成语音。"); - var am = FindObjectOfType(); - if (am != null) + // 彻底修复:使用静态标志位锁定播放动作,这一轮绝不会播两次 + if (!_hasPlayedFinishVoice) { + _hasPlayedFinishVoice = true; + Debug.Log("[PingClick] 所有的 ping 都已经绑成 chanrao 了!播放完成语音。"); + var am = FindObjectOfType(); + if (am != null) + { #if UNITY_EDITOR - var finishClip = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/Audio/people/伤员一切正常,包扎完毕.mp3"); - if (finishClip != null) am.PlayVoice(finishClip); + var finishClip = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/Audio/people/伤员一切正常,包扎完毕.mp3"); + if (finishClip != null) am.PlayVoice(finishClip); #endif - } - - var uiMgr = FindObjectOfType(); - if (uiMgr != null) - { - uiMgr.HideVideo(); + } + + var uiMgr = FindObjectOfType(); + if (uiMgr != null) + { + uiMgr.HideVideo(); + uiMgr.ShowEndPanel(); // 调用显示结束面板 + } } } } @@ -198,7 +225,7 @@ public class PingClickHandler : MonoBehaviour } _isBlinking = true; - _enableFrame = Time.frameCount; // 防止重置瞬间被点中 + _enableFrame = Time.frameCount; if (_outlineMesh != null) { @@ -221,7 +248,11 @@ public class PingClickHandler : MonoBehaviour ShowSelf(); _modelShown = true; _isBlinking = true; - _enableFrame = Time.frameCount; // 防止重置瞬间被点中 + _enableFrame = Time.frameCount; + + // 撤销的时候,把语音锁打开,以便能够重新播放 + _hasPlayedFinishVoice = false; + if (_outlineMesh != null) _outlineMesh.enabled = true; StopAllCoroutines(); StartCoroutine(BlinkOutline()); @@ -254,7 +285,10 @@ public class PingClickHandler : MonoBehaviour { _modelShown = false; _isBlinking = true; - _enableFrame = Time.frameCount; // 防止重置瞬间被点中 + _enableFrame = Time.frameCount; + + _hasPlayedFinishVoice = false; + if (_modelRenderer != null) _modelRenderer.enabled = false; foreach (var r in GetComponentsInChildren(true)) { if (_outlineMesh != null && r == _outlineMesh) continue; r.enabled = false; } if (_outlineMesh != null) _outlineMesh.enabled = true; diff --git a/Assets/Scripts/UI/UIManager.cs b/Assets/Scripts/UI/UIManager.cs index 5894a10..d48130b 100644 --- a/Assets/Scripts/UI/UIManager.cs +++ b/Assets/Scripts/UI/UIManager.cs @@ -1,11 +1,6 @@ using UnityEngine; using UnityEngine.UI; -/// -/// UI 中央调度器:引用所有子面板,绑定 StepManager 事件。 -/// 按钮策略:同时绑定 Inspector 引用(旧 RightButtons)和 ExperimentPanel/Navbar 按钮(新 UI), -/// 两者均可触发,互不冲突。 -/// public class UIManager : MonoBehaviour { [Header("管理器引用")] @@ -27,7 +22,7 @@ public class UIManager : MonoBehaviour [Header("=== 画中画视频 ===")] [SerializeField] private GameObject _videoPanel; - [Header("=== 仿真导航栏按钮(旧引用 + Navbar)===")] + [Header("=== 仿真导航栏按钮 ===")] [SerializeField] private Button _btnFullScreen; [SerializeField] private Button _btnPrevious; [SerializeField] private Button _btnNext; @@ -41,185 +36,100 @@ public class UIManager : MonoBehaviour [Header("=== 弹窗 ===")] [SerializeField] private ExamPopup _examPopup; - [SerializeField] private ScoreDisplay _scoreDisplay; - - [Header("=== 底部图标 ===")] - [SerializeField] private Button _btnCPR; - [SerializeField] private Button _btnFracture; - [SerializeField] private Button _btnSprain; - [SerializeField] private Button _btnBleeding; - [SerializeField] private Button _btnTransport; - - [Header("=== 固定方式选择(仅骨折固定) ===")] - [SerializeField] private GameObject _fixationPanel; - [SerializeField] private Button _btnLimbFixation; - [SerializeField] private Button _btnSplintFixation; + [SerializeField] private GameObject _endPanel; private void Start() { Debug.Log("=== UIManager 初始化开始 ==="); - - // 确保视频最开始是隐藏的 HideVideo(); + if (_endPanel != null) _endPanel.SetActive(false); if (_stepManager == null) - { _stepManager = FindObjectOfType(); - if (_stepManager != null) Debug.Log("UIManager: _stepManager 自动查找成功"); - else Debug.LogError("UIManager: _stepManager 未找到!"); - } - + if (_interactionSystem == null) - { _interactionSystem = FindObjectOfType(); - if (_interactionSystem != null) Debug.Log("UIManager: _interactionSystem 自动查找成功"); - } BindEvents(); - BindButtons(); // 旧引用(RightButtons 等) - BindNavbarButtons(); // 新 Navbar 按钮 + BindButtons(); + BindNavbarButtons(); Debug.Log("=== UIManager 初始化完成 ==="); } - #region Navbar 按钮绑定(始终执行,不依赖 Inspector 引用) - private void BindNavbarButtons() { - Debug.Log("--- UIManager: 开始绑定 Navbar 按钮 ---"); - var experimentPanel = GameObject.Find("ExperimentPanel"); - if (experimentPanel == null) - { - Debug.LogError("UIManager: ExperimentPanel 未找到!无法绑定 Navbar 按钮!"); - return; - } - + if (experimentPanel == null) return; var navbar = experimentPanel.transform.Find("Navbar"); - if (navbar == null) - { - Debug.LogError("UIManager: ExperimentPanel/Navbar 未找到!无法绑定 Navbar 按钮!"); - return; - } + if (navbar == null) return; - // FullScreen var btnFS = navbar.Find("FullScreen"); - if (btnFS != null) - { - var btn = btnFS.GetComponent