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

@@ -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>();