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:
2026-06-02 16:10:44 +08:00
parent bcd8b0e90b
commit 06048c966a
47 changed files with 1912 additions and 1195 deletions

View File

@@ -0,0 +1,115 @@
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using BaseGames.Combat;
namespace BaseGames.Editor.Combat
{
/// <summary>
/// HitBox 自定义 Inspector。
/// 在"多形状碰撞体"面板中提供快捷按钮,一键创建带 HitBoxColliderProxy 的子形状节点。
///
/// 生成子节点结构:
/// [HitBoxParent] ← HitBox 组件(可无 Collider2D
/// ├── [Shape_Box] ← BoxCollider2D(isTrigger) + HitBoxColliderProxy
/// ├── [Shape_Circle] ← CircleCollider2D(isTrigger) + HitBoxColliderProxy
/// ├── [Shape_Capsule] ← CapsuleCollider2D(isTrigger) + HitBoxColliderProxy
/// └── [Shape_Polygon] ← PolygonCollider2D(isTrigger) + HitBoxColliderProxy
/// </summary>
[CustomEditor(typeof(HitBox))]
public class HitBoxEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
EditorGUILayout.Space(6);
EditorGUILayout.LabelField("── 多形状碰撞体 ──", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"在子节点添加 HitBoxColliderProxy + Collider2D 以组合多形状判定盒。\n" +
"子节点 Layer 自动继承本节点Collider2D.IsTrigger 自动设为 true。",
MessageType.Info);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("+ Box"))
AddShapeChild((HitBox)target, ShapeKind.Box);
if (GUILayout.Button("+ Circle"))
AddShapeChild((HitBox)target, ShapeKind.Circle);
if (GUILayout.Button("+ Capsule"))
AddShapeChild((HitBox)target, ShapeKind.Capsule);
if (GUILayout.Button("+ Polygon"))
AddShapeChild((HitBox)target, ShapeKind.Polygon);
EditorGUILayout.EndHorizontal();
if (Application.isPlaying)
{
EditorGUILayout.Space(2);
int proxyCount = ((HitBox)target).GetComponentsInChildren<HitBoxColliderProxy>(true).Length;
EditorGUILayout.LabelField($"子代 HitBoxColliderProxy 数:{proxyCount}", EditorStyles.miniLabel);
Repaint();
}
}
// ── 内部工具 ──────────────────────────────────────────────────────────
private enum ShapeKind { Box, Circle, Capsule, Polygon }
private static void AddShapeChild(HitBox hitBox, ShapeKind kind)
{
var child = new GameObject($"[Shape_{kind}]");
Undo.RegisterCreatedObjectUndo(child, $"Add HitBox {kind} Shape");
child.transform.SetParent(hitBox.transform, false);
child.layer = hitBox.gameObject.layer;
Collider2D col = kind switch
{
ShapeKind.Box => CreateBox(child),
ShapeKind.Circle => CreateCircle(child),
ShapeKind.Capsule => CreateCapsule(child),
ShapeKind.Polygon => CreatePolygon(child),
_ => CreateBox(child),
};
col.isTrigger = true;
Undo.AddComponent<HitBoxColliderProxy>(child);
Selection.activeGameObject = child;
EditorGUIUtility.PingObject(child);
}
private static BoxCollider2D CreateBox(GameObject go)
{
var c = Undo.AddComponent<BoxCollider2D>(go);
c.size = new Vector2(1f, 0.5f);
return c;
}
private static CircleCollider2D CreateCircle(GameObject go)
{
var c = Undo.AddComponent<CircleCollider2D>(go);
c.radius = 0.4f;
return c;
}
private static CapsuleCollider2D CreateCapsule(GameObject go)
{
var c = Undo.AddComponent<CapsuleCollider2D>(go);
c.size = new Vector2(0.5f, 1f);
return c;
}
private static PolygonCollider2D CreatePolygon(GameObject go)
{
var c = Undo.AddComponent<PolygonCollider2D>(go);
c.SetPath(0, new Vector2[]
{
new( 0f, 0.3f),
new( 0.5f, 0f ),
new( 0f, -0.3f),
new(-0.5f, 0f ),
});
return c;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 414d2528016107148929f4d65d5507e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -6,9 +6,14 @@ using BaseGames.Combat;
namespace BaseGames.Editor
{
/// <summary>
/// HurtBox 运行时注入状态可视化面板
/// 通过 HurtBox 上的 Editor* 属性读取注入状态,以颜色区分是否注入成功
/// 绿色 = 注入完成;橙色 = 未注入(该能力静默不生效);灰色 = 非 PlayMode。
/// HurtBox 自定义 Inspector
/// ① 提供"多形状受击区域"快捷按钮:一键创建带 HurtBox 的子形状节点(共享同一 HP 池)
/// ② 运行时注入状态可视化面板:绿色 = 注入完成;橙色 = 未注入;灰色 = 非 PlayMode。
///
/// 生成子节点结构:
/// [HurtBoxParent] ← HurtBox + 任意 Collider2D主受击区
/// ├── [HurtShape_Box] ← BoxCollider2D(isTrigger) + HurtBox共享 HP
/// └── [HurtShape_Circle] ← CircleCollider2D(isTrigger) + HurtBox共享 HP
/// </summary>
[CustomEditor(typeof(HurtBox))]
public class HurtBoxEditor : UnityEditor.Editor
@@ -27,6 +32,24 @@ namespace BaseGames.Editor
{
DrawDefaultInspector();
// ── 多形状受击区域 ──────────────────────────────────────────────
EditorGUILayout.Space(6);
EditorGUILayout.LabelField("── 多形状受击区域 ──", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"在子节点添加 HurtBox + Collider2D 以组合多形状受击区域,各子节点共享同一 HP 池。\n" +
"子节点 Layer 自动继承本节点HurtBoxOwnerGuard 防止同一次攻击重复扣血。",
MessageType.Info);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("+ Box"))
AddShapeChild((HurtBox)target, ShapeKind.Box);
if (GUILayout.Button("+ Circle"))
AddShapeChild((HurtBox)target, ShapeKind.Circle);
if (GUILayout.Button("+ Capsule"))
AddShapeChild((HurtBox)target, ShapeKind.Capsule);
EditorGUILayout.EndHorizontal();
// ── 运行时注入状态 ──────────────────────────────────────────────
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("── 运行时注入状态 ──", EditorStyles.boldLabel);
@@ -59,6 +82,34 @@ namespace BaseGames.Editor
// 持续刷新(避免只显示初始状态)
if (Application.isPlaying) Repaint();
}
// ── 子形状创建工具 ─────────────────────────────────────────────────────
private enum ShapeKind { Box, Circle, Capsule }
private static void AddShapeChild(HurtBox hurtBox, ShapeKind kind)
{
var child = new GameObject($"[HurtShape_{kind}]");
Undo.RegisterCreatedObjectUndo(child, $"Add HurtBox {kind} Shape");
child.transform.SetParent(hurtBox.transform, false);
child.layer = hurtBox.gameObject.layer;
// 先加 Collider2D 以满足 HurtBox 的 [RequireComponent(typeof(Collider2D))]
// 再 AddComponent<HurtBox>() 则不会重复创建 Collider2D。
Collider2D col = kind switch
{
ShapeKind.Box => Undo.AddComponent<BoxCollider2D>(child),
ShapeKind.Circle => Undo.AddComponent<CircleCollider2D>(child),
ShapeKind.Capsule => Undo.AddComponent<CapsuleCollider2D>(child),
_ => Undo.AddComponent<BoxCollider2D>(child),
};
col.isTrigger = true;
Undo.AddComponent<HurtBox>(child);
Selection.activeGameObject = child;
EditorGUIUtility.PingObject(child);
}
}
}
#endif