This commit is contained in:
2026-10-08 07:25:02 +08:00
parent fd100521f9
commit 3940299dbd
23 changed files with 6266 additions and 92 deletions
@@ -0,0 +1,160 @@
// ============================================================
// 文件:FishOutOfScreenDetector.cs
// 职责:出界检测(第二层)——挂在比屏幕大一圈的大 Box2D 触发器上,
// 统一收所有鱼的 OnTriggerEnter2D / OnTriggerExit2D,
// 当一条"沿路线移动(isMove=true)"的鱼的所有碰撞器都离开触发区、
// 且路线进度 > progressThreshold 时,静默回收这条鱼。
//
// 场景布置(两层):
// 第一层:贴屏幕四边的 4 个碰撞器 → 挡/反弹子弹(本脚本不管)
// 第二层:比屏幕大一圈的一个 BoxCollider2D(IsTrigger=true) → 本脚本挂它上面
//
// 使用前提:
// 1. 第二层触发器所在物体需要一个 Kinematic 或 Dynamic 的 Rigidbody2D,
// 否则 Trigger 事件不会触发(Static 的 Rigidbody2D 不算)
// 2. Physics2D 层碰撞矩阵里,第二层所在层必须与 Fish 层勾选
// 3. 鱼身上的碰撞器 tag / layer 都是 Fish(可在 Inspector 里改过滤值)
// ============================================================
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 出界检测:鱼离开"比屏幕大一圈"的触发器,且路线走过一半以上时,静默回收这条鱼。
/// </summary>
public class FishOutOfScreenDetector : MonoBehaviour
{
[Header("鱼碰撞器过滤")]
[Tooltip("鱼的 tag,留空表示不按 tag 过滤")]
public string fishTag = "Fish";
[Tooltip("鱼所在的 layer 名,留空表示不按 layer 过滤")]
public string fishLayer = "Fish";
[Header("出界判定")]
[Tooltip("路线进度必须超过这个值才允许出界回收(0~1)。\n" +
"防止出生点在屏外、正往屏内游的鱼被误删")]
[Range(0f, 1f)]
public float progressThreshold = 0.5f;
[Header("调试")]
public bool enableLog = false;
/// <summary>鱼 → 当前仍在触发区内的碰撞器集合(一条鱼可能有多个碰撞器)</summary>
private readonly Dictionary<MonsterContral, HashSet<Collider2D>> _tracked =
new Dictionary<MonsterContral, HashSet<Collider2D>>();
/// <summary>碰撞器 → 所属鱼(Exit 时快速反查,避免再 GetComponentInParent)</summary>
private readonly Dictionary<Collider2D, MonsterContral> _colliderOwner =
new Dictionary<Collider2D, MonsterContral>();
private int _fishLayerId = -1;
private void Awake()
{
_fishLayerId = string.IsNullOrEmpty(fishLayer) ? -1 : LayerMask.NameToLayer(fishLayer);
if (!string.IsNullOrEmpty(fishLayer) && _fishLayerId < 0)
Debug.LogWarning($"[出界检测] 找不到 layer: {fishLayer},请检查 Layer 配置");
}
/// <summary>这个碰撞器是不是"要检测的鱼"(tag + layer 都符合才受理)</summary>
private bool IsFishCollider(Collider2D col)
{
if (col == null) return false;
// ⭐ 用字符串比较而不是 CompareTag:tag 未在 Tag Manager 里定义时 CompareTag 会抛异常
if (!string.IsNullOrEmpty(fishTag) && col.gameObject.tag != fishTag) return false;
if (_fishLayerId >= 0 && col.gameObject.layer != _fishLayerId) return false;
return true;
}
/// <summary>进入触发区:登记这条鱼(开始出发时存起来)</summary>
private void OnTriggerEnter2D(Collider2D other)
{
if (!IsFishCollider(other)) return;
// 鱼脚本挂在根节点,碰撞器可能是子物体 → 往上找
var monster = other.GetComponentInParent<MonsterContral>();
if (monster == null) return;
// ⭐ 只处理沿路线移动的怪
if (!monster.isMove) return;
// 同一个碰撞器重复进入(池复用/漏收 Exit):先清掉旧登记再重新登记
if (_colliderOwner.TryGetValue(other, out var oldOwner) && oldOwner != null)
{
if (_tracked.TryGetValue(oldOwner, out var oldSet)) oldSet.Remove(other);
}
_colliderOwner.Remove(other);
if (!_tracked.TryGetValue(monster, out var set))
{
set = new HashSet<Collider2D>();
_tracked[monster] = set;
}
set.Add(other);
_colliderOwner[other] = monster;
if (enableLog)
Debug.Log($"[出界检测] 进入触发区 guid={monster.guid} collider={other.name} 该鱼碰撞器数={set.Count}");
}
/// <summary>离开触发区:全部碰撞器都离开后,再按路线进度决定是否回收</summary>
private void OnTriggerExit2D(Collider2D other)
{
if (other == null) return;
// 没登记过的碰撞器(不是鱼/被过滤/没收到 Enter):忽略
if (!_colliderOwner.TryGetValue(other, out var monster)) return;
_colliderOwner.Remove(other);
if (monster == null || !_tracked.TryGetValue(monster, out var set)) return;
set.Remove(other);
// ⭐ 这条鱼还有别的碰撞器贴着触发区 → 不回收,继续等
if (set.Count > 0)
{
if (enableLog)
Debug.Log($"[出界检测] 离开触发区 guid={monster.guid} collider={other.name} 剩余碰撞器={set.Count}");
return;
}
// 所有碰撞器都离开了 → 先把这条鱼从列表里删掉
_tracked.Remove(monster);
TryRecycle(monster);
}
/// <summary>所有碰撞器都出界后:路线进度超过阈值才回收</summary>
private void TryRecycle(MonsterContral monster)
{
if (monster == null || monster.gameObject == null) return;
// 已被击杀/回池(池对象会失活)→ 不做
if (!monster.gameObject.activeInHierarchy) return;
if (!monster.isMove) return;
var line = monster.monsterData != null ? monster.monsterData.moveLine : null;
if (line == null || line.moveTime <= 0f) return; // 没有路线时长的(非路线鱼)不处理
float progress = monster.moveTime / line.moveTime;
if (progress <= progressThreshold)
{
if (enableLog)
Debug.Log($"[出界检测] 已出界但进度不足({progress:F2} <= {progressThreshold:F2}),只清列表不回收 guid={monster.guid}");
return;
}
var mgr = CreateFishManager.Instance;
if (mgr == null || mgr.fishDatas_hashCodekey == null) return;
// 已经不在鱼列表里(被击杀/已回收)→ 不做,避免刷 NOT_FOUND
if (!mgr.fishDatas_hashCodekey.ContainsKey(monster.guid))
{
if (enableLog)
Debug.Log($"[出界检测] 出界但已不在鱼列表(可能已被击杀) guid={monster.guid}");
return;
}
if (enableLog)
Debug.Log($"[出界检测] 出界回收 guid={monster.guid} 进度={progress:F2} 位置={monster.transform.position}");
// 静默回收:无死亡粒子/无金币特效,直接从鱼列表移除并回池
mgr.Id_DieFish_NotFX(monster.guid);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ccd5485cb78bc22459d6860f93b0c2ec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: