using System.Collections; using System.Collections.Generic; using UnityEngine; using BaseGames.Core; namespace BaseGames.Camera { /// /// 相机区域切换模式。 /// public enum CameraZoneSwitchMode { /// 进入即切换(默认)。 /// 只要走进触发区域就立刻切换,不需要完全离开当前区域。 Immediate, /// 必须离开当前区域才切换。 /// 进入新区域后仅将其加入候选列表,等玩家完全离开当前激活区域后再接管。 ExitFirst, } /// /// 相机区域切换触发器。 /// 支持两种切换模式,可通过 Inspector 配置: /// /// Immediate:进入即切换,不等待离开旧区域。 /// ExitFirst:必须离开当前激活区域后才切换。 /// /// [ExecuteAlways] [RequireComponent(typeof(PolygonCollider2D))] public class CameraTriggerZone : MonoBehaviour { [SerializeField] private CameraArea _targetArea; [Tooltip("玩家离开此触发区域时回退到的区域(留空则退出时不做处理)。\n" + "通常设为上级/相邻的包含区域,使玩家返回时相机自然过渡。")] [SerializeField] private CameraArea _exitFallbackArea; [Tooltip("触发区域优先级。同时在多个触发区域内时,高优先级区域胜出。\n" + "相同优先级则后进入的胜出(推荐默认值 1)。")] [SerializeField] private int _priority = 1; [Tooltip("切换模式。\n" + "Immediate:进入即切换,无需离开当前区域(默认)。\n" + "ExitFirst:必须离开当前激活区域后才切换。")] [SerializeField] private CameraZoneSwitchMode _switchMode = CameraZoneSwitchMode.Immediate; [SerializeField] private string _playerTag = "Player"; private PolygonCollider2D _collider; private bool _isPlayerInside; /// 触发区域优先级(只读),供外部按优先级选择最佳区域。 public int Priority => _priority; // ── 静态:跨实例共享触发状态 ────────────────────────────────────────── // 玩家当前物理上所在的所有触发区域(按进入顺序排列) private static readonly List s_InsideZones = new(); // 当前已向 ICameraService 发出 SwitchArea 请求的触发区域 private static CameraTriggerZone s_ActiveZone; /// /// 在每次进入 Play Mode 前(或禁用 Domain Reload 时的跨会话)重置静态状态, /// 防止上一次游戏会话残留的区域引用导致触发逻辑错误。 /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetStaticState() { s_InsideZones.Clear(); s_ActiveZone = null; } private void Awake() { _collider = GetComponent(); _collider.isTrigger = true; } private void OnDisable() { if (!Application.isPlaying) return; HandlePlayerExit(); } /// /// 若玩家出生时已在触发区域内,OnTriggerEnter2D 不会触发。 /// 延迟一帧(确保 RoomController.Start 先完成基准区域设置)后主动检测。 /// private IEnumerator Start() { if (!Application.isPlaying) yield break; // 等一帧:让 RoomController.Start(priority=0)先建立基准区域, // 再以 _priority 叠加子区域,保证栈顺序正确。 yield return null; if (_targetArea == null) yield break; GameObject player = GameObject.FindWithTag(_playerTag); if (player == null || !_collider.OverlapPoint(player.transform.position)) yield break; // OnTriggerEnter2D 可能已先一步处理,避免重复加入 if (!_isPlayerInside) { _isPlayerInside = true; s_InsideZones.Add(this); } EvaluateAndSwitch(); } private void OnTriggerEnter2D(Collider2D other) { if (!Application.isPlaying) return; // 兼容碰撞体挂在子节点的玩家结构:先检查碰撞体本身标签, // 再检查其挂载的 Rigidbody2D 所在节点标签(通常为带标签的角色根节点)。 if (!other.CompareTag(_playerTag) && (other.attachedRigidbody == null || !other.attachedRigidbody.CompareTag(_playerTag))) return; if (_targetArea == null || _isPlayerInside) return; _isPlayerInside = true; s_InsideZones.Add(this); // Immediate:进入即评估切换。 // ExitFirst:仅在当前无激活区域时才先先激活,否则等待玩家离开当前激活区域。 if (_switchMode == CameraZoneSwitchMode.Immediate || s_ActiveZone == null) EvaluateAndSwitch(); } private void OnTriggerExit2D(Collider2D other) { if (!Application.isPlaying) return; if (!other.CompareTag(_playerTag) && (other.attachedRigidbody == null || !other.attachedRigidbody.CompareTag(_playerTag))) return; // 复合碰撞体场景:某个子碰撞体退出时,验证玩家根节点是否仍在区域内。 // 若根节点还在区域内(其他碰撞体尚未退出),则忽略此次退出事件。 Transform playerRoot = other.attachedRigidbody != null ? other.attachedRigidbody.transform : other.transform; if (_collider != null && _collider.OverlapPoint(playerRoot.position)) return; HandlePlayerExit(); } /// /// 玩家离开触发区域的统一处理(、 /// 边缘检测及 共同调用)。 /// 带幂等保护,多次调用安全。 /// private void HandlePlayerExit() { if (!_isPlayerInside) return; // 幂等保护:防止重复触发 _isPlayerInside = false; s_InsideZones.Remove(this); if (s_ActiveZone == this) Deactivate(this); else ServiceLocator.GetOrDefault()?.ReleaseArea(_targetArea, null); } // ── 静态辅助方法 ──────────────────────────────────────────────────────── /// /// 评估 并在需要时切换激活区域。 /// /// 无激活区域时:直接激活最后进入的区域。 /// SelectBest() 与当前激活不同时:立即覆盖切换。 /// 由于 SelectBest 使用 >= 规则,进入任何新区域都会触发切换。 /// /// private static void EvaluateAndSwitch() { if (s_ActiveZone == null) { Activate(s_InsideZones[s_InsideZones.Count - 1]); return; } CameraTriggerZone best = SelectBest(); if (best != s_ActiveZone) OverrideActive(best); } private static void Activate(CameraTriggerZone zone) { s_ActiveZone = zone; ServiceLocator.GetOrDefault()?.SwitchArea(zone._targetArea, zone._priority); } /// /// 不经过 Exit 事件,直接将激活区域切换为 。 /// 旧区域保留在 中(玩家仍在其内部), /// 不立即释放旧区域——等玩家物理离开旧区域时由 清理。 /// private static void OverrideActive(CameraTriggerZone newZone) { s_ActiveZone = newZone; ServiceLocator.GetOrDefault()?.SwitchArea(newZone._targetArea, newZone._priority); } /// /// 离开 时的处理: /// 若还有其他触发区域,先激活最优者再释放 leaving(避免短暂回退到房间基线); /// 否则直接释放并使用 。 /// private static void Deactivate(CameraTriggerZone leaving) { ICameraService svc = ServiceLocator.GetOrDefault(); if (s_InsideZones.Count > 0) { // 先激活下一个,再释放 leaving —— 此时 _currentArea 已更新为 next, // ReleaseArea(leaving) 中 wasActive=false,仅从 _activeZones 移除,不触发额外跳转 CameraTriggerZone next = SelectBest(); s_ActiveZone = next; svc?.SwitchArea(next._targetArea, next._priority); svc?.ReleaseArea(leaving._targetArea, null); } else { s_ActiveZone = null; svc?.ReleaseArea(leaving._targetArea, leaving._exitFallbackArea); } } /// /// 从 中选出最优区域。 /// 优先级高者优先;优先级相同时取最后进入的区域(后进入的胜出), /// 确保进入任何新区域时都会立即切换,而不是等待离开旧区域。 /// private static CameraTriggerZone SelectBest() { CameraTriggerZone best = s_InsideZones[0]; for (int i = 1; i < s_InsideZones.Count; i++) if (s_InsideZones[i]._priority >= best._priority) // >= 使同优先级时后进入的胜出 best = s_InsideZones[i]; return best; } private void OnDrawGizmos() { if (_collider == null) _collider = GetComponent(); if (_collider == null || _collider.pathCount == 0) return; Gizmos.matrix = transform.localToWorldMatrix; // 多边形触发边界(进入检测外框) Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.8f); var pts = new List(); _collider.GetPath(0, pts); for (int i = 0; i < pts.Count; i++) { Vector3 a = new Vector3(pts[i].x, pts[i].y, 0f); Vector3 b = new Vector3(pts[(i + 1) % pts.Count].x, pts[(i + 1) % pts.Count].y, 0f); Gizmos.DrawLine(a, b); } } } }