Files
zeling_v2/Assets/_Game/Scripts/Camera/CameraTriggerZone.cs
Joywayer b7baf7ad6a Add WeaponFeedback component and AddressableManagerWindow meta file
- Implemented WeaponFeedback class for handling weapon-related feedbacks such as hit effects and attack sounds.
- Added meta file for AddressableManagerWindow to manage addressable assets.
- Included a new jump.data file for profiler data.
2026-05-22 22:03:32 +08:00

282 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Core;
namespace BaseGames.Camera
{
/// <summary>
/// 相机区域切换模式。
/// </summary>
public enum CameraZoneSwitchMode
{
/// <summary>进入即切换(默认)。
/// 只要走进触发区域就立刻切换,不需要完全离开当前区域。</summary>
Immediate,
/// <summary>必须离开当前区域才切换。
/// 进入新区域后仅将其加入候选列表,等玩家完全离开当前激活区域后再接管。</summary>
ExitFirst,
}
/// <summary>
/// 相机区域切换触发器。
/// 支持两种切换模式,可通过 Inspector 配置:
/// <list type="bullet">
/// <item><b>Immediate</b>:进入即切换,不等待离开旧区域。</item>
/// <item><b>ExitFirst</b>:必须离开当前激活区域后才切换。</item>
/// </list>
/// </summary>
[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;
/// <summary>触发区域优先级(只读),供外部按优先级选择最佳区域。</summary>
public int Priority => _priority;
// ── 静态:跨实例共享触发状态 ──────────────────────────────────────────
// 玩家当前物理上所在的所有触发区域(按进入顺序排列)
private static readonly List<CameraTriggerZone> s_InsideZones = new();
// 场景内所有已启用的触发区域,供 RoomController 等查询(替代 FindObjectsOfType
private static readonly List<CameraTriggerZone> s_AllZones = new();
// 当前已向 ICameraService 发出 SwitchArea 请求的触发区域
private static CameraTriggerZone s_ActiveZone;
/// <summary>场景内当前所有已启用的触发区域(只读)。</summary>
public static IReadOnlyList<CameraTriggerZone> AllZones => s_AllZones;
/// <summary>
/// 在每次进入 Play Mode 前(或禁用 Domain Reload 时的跨会话)重置静态状态,
/// 防止上一次游戏会话残留的区域引用导致触发逻辑错误。
/// </summary>
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStaticState()
{
s_InsideZones.Clear();
s_AllZones.Clear();
s_ActiveZone = null;
}
private void Awake()
{
_collider = GetComponent<PolygonCollider2D>();
_collider.isTrigger = true;
}
private void OnEnable()
{
if (Application.isPlaying)
s_AllZones.Add(this);
}
private void OnDisable()
{
if (!Application.isPlaying) return;
s_AllZones.Remove(this);
HandlePlayerExit();
}
/// <summary>判断世界坐标点是否在本触发区域多边形内(供 RoomController 等无需 GetComponent 直接查询)。</summary>
public bool ContainsPoint(Vector2 worldPoint) => _collider != null && _collider.OverlapPoint(worldPoint);
/// <summary>
/// 若玩家出生时已在触发区域内OnTriggerEnter2D 不会触发。
/// 延迟一帧(确保 RoomController.Start 先完成基准区域设置)后主动检测。
/// </summary>
private IEnumerator Start()
{
if (!Application.isPlaying) yield break;
// 等一帧:让 RoomController.Startpriority=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();
}
/// <summary>
/// 玩家离开触发区域的统一处理(<see cref="OnTriggerExit2D"/>、
/// <see cref="FixedUpdate"/> 边缘检测及 <see cref="OnDisable"/> 共同调用)。
/// 带幂等保护,多次调用安全。
/// </summary>
private void HandlePlayerExit()
{
if (!_isPlayerInside) return; // 幂等保护:防止重复触发
_isPlayerInside = false;
s_InsideZones.Remove(this);
if (s_ActiveZone == this)
Deactivate(this);
else
ServiceLocator.GetOrDefault<ICameraService>()?.ReleaseArea(_targetArea, null);
}
// ── 静态辅助方法 ────────────────────────────────────────────────────────
/// <summary>
/// 评估 <see cref="s_InsideZones"/> 并在需要时切换激活区域。
/// <list type="bullet">
/// <item>无激活区域时:直接激活最后进入的区域。</item>
/// <item>新 <c>SelectBest()</c> 与当前激活不同时:立即覆盖切换。
/// 由于 <c>SelectBest</c> 使用 &gt;= 规则,进入任何新区域都会触发切换。</item>
/// </list>
/// </summary>
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<ICameraService>()?.SwitchArea(zone._targetArea, zone._priority);
}
/// <summary>
/// 不经过 Exit 事件,直接将激活区域切换为 <paramref name="newZone"/>。
/// 旧区域保留在 <see cref="s_InsideZones"/> 中(玩家仍在其内部),
/// 不立即释放旧区域——等玩家物理离开旧区域时由 <see cref="OnTriggerExit2D"/> 清理。
/// </summary>
private static void OverrideActive(CameraTriggerZone newZone)
{
s_ActiveZone = newZone;
ServiceLocator.GetOrDefault<ICameraService>()?.SwitchArea(newZone._targetArea, newZone._priority);
}
/// <summary>
/// 离开 <paramref name="leaving"/> 时的处理:
/// 若还有其他触发区域,先激活最优者再释放 leaving避免短暂回退到房间基线
/// 否则直接释放并使用 <see cref="_exitFallbackArea"/>。
/// </summary>
private static void Deactivate(CameraTriggerZone leaving)
{
ICameraService svc = ServiceLocator.GetOrDefault<ICameraService>();
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);
}
}
/// <summary>
/// 从 <see cref="s_InsideZones"/> 中选出最优区域。
/// 优先级高者优先;优先级相同时取最后进入的区域(后进入的胜出),
/// 确保进入任何新区域时都会立即切换,而不是等待离开旧区域。
/// </summary>
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<PolygonCollider2D>();
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<Vector2>();
_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);
}
}
}
}