feat(enemy): 嘲风迁移到新轨——能力资产 + 对象树 + 开战触发
删 5 个空 BossSkillSO,用向导重建 6 个 EnemyAbilitySO(含入场演出)。 预制体经放置工具重新产出:EnemyAiBrain(_definitionId=ChaoFeng) + BossPhaseAbilityGate + 6 个能力组件,不再有 BossSkillExecutor。 TestRoomA 加开战触发区。三件自检 0 error,EditMode 260/260。 首次真正跑通两条脚手架,暴露并修掉三个它们的缺陷: 1. 放置工具漏挂 EnemyLocomotion(PlaceChaoFeng 及其余 7 个敌人放置入口)。 EnemyBase.Awake 找不到它即报错且 Locomotion 为 null, AiStateFragments.ApplyLocomotion 直接 NRE、AiRuntime 构造失败—— Boss 建不出决策层,完全不动。已在所有敌人放置入口补齐。 2. 向导把空中阶段的 wind_stone 建成 requiresGrounded=true(SO 默认值)。 Boss 浮空后 IsGrounded 恒 false,选招器把它滤掉,空中阶段站着不打。 能力定义表加 grounded 列,阶段 1 的招显式置 false。 3. BossPhaseAbilityGate.ApplyPhase 只在 EnterPhase 与 OnSpawn 调用, 直接摆场景里的 Boss 两条路径都不走,开局后续阶段的招仍在候选池里。 BossBase.Start 补一次初始阶段应用。 另:Phase1_Tornado_HitBox 确认零消费者(龙卷判定随弹体走), 从放置工具删除其创建与伤害源绑定; EnemyBrainContext.UseAbility 对解析不到的能力 id 改为 LogError, 不再静默返回 false(CLAUDE.md 第 6 条); 新增 PlaceBossFightTrigger 脚手架(此前无对应创建入口,属裸建)。 播放模式实测状态序列(TestRoomA,玩家走进触发区): Wait →(Engaged) Intro → Ground⇄GroundAttack →(hp<50%) PhaseTransition → Air⇄AirAttack,浮空生效,阶段门按阶段换池,全程 0 报错。 动画 Clip 未接入,PlayClipAbility 空 clip 立即完成,故只验证了状态流转, 判定时序待美术接入后再校。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -302,6 +302,7 @@ namespace BaseGames.Editor
|
||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<BaseGames.Enemies.Navigation.GroundNavigator>(go); // 地面导航(直接移动,无寻路图)
|
||||
GetOrAddComponent<EnemyLocomotion>(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
|
||||
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||
|
||||
var (hurtBox, bodyContact, contactHitBox) =
|
||||
@@ -445,6 +446,7 @@ namespace BaseGames.Editor
|
||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<BaseGames.Enemies.Navigation.GroundNavigator>(go); // 地面导航(直接移动,无寻路图)
|
||||
GetOrAddComponent<EnemyLocomotion>(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
|
||||
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||
|
||||
var (hurtBox, bodyContact, contactHitBox) =
|
||||
@@ -493,9 +495,7 @@ namespace BaseGames.Editor
|
||||
|
||||
SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report);
|
||||
|
||||
GetOrAddComponent<EnemyLocomotion>(go);
|
||||
|
||||
// BrainGraph AI:挂载决策组件并绑定 AI 配方资产(取代旧的 Opsive BehaviorTree)
|
||||
// BrainGraph AI:挂载决策组件并绑定 AI 配方资产
|
||||
var brain = GetOrAddComponent<EnemyAiBrain>(go);
|
||||
var recipe = AssetDatabase.LoadAssetAtPath<PerceptionRecipeSO>(
|
||||
"Assets/_Game/Data/Enemies/E001/ENM_E001_Ai.asset");
|
||||
@@ -550,6 +550,7 @@ namespace BaseGames.Editor
|
||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<BaseGames.Enemies.Navigation.GroundNavigator>(go); // 地面导航(直接移动,无寻路图)
|
||||
GetOrAddComponent<EnemyLocomotion>(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
|
||||
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||
|
||||
// HurtBox + ContactDamageZone(身体接触伤害常驻开启;HurtBox 悬挂期外关闭见下)
|
||||
@@ -651,6 +652,7 @@ namespace BaseGames.Editor
|
||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<BaseGames.Enemies.Navigation.GroundNavigator>(go); // 地面导航(直接移动,无寻路图)
|
||||
GetOrAddComponent<EnemyLocomotion>(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
|
||||
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||
|
||||
var (hurtBox, bodyContact, contactHitBox) =
|
||||
@@ -741,6 +743,7 @@ namespace BaseGames.Editor
|
||||
EnemyFeedback feedback = GetOrAddComponent<EnemyFeedback>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<BaseGames.Enemies.Navigation.GroundNavigator>(go); // 地面导航(直接移动,无寻路图)
|
||||
GetOrAddComponent<EnemyLocomotion>(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
|
||||
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||
|
||||
var (hurtBox, bodyContact, contactHitBox) =
|
||||
@@ -871,6 +874,7 @@ namespace BaseGames.Editor
|
||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<BaseGames.Enemies.Navigation.GroundNavigator>(go); // 地面导航(直接移动,无寻路图)
|
||||
GetOrAddComponent<EnemyLocomotion>(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
|
||||
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||
|
||||
var (hurtBox, bodyContact, contactHitBox) =
|
||||
@@ -982,6 +986,7 @@ namespace BaseGames.Editor
|
||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<BaseGames.Enemies.Navigation.GroundNavigator>(go); // 地面导航(直接移动,无寻路图)
|
||||
GetOrAddComponent<EnemyLocomotion>(go); // 移动执行器:EnemyBase.Awake 必须找得到,否则 AI 图的移动意图无处落地
|
||||
PhysicsPerceptionSystem sensorHub = GetOrAddComponent<PhysicsPerceptionSystem>(go);
|
||||
|
||||
var (hurtBox, bodyContact, contactHitBox) =
|
||||
@@ -1084,6 +1089,10 @@ namespace BaseGames.Editor
|
||||
EnemyFeedback feedback = GetOrAddComponent<EnemyFeedback>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<BaseGames.Enemies.Navigation.GroundNavigator>(go); // 地面导航(直接移动,无寻路图)
|
||||
// 移动执行器:AI 图的移动意图(Idle/Face/Approach)全靠它落地。
|
||||
// 少了它 EnemyBase.Awake 就报「未找到 EnemyLocomotion」,且 Locomotion 为 null,
|
||||
// 建图时 AiStateFragments.ApplyLocomotion 会 NRE,AiRuntime 构造失败 → Boss 完全不动。
|
||||
GetOrAddComponent<EnemyLocomotion>(go);
|
||||
EnemyAiBrain brain = GetOrAddComponent<EnemyAiBrain>(go);
|
||||
BossPhaseAbilityGate phaseGate = GetOrAddComponent<BossPhaseAbilityGate>(go);
|
||||
ChaoFengFloatController floatCtrl = GetOrAddComponent<ChaoFengFloatController>(go);
|
||||
@@ -1094,16 +1103,16 @@ namespace BaseGames.Editor
|
||||
var (hurtBox, bodyContact, contactHitBox) =
|
||||
SetupHurtAndContactBoxes(go, size, report);
|
||||
|
||||
// Phase1 攻击 HitBox(默认禁用;由能力按 EnemyAttackSO.hitBoxSlot 归一化时机开关)。
|
||||
// 计划:挥扇三连 FanCombo ×3 + 龙卷接触 Tornado。
|
||||
// 近战攻击 HitBox(默认禁用;由 MeleeAttackAbility 按 EnemyAttackSO.hitBoxSlot 归一化时机开关)。
|
||||
// 只有挥扇三连需要 Boss 自身的判定;回旋扇 / 龙卷 / 风石都是弹体招,
|
||||
// 判定随 SpawnProjectile 动画事件生成的弹体走,Boss 身上不留对应 HitBox
|
||||
// (留一个无人引用的 HitBox 会让作者误以为该招判定挂在 Boss 身上)。
|
||||
HitBox fan1 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_1", "EnemyHitBox",
|
||||
true, report, size: new Vector2(1.0f, 0.5f));
|
||||
HitBox fan2 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_2", "EnemyHitBox",
|
||||
true, report, size: new Vector2(1.0f, 0.5f));
|
||||
HitBox fan3 = CreateDisabledHitBox(go.transform, "Phase1_FanCombo_HitBox_3", "EnemyHitBox",
|
||||
true, report, size: new Vector2(1.2f, 0.6f));
|
||||
HitBox tornadoHB = CreateDisabledHitBox(go.transform, "Phase1_Tornado_HitBox", "EnemyHitBox",
|
||||
true, report, size: new Vector2(0.6f, 1.2f));
|
||||
|
||||
// 弹体发射点(Phase1 回旋扇 / 龙卷;Phase2 风石)
|
||||
Transform boomerangMuzzleT = GetOrCreateChild(go.transform, "BoomerangMuzzle");
|
||||
@@ -1212,7 +1221,7 @@ namespace BaseGames.Editor
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody");
|
||||
if (dmgSrc != null)
|
||||
{
|
||||
foreach (var hb in new[] { fan1, fan2, fan3, tornadoHB, contactHitBox })
|
||||
foreach (var hb in new[] { fan1, fan2, fan3, contactHitBox })
|
||||
if (hb != null) AssignReference(hb, "_defaultSource", dmgSrc, report);
|
||||
}
|
||||
|
||||
@@ -1235,6 +1244,75 @@ namespace BaseGames.Editor
|
||||
MarkDirtyAndLog("Boss 嘲风 (ChaoFeng)", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Boss 开战触发区 (嘲风)", priority = 118)]
|
||||
public static void PlaceChaoFengFightTrigger() => PlaceBossFightTrigger("ChaoFeng");
|
||||
|
||||
/// <summary>
|
||||
/// 放置 Boss 开战触发区:玩家进入即广播开战事件并让 Boss 决策层进战。
|
||||
/// 没有它,Boss 会一直停在静置态——开战事件频道此前全项目零发送方。
|
||||
/// </summary>
|
||||
/// <param name="bossId">
|
||||
/// Boss id。既写进触发器的 <c>_bossId</c>(BGM / 血条按它定位 Boss),
|
||||
/// 也用来在活动场景里定位目标 Boss——按 <c>EnemyAiBrain._definitionId</c> 匹配。
|
||||
/// </param>
|
||||
public static void PlaceBossFightTrigger(string bossId)
|
||||
{
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place Boss Fight Trigger");
|
||||
|
||||
GameObject go = new GameObject($"BossFightTrigger_{bossId}");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place Boss Fight Trigger");
|
||||
go.transform.position = GetDropPosition();
|
||||
SetLayer(go, "TriggerZone", report);
|
||||
|
||||
// 碰撞体必须先于 BossFightTrigger 挂上:该组件标了 [RequireComponent(typeof(Collider2D))],
|
||||
// 而 Collider2D 是抽象类,Unity 补不出来,AddComponent 会返回真正的 null。
|
||||
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(go);
|
||||
col.isTrigger = true;
|
||||
col.size = new Vector2(4f, 6f); // 门口尺寸占位,按竞技场入口在 Inspector 调整
|
||||
|
||||
BossFightTrigger trigger = GetOrAddComponent<BossFightTrigger>(go);
|
||||
|
||||
AssignString(trigger, "_bossId", bossId, report);
|
||||
AssignLayerMask(trigger, "_playerLayers", "Player", report);
|
||||
AssignAsset(trigger, "_onBossFightStarted", report, true, "EVT_BossFightStarted");
|
||||
// 开关频道:资产名叫 EVT_BossFightEnded,但实际是「战斗中 true/false」的开关,
|
||||
// BGMController 与 BossHPBar 绑的也是它。名字与用途不符是既存问题,此处沿用同一资产。
|
||||
AssignAsset(trigger, "_onBossFightToggled", report, true, "EVT_BossFightEnded");
|
||||
|
||||
// 目标 Boss:按 EnemyAiBrain._definitionId 在活动场景里精确匹配,不做名字模糊猜测。
|
||||
// 匹配不到(或匹配到多个)一律如实报告,不塞一个凑数的引用——
|
||||
// 漏绑时 BossFightTrigger.Awake 也会再报一次并停用自己。
|
||||
var matches = new List<EnemyAiBrain>();
|
||||
foreach (var root in UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().GetRootGameObjects())
|
||||
foreach (var b in root.GetComponentsInChildren<EnemyAiBrain>(true))
|
||||
if (new SerializedObject(b).FindProperty("_definitionId")?.stringValue == bossId)
|
||||
matches.Add(b);
|
||||
|
||||
if (matches.Count == 1)
|
||||
{
|
||||
AssignReference(trigger, "_bossBrain", matches[0], report);
|
||||
report.Add($"[OK] _bossBrain → {matches[0].name}(_definitionId={bossId})");
|
||||
}
|
||||
else if (matches.Count == 0)
|
||||
{
|
||||
report.Add($"✗ 活动场景里找不到 _definitionId={bossId} 的 EnemyAiBrain," +
|
||||
"_bossBrain 未绑定。请先放置该 Boss,再重跑本操作。");
|
||||
}
|
||||
else
|
||||
{
|
||||
report.Add($"✗ 活动场景里有 {matches.Count} 个 _definitionId={bossId} 的 EnemyAiBrain," +
|
||||
"无法确定目标,_bossBrain 未绑定。请先清理重复的 Boss。");
|
||||
}
|
||||
|
||||
report.Add("★ 把触发区拖到竞技场入口并按门口尺寸调整 BoxCollider2D。只触发一次。");
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog($"Boss 开战触发区 ({bossId})", go, report);
|
||||
}
|
||||
|
||||
// ══ 敌人放置辅助方法 ═══════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user