feat: Add HurtBoxOwnerGuard to prevent multiple damage registrations from the same HitBox activation
- Implemented HurtBoxOwnerGuard to ensure that multiple HurtBoxes on the same character do not register damage multiple times during a single HitBox activation. - Added custom editor for HitBox to facilitate the creation of shape colliders with HitBoxColliderProxy. - Introduced PhysicsPerceptionSystem for enemy perception, supporting multiple detection modes including RangeCircle, BatchLOS, FanCast, and BoxCast. - Created EnemyPatrolZone to define patrol and chase areas for enemies, allowing for shared zones among multiple enemies. - Added BD_IsOutsideZone conditional task for Behavior Designer to check if an enemy or player is outside a defined patrol zone.
This commit is contained in:
@@ -1,89 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Micosmo.SensorToolkit;
|
||||
|
||||
namespace BaseGames.Enemies.Perception
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人感知 Hub(架构 07_EnemyModule §9)。
|
||||
/// 集中暴露挂载在敌人 Prefab 上的各种 SensorToolkit Sensor,BD 任务通过
|
||||
/// 字符串槽位查询,避免在 BD 任务 Inspector 中拖具体 Sensor 引用。
|
||||
///
|
||||
/// 典型槽位命名约定:
|
||||
/// - "aggro" : RangeSensor2D,玩家入侵警戒圈
|
||||
/// - "attack_melee" : RangeSensor2D,近战触发距离
|
||||
/// - "attack_range" : RangeSensor2D,远程触发距离
|
||||
/// - "los" : LOSSensor2D,视线
|
||||
/// - "wall_ahead" : RaySensor2D,前方墙体检测
|
||||
/// - "ledge" : RaySensor2D,前方悬崖检测
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class EnemySensorHub : MonoBehaviour, IPerceptionSystem
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct SensorSlot
|
||||
{
|
||||
public string slotName;
|
||||
public Sensor sensor;
|
||||
}
|
||||
|
||||
[SerializeField] private SensorSlot[] _slots;
|
||||
|
||||
private Dictionary<string, Sensor> _map;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_map = new Dictionary<string, Sensor>(_slots?.Length ?? 0);
|
||||
if (_slots == null) return;
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
{
|
||||
var s = _slots[i];
|
||||
if (s.sensor != null && !string.IsNullOrEmpty(s.slotName))
|
||||
_map[s.slotName] = s.sensor;
|
||||
}
|
||||
}
|
||||
|
||||
public Sensor Get(string slotName)
|
||||
{
|
||||
if (_map == null || string.IsNullOrEmpty(slotName)) return null;
|
||||
_map.TryGetValue(slotName, out var s);
|
||||
return s;
|
||||
}
|
||||
|
||||
public bool IsDetecting(string slotName, GameObject target)
|
||||
{
|
||||
var s = Get(slotName);
|
||||
return s != null && target != null && s.IsDetected(target);
|
||||
}
|
||||
|
||||
public bool HasAnyDetection(string slotName)
|
||||
{
|
||||
var s = Get(slotName);
|
||||
if (s == null) return false;
|
||||
foreach (var _ in s.Detections) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public GameObject GetFirstDetection(string slotName)
|
||||
{
|
||||
var s = Get(slotName);
|
||||
if (s == null) return null;
|
||||
foreach (var go in s.Detections) return go;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停或恢复所有插槽的 Sensor。
|
||||
/// 当敌人超出 QuotaManager 活跃范围时调用(关闭),归入活跃范围时恢复(开启)。
|
||||
/// </summary>
|
||||
public void SetSuspended(bool suspended)
|
||||
{
|
||||
if (_slots == null) return;
|
||||
for (int i = 0; i < _slots.Length; i++)
|
||||
{
|
||||
var sensor = _slots[i].sensor;
|
||||
if (sensor != null) sensor.enabled = !suspended;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,15 @@ namespace BaseGames.Enemies.Perception
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人感知系统接口。
|
||||
/// EnemyBase 通过此接口与感知实现解耦,支持运行时替换(SensorToolkit / 自定义实现)。
|
||||
/// EnemyBase 通过此接口与感知实现解耦,支持运行时替换。
|
||||
/// 当前实现为 <see cref="PhysicsPerceptionSystem"/>(纯物理射线 / 圆形范围检测)。
|
||||
/// 若未来替换底层传感器实现,只需重新实现此接口,上层代码无需改动。
|
||||
/// </summary>
|
||||
public interface IPerceptionSystem
|
||||
{
|
||||
/// <summary>指定槽位是否已配置(用于运行前的能力检测,避免无效查询)。</summary>
|
||||
bool HasSlot(string slotName);
|
||||
|
||||
/// <summary>指定槽位是否检测到任意目标。</summary>
|
||||
bool HasAnyDetection(string slotName);
|
||||
|
||||
@@ -17,6 +22,20 @@ namespace BaseGames.Enemies.Perception
|
||||
/// <summary>返回指定槽位第一个检测到的对象,无检测则返回 null。</summary>
|
||||
GameObject GetFirstDetection(string slotName);
|
||||
|
||||
/// <summary>
|
||||
/// 返回指定槽位感知区域的半径(圆形区域)。
|
||||
/// 槽位不存在、非圆形区域或实现不支持时返回 -1。
|
||||
/// 主要供编辑器 Gizmos 绘制使用。
|
||||
/// </summary>
|
||||
float GetSensorRadius(string slotName);
|
||||
|
||||
/// <summary>
|
||||
/// 返回指定槽位检测原点相对于感知组件 transform 的偏移(X 分量已根据朝向翻转)。
|
||||
/// 槽位不存在时返回 <see cref="Vector2.zero"/>。
|
||||
/// 供 EnemyBase.OnDrawGizmos 定位各感知圆心使用,避免所有圆重叠在 transform.position。
|
||||
/// </summary>
|
||||
Vector2 GetSensorOffset(string slotName);
|
||||
|
||||
/// <summary>暂停或恢复感知系统(LOD / 超出活跃范围时调用)。</summary>
|
||||
void SetSuspended(bool suspended);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Perception
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人感知系统(自研纯物理实现)。
|
||||
/// 每个 <see cref="PerceptionSlot"/> 独立配置并独立运行,支持四种检测模式:
|
||||
/// • RangeCircle — Physics2D.OverlapCircleNonAlloc(可选 LOS 视线遮挡校验)
|
||||
/// • BatchLOS — 委托 EnemyBase.IsPlayerVisible()(BatchLOSSystem 批量射线)
|
||||
/// • FanCast — 以朝向为轴的扇形多射线视野(支持遮挡层)
|
||||
/// • BoxCast — 矩形区域重叠检测(X 偏移随 localScale.x 自动翻转)
|
||||
///
|
||||
/// EnemyBase.Awake() 通过 GetComponentInChildren<IPerceptionSystem>()
|
||||
/// 自动发现本组件,无需修改 EnemyBase。
|
||||
/// 槽位名称常量统一定义于 <see cref="SensorSlotNames"/>。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class PhysicsPerceptionSystem : MonoBehaviour, IPerceptionSystem
|
||||
{
|
||||
// ── 槽位类型 ──────────────────────────────────────────────────────────
|
||||
|
||||
public enum SlotType
|
||||
{
|
||||
/// <summary>Physics2D 圆形重叠检测</summary>
|
||||
RangeCircle,
|
||||
/// <summary>委托 EnemyBase.IsPlayerVisible()(BatchLOSSystem 批量射线视线检测)</summary>
|
||||
BatchLOS,
|
||||
/// <summary>以朝向为轴的扇形射线视野,遮挡层阻断视线</summary>
|
||||
FanCast,
|
||||
/// <summary>矩形区域重叠检测,X 偏移随 localScale.x 自动翻转</summary>
|
||||
BoxCast
|
||||
}
|
||||
|
||||
// ── 槽位定义 ──────────────────────────────────────────────────────────
|
||||
|
||||
[Serializable]
|
||||
public struct PerceptionSlot
|
||||
{
|
||||
[Tooltip("槽位名称,与 SensorSlotNames 常量保持一致\n(aggro / los / attack_melee / attack_range)")]
|
||||
public string slotName;
|
||||
|
||||
[Tooltip("RangeCircle:Physics2D 圆形范围检测\nBatchLOS:视线射线检测(BatchLOSSystem)\nFanCast:以朝向为轴的扇形射线视野\nBoxCast:矩形区域重叠检测")]
|
||||
public SlotType type;
|
||||
|
||||
[Min(0f)]
|
||||
[Tooltip("RangeCircle / FanCast:检测半径(米)\nBatchLOS:最大视线检测距离(0 = 不限制)\nBoxCast:忽略此值")]
|
||||
public float radius;
|
||||
|
||||
[Tooltip("目标检测层(通常为 Player 层);BatchLOS 忽略此值")]
|
||||
public LayerMask detectLayer;
|
||||
|
||||
[Tooltip("RangeCircle / BoxCast:基础重叠命中后额外校验视线(Physics2D.Raycast)\nFanCast:true = 射线被 losBlockMask 层遮挡;false = 穿透所有障碍物")]
|
||||
public bool requireLOS;
|
||||
|
||||
[Tooltip("requireLOS = true / FanCast(requireLOS = true):视线遮挡检测层(通常为 Platform + Wall)\nFanCast 射线只在 requireLOS = true 时被此层遮挡")]
|
||||
public LayerMask losBlockMask;
|
||||
|
||||
[Header("Origin")]
|
||||
[Tooltip("检测原点相对于 transform.position 的偏移(米)。\nX 分量随 localScale.x 朝向自动翻转;BatchLOS 仅影响 Gizmo,不影响实际射线。")]
|
||||
public Vector2 offset;
|
||||
|
||||
[Header("FanCast")]
|
||||
[Tooltip("FanCast:扇形张角(度),以朝向为中轴左右均匀展开")]
|
||||
public float fanAngle;
|
||||
|
||||
[Tooltip("FanCast:扇形内均匀分布的射线数量(建议 5–11 条)")]
|
||||
[Min(2)]
|
||||
public int fanRayCount;
|
||||
|
||||
[Header("BoxCast")]
|
||||
[Tooltip("BoxCast:检测框尺寸 (宽, 高),单位米")]
|
||||
public Vector2 boxSize;
|
||||
|
||||
[Tooltip("BoxCast:相对于感知中心的偏移,X 分量随 localScale.x 自动翻转")]
|
||||
public Vector2 boxOffset;
|
||||
|
||||
[Header("Gizmos")]
|
||||
[Tooltip("Scene 视图 Gizmo 颜色。留空(全透明黑色)时自动使用紫色回退,以确保可见。")]
|
||||
public Color gizmoColor;
|
||||
}
|
||||
|
||||
// ── 字段 ──────────────────────────────────────────────────────────────
|
||||
|
||||
[SerializeField] private PerceptionSlot[] _slots;
|
||||
|
||||
private readonly Dictionary<string, List<GameObject>> _detected =
|
||||
new Dictionary<string, List<GameObject>>();
|
||||
private readonly Collider2D[] _overlapBuffer = new Collider2D[32];
|
||||
private bool _suspended;
|
||||
private EnemyBase _owner;
|
||||
|
||||
// ── Unity 生命周期 ────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_owner = GetComponentInParent<EnemyBase>();
|
||||
if (_slots == null) return;
|
||||
foreach (var slot in _slots)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(slot.slotName) && !_detected.ContainsKey(slot.slotName))
|
||||
_detected[slot.slotName] = new List<GameObject>(4);
|
||||
}
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (_suspended || _slots == null) return;
|
||||
foreach (var slot in _slots)
|
||||
RefreshSlot(slot);
|
||||
}
|
||||
|
||||
// ── 内部检测逻辑 ──────────────────────────────────────────────────────
|
||||
|
||||
private void RefreshSlot(PerceptionSlot slot)
|
||||
{
|
||||
if (string.IsNullOrEmpty(slot.slotName)) return;
|
||||
if (!_detected.TryGetValue(slot.slotName, out var list)) return;
|
||||
list.Clear();
|
||||
|
||||
switch (slot.type)
|
||||
{
|
||||
case SlotType.BatchLOS:
|
||||
if (_owner != null && _owner.IsPlayerVisible() && _owner.PlayerTransform != null)
|
||||
{
|
||||
if (slot.radius > 0f)
|
||||
{
|
||||
float dist = Vector2.Distance(
|
||||
(Vector2)transform.position, (Vector2)_owner.PlayerTransform.position);
|
||||
if (dist > slot.radius) break;
|
||||
}
|
||||
list.Add(_owner.PlayerTransform.gameObject);
|
||||
}
|
||||
break;
|
||||
|
||||
case SlotType.RangeCircle:
|
||||
RefreshRangeCircle(slot, list);
|
||||
break;
|
||||
|
||||
case SlotType.FanCast:
|
||||
RefreshFanCast(slot, list);
|
||||
break;
|
||||
|
||||
case SlotType.BoxCast:
|
||||
RefreshBoxCast(slot, list);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshRangeCircle(in PerceptionSlot slot, System.Collections.Generic.List<GameObject> list)
|
||||
{
|
||||
if (slot.radius <= 0f || slot.detectLayer == 0) return;
|
||||
float facingSign = transform.localScale.x < 0f ? -1f : 1f;
|
||||
Vector2 origin = (Vector2)transform.position + new Vector2(slot.offset.x * facingSign, slot.offset.y);
|
||||
int count = Physics2D.OverlapCircleNonAlloc(origin, slot.radius, _overlapBuffer, slot.detectLayer);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var col = _overlapBuffer[i];
|
||||
if (col == null) continue;
|
||||
if (slot.requireLOS && !HasLineOfSight(origin, col.gameObject, slot.losBlockMask)) continue;
|
||||
list.Add(col.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshFanCast(in PerceptionSlot slot, System.Collections.Generic.List<GameObject> list)
|
||||
{
|
||||
if (slot.radius <= 0f || slot.detectLayer == 0 || slot.fanAngle <= 0f) return;
|
||||
|
||||
float facingSign = transform.localScale.x < 0f ? -1f : 1f;
|
||||
var forward = new Vector2(facingSign, 0f);
|
||||
Vector2 origin = (Vector2)transform.position + new Vector2(slot.offset.x * facingSign, slot.offset.y);
|
||||
float halfAngle = slot.fanAngle * 0.5f;
|
||||
int rays = Mathf.Max(2, slot.fanRayCount);
|
||||
// requireLOS = true:射线被 losBlockMask 遮挡;false:仅检测 detectLayer(穿透障碍物)
|
||||
int castMask = slot.requireLOS
|
||||
? ((int)slot.detectLayer | (int)slot.losBlockMask)
|
||||
: (int)slot.detectLayer;
|
||||
|
||||
for (int r = 0; r < rays; r++)
|
||||
{
|
||||
float t = (float)r / (rays - 1);
|
||||
Vector2 dir = RotateVector(forward, Mathf.Lerp(-halfAngle, halfAngle, t));
|
||||
RaycastHit2D hit = Physics2D.Raycast(origin, dir, slot.radius, castMask);
|
||||
if (hit.collider == null) continue;
|
||||
if (((1 << hit.collider.gameObject.layer) & (int)slot.detectLayer) == 0) continue;
|
||||
|
||||
var go = hit.collider.gameObject;
|
||||
if (!list.Contains(go)) list.Add(go);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshBoxCast(in PerceptionSlot slot, System.Collections.Generic.List<GameObject> list)
|
||||
{
|
||||
if (slot.boxSize.x <= 0f || slot.boxSize.y <= 0f || slot.detectLayer == 0) return;
|
||||
|
||||
float facingSign = transform.localScale.x < 0f ? -1f : 1f;
|
||||
Vector2 origin = (Vector2)transform.position + new Vector2(slot.offset.x * facingSign, slot.offset.y);
|
||||
Vector2 center = origin + new Vector2(slot.boxOffset.x * facingSign, slot.boxOffset.y);
|
||||
|
||||
int count = Physics2D.OverlapBoxNonAlloc(center, slot.boxSize, 0f, _overlapBuffer, slot.detectLayer);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var col = _overlapBuffer[i];
|
||||
if (col == null) continue;
|
||||
if (slot.requireLOS && !HasLineOfSight(origin, col.gameObject, slot.losBlockMask)) continue;
|
||||
list.Add(col.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasLineOfSight(Vector2 origin, GameObject target, LayerMask blockMask)
|
||||
{
|
||||
// 优先使用碰撞体包围盒中心(比 transform.position 更准确,避免偏心轴误判)
|
||||
Vector2 targetPos;
|
||||
var col = target.GetComponent<Collider2D>();
|
||||
targetPos = col != null ? (Vector2)col.bounds.center : (Vector2)target.transform.position;
|
||||
|
||||
var dir = targetPos - origin;
|
||||
float dist = dir.magnitude;
|
||||
if (dist <= 0f) return true;
|
||||
var hit = Physics2D.Raycast(origin, dir / dist, dist, blockMask);
|
||||
// 未命中任何障碍,或者第一个命中的就是目标自身(含子物体)
|
||||
return hit.collider == null || hit.collider.transform.IsChildOf(target.transform) || hit.collider.gameObject == target;
|
||||
}
|
||||
|
||||
private static Vector2 RotateVector(Vector2 v, float angleDeg)
|
||||
{
|
||||
float rad = angleDeg * Mathf.Deg2Rad;
|
||||
float cos = Mathf.Cos(rad);
|
||||
float sin = Mathf.Sin(rad);
|
||||
return new Vector2(cos * v.x - sin * v.y, sin * v.x + cos * v.y);
|
||||
}
|
||||
|
||||
// ── IPerceptionSystem ─────────────────────────────────────────────────
|
||||
|
||||
public bool HasSlot(string slotName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(slotName)) return false;
|
||||
// 运行时通过字典; 编辑器模式遍历数组
|
||||
if (_detected.Count > 0) return _detected.ContainsKey(slotName);
|
||||
if (_slots == null) return false;
|
||||
foreach (var s in _slots)
|
||||
if (s.slotName == slotName) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool HasAnyDetection(string slotName) =>
|
||||
_detected.TryGetValue(slotName, out var list) && list.Count > 0;
|
||||
|
||||
public bool IsDetecting(string slotName, GameObject target)
|
||||
{
|
||||
if (!_detected.TryGetValue(slotName, out var list)) return false;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
if (list[i] == target) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public GameObject GetFirstDetection(string slotName)
|
||||
{
|
||||
if (!_detected.TryGetValue(slotName, out var list) || list.Count == 0) return null;
|
||||
return list[0];
|
||||
}
|
||||
|
||||
public float GetSensorRadius(string slotName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(slotName) || _slots == null) return -1f;
|
||||
foreach (var s in _slots)
|
||||
if (s.slotName == slotName && s.type == SlotType.RangeCircle)
|
||||
return s.radius;
|
||||
return -1f;
|
||||
}
|
||||
|
||||
public Vector2 GetSensorOffset(string slotName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(slotName) || _slots == null) return Vector2.zero;
|
||||
float facingSign = transform.localScale.x < 0f ? -1f : 1f;
|
||||
foreach (var s in _slots)
|
||||
if (s.slotName == slotName)
|
||||
return new Vector2(s.offset.x * facingSign, s.offset.y);
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
public void SetSuspended(bool suspended)
|
||||
{
|
||||
_suspended = suspended;
|
||||
if (suspended)
|
||||
foreach (var list in _detected.Values)
|
||||
list.Clear();
|
||||
}
|
||||
|
||||
// ── 编辑器 API(仅 UNITY_EDITOR 访问)────────────────────────────────
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public PerceptionSlot[] EditorSlots => _slots;
|
||||
public IReadOnlyDictionary<string, List<GameObject>> EditorDetected => _detected;
|
||||
public EnemyBase EditorOwner => _owner;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58cb3ac0e49c151429cad39d3e164a3d
|
||||
guid: c0026fe36cfaffc4e95698bccd0a8380
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -1,43 +1,33 @@
|
||||
namespace BaseGames.Enemies.Perception
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="EnemySensorHub"/> 槽位名称常量。
|
||||
/// <see cref="PhysicsPerceptionSystem"/> 槽位名称常量。
|
||||
///
|
||||
/// 统一定义字符串键,避免在 BD Task Inspector 和代码中散布魔法字符串。
|
||||
/// Prefab 上 EnemySensorHub 组件的 slotName 字段必须与此处常量保持一致。
|
||||
/// Prefab 上 <see cref="PhysicsPerceptionSystem"/> 组件的 slotName 字段必须与此处常量保持一致。
|
||||
/// </summary>
|
||||
public static class SensorSlotNames
|
||||
{
|
||||
/// <summary>
|
||||
/// 警戒范围(RangeSensor2D):玩家进入此圈触发 Alert 阶段。
|
||||
/// 警戒范围(RangeCircle):玩家进入此圈触发 Alert 阶段。
|
||||
/// 通常半径大于攻击范围,小于视线检测范围。
|
||||
/// </summary>
|
||||
public const string Aggro = "aggro";
|
||||
|
||||
/// <summary>
|
||||
/// 视线检测(LOSSensor2D):敌我之间无遮挡时持续为 true。
|
||||
/// 视线检测(BatchLOS):敌我之间无遮挡时持续为 true。
|
||||
/// 由 BatchLOSSystem 批量计算,BD_IsPlayerVisible 读取结果。
|
||||
/// </summary>
|
||||
public const string LOS = "los";
|
||||
|
||||
/// <summary>
|
||||
/// 近战攻击范围(RangeSensor2D):玩家进入时触发近战攻击条件。
|
||||
/// 近战攻击范围(RangeCircle):玩家进入时触发近战攻击条件。
|
||||
/// </summary>
|
||||
public const string AttackMelee = "attack_melee";
|
||||
|
||||
/// <summary>
|
||||
/// 远程攻击范围(RangeSensor2D):玩家进入时触发远程攻击条件。
|
||||
/// 远程攻击范围(RangeCircle):玩家进入时触发远程攻击条件。
|
||||
/// </summary>
|
||||
public const string AttackRange = "attack_range";
|
||||
|
||||
/// <summary>
|
||||
/// 前方墙体(RaySensor2D):水平方向检测,用于巡逻转向。
|
||||
/// </summary>
|
||||
public const string WallAhead = "wall_ahead";
|
||||
|
||||
/// <summary>
|
||||
/// 前方悬崖(RaySensor2D):斜向下检测地面是否存在,用于巡逻转向。
|
||||
/// </summary>
|
||||
public const string Ledge = "ledge";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user