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

@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Boss;
@@ -15,6 +15,8 @@ using BaseGames.Parry;
using BaseGames.Player;
using BaseGames.Player.States;
using BaseGames.Skills;
using BaseGames.Editor.Combat;
using BaseGames.Editor.Skills;
namespace BaseGames.Editor
{
@@ -59,6 +61,7 @@ namespace BaseGames.Editor
// 小怪类型选择 — 具体敌人类型
private int _enemyTypeIndex = 0;
private SceneObjectPlacerTool.EnemyBodyColliderType _enemyBodyCollider = SceneObjectPlacerTool.EnemyBodyColliderType.Box;
private static readonly (string id, string name)[] EnemyTypes =
{
("E001", "草蛭"),
@@ -195,6 +198,8 @@ namespace BaseGames.Editor
var jumpGroup = MakeActionGroup();
jumpGroup.Add(MakeJumpButton("Data Hub武器/技能/形态)", DataHubWindow.Open));
jumpGroup.Add(MakeJumpButton("武器 HitBox 向导", WeaponHitBoxWizard.Open));
jumpGroup.Add(MakeJumpButton("技能 HitBox 向导", SkillHitBoxWizard.Open));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
root.Add(jumpGroup);
@@ -248,8 +253,9 @@ namespace BaseGames.Editor
root.Add(MakeSectionHeader("▶ 专项编辑器"));
var jumpGroup = MakeActionGroup();
jumpGroup.Add(MakeJumpButton("Data Hub敌人数据", DataHubWindow.Open));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
jumpGroup.Add(MakeJumpButton("Data Hub敌人数据", DataHubWindow.Open));
jumpGroup.Add(MakeJumpButton("武器 HitBox 向导", WeaponHitBoxWizard.Open));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
root.Add(jumpGroup);
return root;
@@ -286,11 +292,16 @@ namespace BaseGames.Editor
container.Add(MakeSeparator());
container.Add(MakeSectionHeader("▶ 场景搭建"));
container.Add(MakeHelpBox("在当前活动场景中放置完整组件树并自动绑定已有 SO。"));
container.Add(MakeHelpBox("在当前活动场景中放置完整组件树(含 Visual 子节点对齐 Collider并自动绑定已有 SO。"));
var colliderField = new UnityEngine.UIElements.EnumField("主体碰撞器类型", _enemyBodyCollider);
colliderField.RegisterValueChangedCallback(evt => _enemyBodyCollider = (SceneObjectPlacerTool.EnemyBodyColliderType)evt.newValue);
container.Add(colliderField);
var sceneGroup = MakeActionGroup();
string sceneLabel = $"放置 {id} {name} 到场景";
sceneGroup.Add(MakeSceneButton(sceneLabel, () => PlaceSpecificEnemy(id)));
sceneGroup.Add(MakeSceneButton(sceneLabel, () => PlaceSpecificEnemy(id, _enemyBodyCollider)));
sceneGroup.Add(MakeSceneButton("迁移选中对象 Visual 节点", MigrateSelectedEnemyVisualNode));
container.Add(sceneGroup);
}
@@ -330,15 +341,17 @@ namespace BaseGames.Editor
var sceneGroup = MakeActionGroup();
sceneGroup.Add(MakeSceneButton("放置嘲风到场景并绑定 SO", SceneObjectPlacerTool.PlaceChaoFeng));
sceneGroup.Add(MakeSceneButton("迁移选中对象 Visual 节点", MigrateSelectedEnemyVisualNode));
root.Add(sceneGroup);
root.Add(MakeSeparator());
root.Add(MakeSectionHeader("▶ 专项编辑器"));
var jumpGroup = MakeActionGroup();
jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow));
jumpGroup.Add(MakeJumpButton("Data HubBoss技能", DataHubWindow.Open));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow));
jumpGroup.Add(MakeJumpButton("武器 HitBox 向导", WeaponHitBoxWizard.Open));
jumpGroup.Add(MakeJumpButton("Data HubBoss技能", DataHubWindow.Open));
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
root.Add(jumpGroup);
return root;
@@ -603,23 +616,45 @@ namespace BaseGames.Editor
$"全部 {id} SO 已创建(已存在的跳过)。\n请放置到场景后检查组件绑定。", "确定");
}
private static void PlaceSpecificEnemy(string id)
private static void PlaceSpecificEnemy(string id, SceneObjectPlacerTool.EnemyBodyColliderType bodyCollider)
{
switch (id)
{
case "E001": SceneObjectPlacerTool.PlaceE001_CaoZhi(); break;
case "E002": SceneObjectPlacerTool.PlaceE002_HuangZhi(); break;
case "E003": SceneObjectPlacerTool.PlaceE003_YouZhi_Enemy(); break;
case "E004": SceneObjectPlacerTool.PlaceE004_ZhiMu_Enemy(); break;
case "E005": SceneObjectPlacerTool.PlaceE005_FeiZhi_Enemy(); break;
case "E006": SceneObjectPlacerTool.PlaceE006_Huan(); break;
case "E001": SceneObjectPlacerTool.PlaceE001_CaoZhi(bodyCollider); break;
case "E002": SceneObjectPlacerTool.PlaceE002_HuangZhi(bodyCollider); break;
case "E003": SceneObjectPlacerTool.PlaceE003_YouZhi_Enemy(bodyCollider); break;
case "E004": SceneObjectPlacerTool.PlaceE004_ZhiMu_Enemy(bodyCollider); break;
case "E005": SceneObjectPlacerTool.PlaceE005_FeiZhi_Enemy(bodyCollider); break;
case "E006": SceneObjectPlacerTool.PlaceE006_Huan(bodyCollider); break;
default:
Debug.LogError($"[CharacterWizardWindow] 未注册的敌人 id '{id}',请在 SceneObjectPlacerTool 中实现对应 PlaceE...() 方法并注册。");
SceneObjectPlacerTool.PlaceEnemy();
SceneObjectPlacerTool.PlaceEnemy(bodyCollider);
break;
}
}
private static void MigrateSelectedEnemyVisualNode()
{
var targets = Selection.gameObjects;
if (targets == null || targets.Length == 0)
{
EditorUtility.DisplayDialog("迁移 Visual 节点",
"请先在 Hierarchy 中选中一个或多个 Enemy 对象。", "确定");
return;
}
int count = 0;
foreach (var go in targets)
{
var movement = go.GetComponent<EnemyMovement>();
if (movement != null) { movement.SetupVisualNode(); count++; }
}
if (count == 0)
EditorUtility.DisplayDialog("迁移 Visual 节点",
"选中的对象中没有找到 EnemyMovement 组件。", "确定");
else
Debug.Log($"[CharacterWizardWindow] 已对 {count} 个对象完成 Visual 节点迁移。");
}
// ── SO 资产工厂:嘲风 Boss ─────────────────────────────────────────────
private static void CreateChaoFengStatsSO()

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

View File

@@ -0,0 +1,227 @@
using System.Text;
using UnityEditor;
using UnityEngine;
using BaseGames.Enemies.Perception;
namespace BaseGames.Editor
{
[CustomEditor(typeof(PhysicsPerceptionSystem))]
public sealed class PhysicsPerceptionSystemEditor : UnityEditor.Editor
{
// ── Scene 视图 Gizmo ──────────────────────────────────────────────────
[DrawGizmo(GizmoType.NonSelected | GizmoType.Selected | GizmoType.InSelectionHierarchy)]
static void DrawGizmos(PhysicsPerceptionSystem system, GizmoType gizmoType)
{
var slots = system.EditorSlots;
if (slots == null) return;
bool isSelected = (gizmoType & (GizmoType.Selected | GizmoType.InSelectionHierarchy)) != 0;
Vector3 rootPos = system.transform.position;
float facingSign = system.transform.localScale.x < 0f ? -1f : 1f;
foreach (var slot in slots)
{
if (string.IsNullOrEmpty(slot.slotName)) continue;
Color baseColor = ResolveGizmoColor(slot.gizmoColor);
Color fill = baseColor; fill.a = isSelected ? 0.12f : 0.04f;
Color outline = baseColor; outline.a = isSelected ? 0.90f : 0.40f;
// 每个 Slot 独立检测原点X 随朝向翻转)
Vector3 slotCenter = rootPos + new Vector3(slot.offset.x * facingSign, slot.offset.y, 0f);
switch (slot.type)
{
case PhysicsPerceptionSystem.SlotType.RangeCircle:
if (slot.radius <= 0f) break;
Handles.color = fill;
Handles.DrawSolidDisc(slotCenter, Vector3.forward, slot.radius);
Handles.color = outline;
Handles.DrawWireDisc(slotCenter, Vector3.forward, slot.radius);
break;
case PhysicsPerceptionSystem.SlotType.FanCast:
if (slot.radius > 0f && slot.fanAngle > 0f)
{
DrawFanGizmo(slotCenter, facingSign, slot, fill, outline);
if (isSelected) DrawOriginDot(slotCenter, outline);
}
break;
case PhysicsPerceptionSystem.SlotType.BoxCast:
if (slot.boxSize.x > 0f && slot.boxSize.y > 0f)
{
DrawBoxGizmo(slotCenter, facingSign, slot, fill, outline);
if (isSelected) DrawOriginDot(slotCenter, outline);
}
break;
// BatchLOS: eye-position marker + optional range disc + runtime ray
case PhysicsPerceptionSystem.SlotType.BatchLOS:
DrawBatchLOSGizmo(system, slotCenter, slot, outline, isSelected);
break;
}
}
Handles.color = Color.white;
}
/// <summary>在 Slot 原点处画一个小十字点,帮助可视化 offset 配置。</summary>
static void DrawOriginDot(Vector3 pos, Color color)
{
Handles.color = color;
Handles.DrawSolidDisc(pos, Vector3.forward, 0.04f);
}
/// <summary>
/// 仅当颜色字段为 Unity 默认值 Color(0,0,0,0)(全零 = 未配置)时,
/// 返回易辨识的紫色回退alpha=1用户明确设置的任何颜色包括近黑色均原样保留。
/// </summary>
static Color ResolveGizmoColor(Color c)
{
bool isDefault = c.r + c.g + c.b + c.a < 0.01f;
return isDefault ? new Color(0.85f, 0.3f, 1.0f, 1.0f) : c;
}
static void DrawBatchLOSGizmo(PhysicsPerceptionSystem system, Vector3 slotCenter,
PhysicsPerceptionSystem.PerceptionSlot slot, Color slotColor, bool isSelected)
{
// slotColor 已经过 ResolveGizmoColor 处理alpha 已由调用方设为 outline alpha。
// 所有 gizmo 元素统一画在 slotCenter由 slot.offset 控制),
// 与 LOSOrigin实际射线起点解耦——gizmo 跟 offset 走。
float facingSign = system.transform.localScale.x < 0f ? -1f : 1f;
// ── 最大检测范围圆slot.radius > 0 时)──
if (slot.radius > 0f)
{
Color fill = slotColor; fill.a = isSelected ? 0.08f : 0.03f;
Handles.color = fill;
Handles.DrawSolidDisc(slotCenter, Vector3.forward, slot.radius);
Color rim = slotColor; rim.a = isSelected ? 0.70f : 0.30f;
Handles.color = rim;
Handles.DrawWireDisc(slotCenter, Vector3.forward, slot.radius);
}
// ── 眼睛中心圆点 ──
Handles.color = slotColor;
Handles.DrawSolidDisc(slotCenter, Vector3.forward, 0.08f);
// ── 朝向指示箭头(沿 facingSign 方向,明确"方向性"视线感知)──
float arrowLen = slot.radius > 0f ? Mathf.Min(slot.radius * 0.6f, 0.6f) : 0.4f;
Vector3 fwdArrow = new Vector3(facingSign * arrowLen, 0f, 0f);
Handles.DrawLine(slotCenter, slotCenter + fwdArrow);
Vector3 arrowTip = slotCenter + fwdArrow;
float headLen = 0.08f;
Handles.DrawLine(arrowTip, arrowTip + new Vector3(-facingSign * headLen, headLen, 0f));
Handles.DrawLine(arrowTip, arrowTip + new Vector3(-facingSign * headLen, -headLen, 0f));
// ── 放射线(仅选中时显示,避免杂乱)──
if (isSelected)
{
float innerR = 0.12f;
float outerR = slot.radius > 0f ? Mathf.Min(slot.radius, 0.30f) : 0.26f;
for (int i = 0; i < 8; i++)
{
float deg = i * 45f;
float rad = deg * Mathf.Deg2Rad;
Vector3 dir = new Vector3(Mathf.Cos(rad), Mathf.Sin(rad), 0f);
Handles.DrawLine(slotCenter + dir * innerR, slotCenter + dir * outerR);
}
}
// ── 运行时:视线连线;绿 = 可见 / 红 = 遮挡 ──
if (!Application.isPlaying) return;
var owner = system.EditorOwner ?? system.GetComponentInParent<BaseGames.Enemies.EnemyBase>();
if (owner == null || owner.PlayerTransform == null) return;
bool visible = owner.IsPlayerVisible();
Color rayCol = visible
? new Color(0.2f, 1.0f, 0.3f, 0.9f)
: new Color(1.0f, 0.3f, 0.3f, 0.45f);
Handles.color = rayCol;
Handles.DrawDottedLine(slotCenter, owner.PlayerTransform.position, 3f);
if (visible)
Handles.DrawSolidDisc(owner.PlayerTransform.position, Vector3.forward, 0.09f);
}
static void DrawFanGizmo(Vector3 center, float facingSign,
PhysicsPerceptionSystem.PerceptionSlot slot, Color fill, Color outline)
{
float halfAngle = slot.fanAngle * 0.5f;
Vector3 fromDir = RotateVec3(new Vector3(facingSign, 0f, 0f), -halfAngle);
Handles.color = fill;
Handles.DrawSolidArc(center, Vector3.forward, fromDir, slot.fanAngle, slot.radius);
Handles.color = outline;
Handles.DrawWireArc(center, Vector3.forward, fromDir, slot.fanAngle, slot.radius);
// Edge rays for clarity
Vector3 edgeL = RotateVec3(new Vector3(facingSign, 0f, 0f), -halfAngle) * slot.radius;
Vector3 edgeR = RotateVec3(new Vector3(facingSign, 0f, 0f), halfAngle) * slot.radius;
Handles.DrawLine(center, center + edgeL);
Handles.DrawLine(center, center + edgeR);
}
static void DrawBoxGizmo(Vector3 center, float facingSign,
PhysicsPerceptionSystem.PerceptionSlot slot, Color fill, Color outline)
{
Vector3 boxCenter = center + new Vector3(slot.boxOffset.x * facingSign, slot.boxOffset.y, 0f);
float hw = slot.boxSize.x * 0.5f;
float hh = slot.boxSize.y * 0.5f;
Vector3[] corners =
{
boxCenter + new Vector3(-hw, -hh, 0f),
boxCenter + new Vector3( hw, -hh, 0f),
boxCenter + new Vector3( hw, hh, 0f),
boxCenter + new Vector3(-hw, hh, 0f),
};
Handles.DrawSolidRectangleWithOutline(corners, fill, outline);
}
static Vector3 RotateVec3(Vector3 v, float angleDeg)
{
float rad = angleDeg * Mathf.Deg2Rad;
float cos = Mathf.Cos(rad);
float sin = Mathf.Sin(rad);
return new Vector3(cos * v.x - sin * v.y, sin * v.x + cos * v.y, 0f);
}
// ── Inspector ─────────────────────────────────────────────────────────
public override void OnInspectorGUI()
{
DrawDefaultInspector();
if (!Application.isPlaying) return;
var system = (PhysicsPerceptionSystem)target;
var detected = system.EditorDetected;
if (detected == null || detected.Count == 0) return;
EditorGUILayout.Space();
EditorGUILayout.LabelField("── 实时检测结果 ──", EditorStyles.boldLabel);
var sb = new StringBuilder();
foreach (var kvp in detected)
{
sb.Clear();
if (kvp.Value.Count > 0)
{
sb.Append("✓");
foreach (var go in kvp.Value)
sb.Append(' ').Append(go != null ? go.name : "null");
}
EditorGUILayout.LabelField(kvp.Key, kvp.Value.Count > 0 ? sb.ToString() : "—");
}
Repaint();
}
}
}

View File

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

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Reflection;
using Animancer;
using BaseGames.Boss;
@@ -36,6 +36,9 @@ namespace BaseGames.Editor
/// </summary>
public static class SceneObjectPlacerTool
{
// ── 碰撞器类型 ────────────────────────────────────────────────────────
public enum EnemyBodyColliderType { Box, Capsule, Circle }
// ══ 菜单入口 ══════════════════════════════════════════════════════════
[MenuItem("BaseGames/Scene/Place/Player", priority = 100)]
@@ -246,7 +249,9 @@ namespace BaseGames.Editor
}
[MenuItem("BaseGames/Scene/Place/Enemy (Basic)", priority = 110)]
public static void PlaceEnemy()
public static void PlaceEnemy() => PlaceEnemy(EnemyBodyColliderType.Box);
public static void PlaceEnemy(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
@@ -264,10 +269,12 @@ namespace BaseGames.Editor
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
GetOrAddComponent<CapsuleCollider2D>(go);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer sr = SetupSpriteRenderer(go);
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.7f, 0.9f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr = SetupSpriteRenderer(visual.gameObject);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
@@ -275,7 +282,7 @@ namespace BaseGames.Editor
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
GetOrAddComponent<EnemySensorHub>(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
// HurtBox child
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
@@ -306,6 +313,7 @@ namespace BaseGames.Editor
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
// Wire EnemyMovement
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr, report);
AssignLayerMask(movement, "_groundMask",
@@ -319,6 +327,7 @@ namespace BaseGames.Editor
else
report.Add("未找到 DamageSourceSOHitBox_Body._defaultSource 未绑定。请创建 CMB_DS_EnemyBody.asset。");
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
report.Add("★ 指定 EnemyBase._statsSO、_animConfig 资产(按所创建的敌人类型命名)。");
report.Add("★ 挂载行为树 BehaviorTree 组件,指定对应 .asset。");
@@ -329,7 +338,9 @@ namespace BaseGames.Editor
}
[MenuItem("BaseGames/Scene/Place/Boss Enemy", priority = 115)]
public static void PlaceBossEnemy()
public static void PlaceBossEnemy() => PlaceBossEnemy(EnemyBodyColliderType.Box);
public static void PlaceBossEnemy(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
@@ -345,12 +356,13 @@ namespace BaseGames.Editor
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
GetOrAddComponent<CapsuleCollider2D>(go);
CreateBodyCollider(go, bodyCollider, new Vector2(1.5f, 2.5f));
GetOrAddComponent<Animator>(go);
SetupSpriteRenderer(go);
BossBase bossBase = GetOrAddComponent<BossBase>(go);
EnemyStats bossStats = GetOrAddComponent<EnemyStats>(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
// HurtBox child
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
@@ -388,6 +400,7 @@ namespace BaseGames.Editor
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
report.Add("填写 _bossId。");
report.Add("挂载 BossSkillSequencer 组件并指定技能序列 SO行为树、NavAgent 需手工添加。");
report.Add("多阶段 Boss 可在此 GameObject 上继续 AddComponent 阶段切换控制器。");
@@ -399,7 +412,9 @@ namespace BaseGames.Editor
// ══ 具体敌人快速放置 ════════════════════════════════════════════════════
[MenuItem("BaseGames/Scene/Place/Enemy E001 (草蛭)", priority = 111)]
public static void PlaceE001_CaoZhi()
public static void PlaceE001_CaoZhi() => PlaceE001_CaoZhi(EnemyBodyColliderType.Box);
public static void PlaceE001_CaoZhi(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
@@ -416,12 +431,12 @@ namespace BaseGames.Editor
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(0.6f, 0.8f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer sr1 = SetupSpriteRenderer(go);
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.6f, 0.8f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr1 = SetupSpriteRenderer(visual.gameObject);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
@@ -429,7 +444,7 @@ namespace BaseGames.Editor
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
@@ -469,6 +484,7 @@ namespace BaseGames.Editor
AssignAsset(movement, "_config", report, false, "ENM_E001_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E001_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr1, report);
AssignLayerMask(movement, "_groundMask",
@@ -478,13 +494,11 @@ namespace BaseGames.Editor
AssignAsset(alertAbility, "_config", report, false, "ABL_E001_Alert");
AssignAsset(chaseAbility, "_config", report, false, "ABL_E001_Chase");
AssignReference(chaseAbility, "_contactDamage", bodyContact, report);
AssignReference(chaseAbility, "_sensorHub", sensorHub, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "wall_ahead", "ledge" }, report);
report.Add("★ 在 EnemySensorHub Inspector 中绑定 Sensor 子节点aggro/wall_ahead/ledge。");
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E001_CaoZhi.asset。");
Undo.CollapseUndoOperations(undoGroup);
@@ -494,7 +508,9 @@ namespace BaseGames.Editor
}
[MenuItem("BaseGames/Scene/Place/Enemy E002 (簧蛭)", priority = 112)]
public static void PlaceE002_HuangZhi()
public static void PlaceE002_HuangZhi() => PlaceE002_HuangZhi(EnemyBodyColliderType.Box);
public static void PlaceE002_HuangZhi(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
@@ -509,18 +525,24 @@ namespace BaseGames.Editor
rb.bodyType = RigidbodyType2D.Kinematic;
rb.gravityScale = 0f;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(0.5f, 0.7f);
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.5f, 0.7f));
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SetupSpriteRenderer(go);
// Visual 子节点:挂载精灵 / 动画EnemyMovement 翻转时操作此节点)
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr = SetupSpriteRenderer(visual.gameObject);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
// HurtBox初始禁用附着天花板时不受伤
// HurtBox初始禁用悬挂阶段无法被攻击
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
@@ -529,24 +551,35 @@ namespace BaseGames.Editor
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
hurtBoxT.gameObject.SetActive(false);
// AttackHitBox下坠发动时由能力启用
Transform atkT = GetOrCreateChild(go.transform, "AttackHitBox");
SetLayer(atkT.gameObject, "EnemyHitBox", report);
BoxCollider2D atkCol = GetOrAddComponent<BoxCollider2D>(atkT.gameObject);
atkCol.isTrigger = true;
atkCol.size = new Vector2(0.5f, 0.5f);
HitBox atkHitBox = GetOrAddComponent<HitBox>(atkT.gameObject);
atkT.gameObject.SetActive(false);
// LandingHitBox落地瞬间 AoE由 CeilingDropAbility 激活
Transform landingHitBoxT = GetOrCreateChild(go.transform, "LandingHitBox");
SetLayer(landingHitBoxT.gameObject, "EnemyHitBox", report);
BoxCollider2D landingCol = GetOrAddComponent<BoxCollider2D>(landingHitBoxT.gameObject);
landingCol.isTrigger = true;
landingCol.size = new Vector2(0.8f, 0.3f);
HitBox landingHitBox = GetOrAddComponent<HitBox>(landingHitBoxT.gameObject);
landingHitBoxT.gameObject.SetActive(false);
// ContactDamageZone地面巡逻时造成接触伤害落地后由行为树启用
Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone");
SetLayer(contactT.gameObject, "EnemyHitBox", report);
CircleCollider2D contactCol = GetOrAddComponent<CircleCollider2D>(contactT.gameObject);
contactCol.isTrigger = true;
contactCol.radius = 0.35f;
HitBox contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
contactT.gameObject.SetActive(false);
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
Transform strikeT = GetOrCreateChild(abilitiesT, "CeilingHangStrikeAbility");
CeilingHangStrikeAbility strikeAbility = GetOrAddComponent<CeilingHangStrikeAbility>(strikeT.gameObject);
Transform dropT = GetOrCreateChild(abilitiesT, "CeilingDropAbility");
CeilingDropAbility dropAbility = GetOrAddComponent<CeilingDropAbility>(dropT.gameObject);
// SOs — assign first so OnValidate doesn't warn during wiring
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E002_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E002_AnimConfig");
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E002_Stats");
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E002_AnimConfig");
AssignReference(enemyBase, "_stats", enemyStats, report);
AssignReference(enemyBase, "_movement", movement, report);
AssignReference(enemyBase, "_animancer", animancer, report);
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
@@ -555,16 +588,32 @@ namespace BaseGames.Editor
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
AssignAsset(strikeAbility, "_config", report, false, "ABL_E002_Strike");
AssignReference(strikeAbility, "_attackHitBox", atkHitBox, report);
AssignReference(strikeAbility, "_hurtBox", hurtBox, report);
AssignAsset(movement, "_config", report, false, "ENM_E002_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E002_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr, report);
AssignLayerMask(movement, "_groundMask",
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
report);
AssignReference(dropAbility, "_landingHitBox", landingHitBox, report);
AssignLayerMask(dropAbility, "_groundMask",
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(atkHitBox, "_defaultSource", dmgSrc, report);
if (dmgSrc != null)
{
AssignReference(landingHitBox, "_defaultSource", dmgSrc, report);
AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
}
SetupSensorHubSlotNames(sensorHub, new[] { "attack_range" }, report);
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_range" }, report);
report.Add("★ 将此对象放置于天花板,调整位置使 CapsuleCollider 正好贴合天花板底面。");
report.Add("★ HurtBox / ContactDamageZone 初始禁用;落地后由行为树激活。");
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E002_HuangZhi.asset。");
report.Add("★ BD 树逻辑建议Idle悬挂→ IsSensorDetecting(aggro) → UseAbility(CeilingDrop) → IsGrounded → Patrol(Pace)。");
Undo.CollapseUndoOperations(undoGroup);
Selection.activeGameObject = go;
@@ -573,7 +622,9 @@ namespace BaseGames.Editor
}
[MenuItem("BaseGames/Scene/Place/Enemy E003 (幼蛭)", priority = 113)]
public static void PlaceE003_YouZhi_Enemy()
public static void PlaceE003_YouZhi_Enemy() => PlaceE003_YouZhi_Enemy(EnemyBodyColliderType.Box);
public static void PlaceE003_YouZhi_Enemy(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
@@ -588,12 +639,12 @@ namespace BaseGames.Editor
rb.bodyType = RigidbodyType2D.Kinematic;
rb.gravityScale = 0f;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(0.5f, 0.6f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer sr3 = SetupSpriteRenderer(go);
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.5f, 0.6f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr3 = SetupSpriteRenderer(visual.gameObject);
E003_YouZhi enemyBase = GetOrAddComponent<E003_YouZhi>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
@@ -601,7 +652,7 @@ namespace BaseGames.Editor
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
@@ -638,6 +689,7 @@ namespace BaseGames.Editor
AssignAsset(movement, "_config", report, false, "ENM_E003_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E003_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr3, report);
AssignLayerMask(movement, "_groundMask",
@@ -650,7 +702,7 @@ namespace BaseGames.Editor
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
SetupSensorHubSlotNames(sensorHub, new[] { "aggro" }, report);
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
report.Add("★ 将此对象放置于天花板下方E003_YouZhi 会在 OnSpawn/ActivateFromCeiling 时执行下坠。");
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E003_YouZhi.asset。");
@@ -661,7 +713,9 @@ namespace BaseGames.Editor
}
[MenuItem("BaseGames/Scene/Place/Enemy E004 (蛭母)", priority = 114)]
public static void PlaceE004_ZhiMu_Enemy()
public static void PlaceE004_ZhiMu_Enemy() => PlaceE004_ZhiMu_Enemy(EnemyBodyColliderType.Box);
public static void PlaceE004_ZhiMu_Enemy(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
@@ -678,12 +732,12 @@ namespace BaseGames.Editor
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(0.8f, 1.2f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer sr4 = SetupSpriteRenderer(go);
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.8f, 1.2f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr4 = SetupSpriteRenderer(visual.gameObject);
E004_ZhiMu enemyBase = GetOrAddComponent<E004_ZhiMu>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
@@ -692,7 +746,7 @@ namespace BaseGames.Editor
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
@@ -756,6 +810,7 @@ namespace BaseGames.Editor
AssignAsset(movement, "_config", report, false, "ENM_E004_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E004_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr4, report);
AssignLayerMask(movement, "_groundMask",
@@ -772,7 +827,6 @@ namespace BaseGames.Editor
AssignReference(slamAbl, "_hitBox", slamHitBox, report);
AssignReference(acidAbl, "_muzzle", acidMuzzleT, report);
AssignReference(chargeAbl, "_chargeHitBox", chargeHitBox, report);
AssignReference(chaseAbl, "_sensorHub", sensorHub, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null)
@@ -782,7 +836,7 @@ namespace BaseGames.Editor
AssignReference(chargeHitBox, "_defaultSource", dmgSrc, report);
}
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "los" }, report);
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E004_ZhiMu.asset。");
Undo.CollapseUndoOperations(undoGroup);
@@ -792,7 +846,9 @@ namespace BaseGames.Editor
}
[MenuItem("BaseGames/Scene/Place/Enemy E005 (肥蛭)", priority = 115)]
public static void PlaceE005_FeiZhi_Enemy()
public static void PlaceE005_FeiZhi_Enemy() => PlaceE005_FeiZhi_Enemy(EnemyBodyColliderType.Box);
public static void PlaceE005_FeiZhi_Enemy(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
@@ -809,12 +865,12 @@ namespace BaseGames.Editor
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(0.9f, 1.0f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer sr5 = SetupSpriteRenderer(go);
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.9f, 1.0f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr5 = SetupSpriteRenderer(visual.gameObject);
E005_FeiZhi enemyBase = GetOrAddComponent<E005_FeiZhi>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
@@ -822,7 +878,7 @@ namespace BaseGames.Editor
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
@@ -863,6 +919,7 @@ namespace BaseGames.Editor
AssignAsset(movement, "_config", report, false, "ENM_E005_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E005_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr5, report);
AssignLayerMask(movement, "_groundMask",
@@ -878,7 +935,7 @@ namespace BaseGames.Editor
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null) AssignReference(biteHitBox, "_defaultSource", dmgSrc, report);
SetupSensorHubSlotNames(sensorHub, new[] { "aggro" }, report);
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
report.Add("★ 在 E005_FeiZhi._deathPreClip 上添加 AnimationEvent 调用 SpawnProjectile(\"spawn_e003\")。");
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E005_FeiZhi.asset。");
@@ -889,7 +946,9 @@ namespace BaseGames.Editor
}
[MenuItem("BaseGames/Scene/Place/Enemy E006 (讙)", priority = 116)]
public static void PlaceE006_Huan()
public static void PlaceE006_Huan() => PlaceE006_Huan(EnemyBodyColliderType.Box);
public static void PlaceE006_Huan(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
@@ -906,12 +965,12 @@ namespace BaseGames.Editor
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(0.7f, 1.0f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer sr6 = SetupSpriteRenderer(go);
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(0.7f, 1.0f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer sr6 = SetupSpriteRenderer(visual.gameObject);
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
@@ -919,7 +978,7 @@ namespace BaseGames.Editor
GetOrAddComponent<EnemyNavAgent>(go);
GetOrAddComponent<NavAgent>(go);
GetOrAddComponent<TransformBasedMovement>(go); // required by EnemyNavAgent [RequireComponent]
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
@@ -966,6 +1025,7 @@ namespace BaseGames.Editor
AssignAsset(movement, "_config", report, false, "ENM_E006_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_E006_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", sr6, report);
AssignLayerMask(movement, "_groundMask",
@@ -977,7 +1037,6 @@ namespace BaseGames.Editor
AssignReference(leapAbl, "_landingHitBox", landHitBox, report);
AssignReference(chaseAbl, "_contactDamage", bodyContact, report);
AssignReference(chaseAbl, "_sensorHub", sensorHub, report);
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
if (dmgSrc != null)
@@ -986,7 +1045,7 @@ namespace BaseGames.Editor
AssignReference(landHitBox, "_defaultSource", dmgSrc, report);
}
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "wall_ahead", "ledge" }, report);
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E006_Huan.asset。");
Undo.CollapseUndoOperations(undoGroup);
@@ -996,7 +1055,9 @@ namespace BaseGames.Editor
}
[MenuItem("BaseGames/Scene/Place/Boss 嘲风 (ChaoFeng)", priority = 117)]
public static void PlaceChaoFeng()
public static void PlaceChaoFeng() => PlaceChaoFeng(EnemyBodyColliderType.Box);
public static void PlaceChaoFeng(EnemyBodyColliderType bodyCollider)
{
var report = new List<string>();
int undoGroup = Undo.GetCurrentGroup();
@@ -1014,12 +1075,12 @@ namespace BaseGames.Editor
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
body.size = new Vector2(1.2f, 2.0f);
GetOrAddComponent<Animator>(go);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
SpriteRenderer srBoss = SetupSpriteRenderer(go);
Collider2D body = CreateBodyCollider(go, bodyCollider, new Vector2(1.2f, 2.0f));
Transform visual = GetOrCreateChild(go.transform, "Visual");
visual.localPosition = (Vector3)(Vector2)body.offset;
GetOrAddComponent<Animator>(visual.gameObject);
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(visual.gameObject);
SpriteRenderer srBoss = SetupSpriteRenderer(visual.gameObject);
ChaoFengBoss bossBase = GetOrAddComponent<ChaoFengBoss>(go);
EnemyStats bossStats = GetOrAddComponent<EnemyStats>(go);
@@ -1031,7 +1092,7 @@ namespace BaseGames.Editor
BossSkillExecutor skillExec = GetOrAddComponent<BossSkillExecutor>(go);
ChaoFengFloatController floatCtrl = GetOrAddComponent<ChaoFengFloatController>(go);
ChaoFengKnockdownCounter knockdown = GetOrAddComponent<ChaoFengKnockdownCounter>(go);
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
// HurtBox
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
@@ -1078,6 +1139,7 @@ namespace BaseGames.Editor
AssignAsset(movement, "_config", report, false, "ENM_ChaoFeng_Stats");
AssignAsset(movement, "_animConfig", report, false, "ENM_ChaoFeng_AnimConfig");
AssignReference(movement, "_visualRoot", visual, report);
AssignReference(movement, "_animancer", animancer, report);
AssignReference(movement, "_spriteRenderer", srBoss, report);
AssignLayerMask(movement, "_groundMask",
@@ -1103,7 +1165,7 @@ namespace BaseGames.Editor
if (hb != null) AssignReference(hb, "_defaultSource", dmgSrc, report);
}
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
report.Add("★ 设置 BossSkillExecutor._bossId = \"ChaoFeng\"。");
report.Add("★ 将各 Phase1 HitBox 引用拖入 BossSkillExecutor._hitBoxes 数组。");
@@ -1145,20 +1207,56 @@ namespace BaseGames.Editor
}
/// <summary>
/// 设置 EnemySensorHub._slots 的 slotName 字段Sensor 引用需在 Inspector 中手工绑定)。
/// 在 <see cref="PhysicsPerceptionSystem"/> 上预填充 <c>_slots</c> 数组,
/// 根据 slotName 自动选择类型、半径、检测层及 GizmoColor。
/// </summary>
private static void SetupSensorHubSlotNames(EnemySensorHub hub, string[] slotNames, List<string> report)
private static void SetupPerceptionSystemSlots(PhysicsPerceptionSystem system, string[] slotNames, List<string> report)
{
var so = new SerializedObject(hub);
var so = new SerializedObject(system);
var slots = so.FindProperty("_slots");
if (slots == null || !slots.isArray)
{
report?.Add("EnemySensorHub._slots 字段未找到,传感器槽位需手工配置。");
report?.Add("PhysicsPerceptionSystem._slots 字段未找到,请检查脚本序列化。");
return;
}
int playerLayer = LayerMask.GetMask("Player");
slots.arraySize = slotNames.Length;
for (int i = 0; i < slotNames.Length; i++)
slots.GetArrayElementAtIndex(i).FindPropertyRelative("slotName").stringValue = slotNames[i];
{
var elem = slots.GetArrayElementAtIndex(i);
string name = slotNames[i];
elem.FindPropertyRelative("slotName").stringValue = name;
int enumIdx = 0; // RangeCircle
float radius = 3f;
int layer = playerLayer;
switch (name)
{
case "aggro": enumIdx = 0; radius = 5f; layer = playerLayer; break;
case "los": enumIdx = 1; radius = 0f; layer = 0; break;
case "attack_melee":enumIdx = 0; radius = 1.5f; layer = playerLayer; break;
case "attack_range":enumIdx = 0; radius = 8f; layer = playerLayer; break;
}
elem.FindPropertyRelative("type").enumValueIndex = enumIdx;
elem.FindPropertyRelative("radius").floatValue = radius;
elem.FindPropertyRelative("detectLayer").intValue = layer;
// 各 slot 分配语义化默认颜色,可在 Inspector 中按需覆盖
Color defaultColor = name switch
{
"aggro" => new Color(1.00f, 0.60f, 0.10f, 1f), // 橙
"los" => new Color(0.00f, 0.80f, 1.00f, 1f), // 青
"attack_melee" => new Color(1.00f, 0.20f, 0.20f, 1f), // 红
"attack_range" => new Color(1.00f, 0.40f, 0.60f, 1f), // 粉红
_ => Color.clear, // 未知 slot 回退为紫色
};
elem.FindPropertyRelative("gizmoColor").colorValue = defaultColor;
}
so.ApplyModifiedPropertiesWithoutUndo();
}
@@ -1713,6 +1811,25 @@ namespace BaseGames.Editor
return Vector3.zero;
}
private static Collider2D CreateBodyCollider(GameObject go, EnemyBodyColliderType type, Vector2 size)
{
switch (type)
{
case EnemyBodyColliderType.Capsule:
var cap = GetOrAddComponent<CapsuleCollider2D>(go);
cap.size = size;
return cap;
case EnemyBodyColliderType.Circle:
var cir = GetOrAddComponent<CircleCollider2D>(go);
cir.radius = Mathf.Min(size.x, size.y) * 0.5f;
return cir;
default: // Box
var box = GetOrAddComponent<BoxCollider2D>(go);
box.size = size;
return box;
}
}
private static T GetOrAddComponent<T>(GameObject go) where T : Component
{
T comp = go.GetComponent<T>();