From ebf0c97320f245c63d0b5b5896e9fdcf0521463e Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 9 Jun 2026 15:56:44 +0800 Subject: [PATCH] =?UTF-8?q?refactor(enemy):=20=E6=95=8C=E4=BA=BA=E4=B8=93?= =?UTF-8?q?=E5=B1=9E=E5=AD=90=E7=B1=BB=E6=94=B9=E4=B8=BA=E9=9B=B6=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E9=85=8D=E7=BD=AE=E5=9E=8B=E8=A1=8C=E4=B8=BA=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Prefabs/Enemies/E003/ENM_YouZhi.prefab | 18 +++- .../Prefabs/Enemies/E004/ENM_ZhiMu.prefab | 46 ++++++---- .../Prefabs/Enemies/E005/ENM_FeiZhi.prefab | 65 ++++++++++---- Assets/_Game/Scenes/Persistent.unity | 32 +++---- .../Editor/Scene/SceneObjectPlacerTool.cs | 35 ++++++-- Assets/_Game/Scripts/Enemies/Behaviors.meta | 8 ++ .../Enemies/Behaviors/EnemyAbilityTrigger.cs | 59 +++++++++++++ .../EnemyAbilityTrigger.cs.meta} | 2 +- .../Behaviors/EnemyBehaviorInterfaces.cs | 29 +++++++ .../EnemyBehaviorInterfaces.cs.meta} | 2 +- .../Enemies/Behaviors/EnemyDeathSequence.cs | 86 +++++++++++++++++++ .../EnemyDeathSequence.cs.meta} | 2 +- .../Enemies/Behaviors/EnemySpawnerOnEvent.cs | 46 ++++++++++ .../Behaviors/EnemySpawnerOnEvent.cs.meta | 11 +++ Assets/_Game/Scripts/Enemies/E003_YouZhi.cs | 34 -------- Assets/_Game/Scripts/Enemies/E004_ZhiMu.cs | 45 ---------- Assets/_Game/Scripts/Enemies/E005_FeiZhi.cs | 66 -------------- Assets/_Game/Scripts/Enemies/EnemyBase.cs | 70 +++++++++++++-- Docs/Architecture/07_EnemyModule.md | 19 +++- Docs/Guides/02_Enemy_Boss_Setup_Guide.md | 47 +++++----- .../Guides/08_BehaviorTree_Authoring_Guide.md | 4 +- zeling_v2.sln | 12 +-- 22 files changed, 496 insertions(+), 242 deletions(-) create mode 100644 Assets/_Game/Scripts/Enemies/Behaviors.meta create mode 100644 Assets/_Game/Scripts/Enemies/Behaviors/EnemyAbilityTrigger.cs rename Assets/_Game/Scripts/Enemies/{E005_FeiZhi.cs.meta => Behaviors/EnemyAbilityTrigger.cs.meta} (83%) create mode 100644 Assets/_Game/Scripts/Enemies/Behaviors/EnemyBehaviorInterfaces.cs rename Assets/_Game/Scripts/Enemies/{E003_YouZhi.cs.meta => Behaviors/EnemyBehaviorInterfaces.cs.meta} (83%) create mode 100644 Assets/_Game/Scripts/Enemies/Behaviors/EnemyDeathSequence.cs rename Assets/_Game/Scripts/Enemies/{E004_ZhiMu.cs.meta => Behaviors/EnemyDeathSequence.cs.meta} (83%) create mode 100644 Assets/_Game/Scripts/Enemies/Behaviors/EnemySpawnerOnEvent.cs create mode 100644 Assets/_Game/Scripts/Enemies/Behaviors/EnemySpawnerOnEvent.cs.meta delete mode 100644 Assets/_Game/Scripts/Enemies/E003_YouZhi.cs delete mode 100644 Assets/_Game/Scripts/Enemies/E004_ZhiMu.cs delete mode 100644 Assets/_Game/Scripts/Enemies/E005_FeiZhi.cs diff --git a/Assets/_Game/Prefabs/Enemies/E003/ENM_YouZhi.prefab b/Assets/_Game/Prefabs/Enemies/E003/ENM_YouZhi.prefab index d5d46a5..b219580 100644 --- a/Assets/_Game/Prefabs/Enemies/E003/ENM_YouZhi.prefab +++ b/Assets/_Game/Prefabs/Enemies/E003/ENM_YouZhi.prefab @@ -414,6 +414,7 @@ GameObject: - component: {fileID: 3801052615690156945} - component: {fileID: 1497225151565698519} - component: {fileID: 1137051351926306612} + - component: {fileID: 8183142527609344657} m_Layer: 13 m_Name: ENM_YouZhi m_TagString: Untagged @@ -521,7 +522,7 @@ MonoBehaviour: m_GameObject: {fileID: 6255869283652534460} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: d86a36c2999f88842a212d095749c349, type: 3} + m_Script: {fileID: 11500000, guid: 1a2dbfbcc31a4c34cbd3ac893f02e07d, type: 3} m_Name: m_EditorClassIdentifier: _enemyId: @@ -547,7 +548,6 @@ MonoBehaviour: _dbg_LastKnownPos: {x: 0, y: 0} _dbg_BtTickInterval: 0 _autoPlayPhaseAnimation: 1 - _activateOnSpawn: 1 --- !u!114 &3136685549398515749 MonoBehaviour: m_ObjectHideFlags: 0 @@ -748,6 +748,20 @@ MonoBehaviour: obstructLayer: serializedVersion: 2 m_Bits: 0 +--- !u!114 &8183142527609344657 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6255869283652534460} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3198979f0bd38e1429478e7937b280b3, type: 3} + m_Name: + m_EditorClassIdentifier: + _abilityId: e003_fall + _executeOnSpawn: 1 --- !u!1 &7519275599598288895 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/_Game/Prefabs/Enemies/E004/ENM_ZhiMu.prefab b/Assets/_Game/Prefabs/Enemies/E004/ENM_ZhiMu.prefab index a241dc0..75eeb95 100644 --- a/Assets/_Game/Prefabs/Enemies/E004/ENM_ZhiMu.prefab +++ b/Assets/_Game/Prefabs/Enemies/E004/ENM_ZhiMu.prefab @@ -761,6 +761,7 @@ GameObject: - component: {fileID: 9180313924888131203} - component: {fileID: 1758362558550688781} - component: {fileID: 1140989549255509988} + - component: {fileID: 4917575637238364831} m_Layer: 13 m_Name: ENM_ZhiMu m_TagString: Untagged @@ -870,7 +871,7 @@ MonoBehaviour: m_GameObject: {fileID: 7501196512915604413} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: cf8f8c7225dca9c42b5a451b177319b9, type: 3} + m_Script: {fileID: 11500000, guid: 1a2dbfbcc31a4c34cbd3ac893f02e07d, type: 3} m_Name: m_EditorClassIdentifier: _enemyId: @@ -896,20 +897,6 @@ MonoBehaviour: _dbg_LastKnownPos: {x: 0, y: 0} _dbg_BtTickInterval: 0 _autoPlayPhaseAnimation: 1 - _deathPreClip: - _FadeDuration: 0.25 - _Speed: 1 - _Events: - _NormalizedTimes: [] - _Callbacks: [] - _Names: [] - _Clip: {fileID: 0} - _NormalizedStartTime: NaN - _hurtBox: {fileID: 0} - _deathPreDuration: 3 - references: - version: 2 - RefIds: [] --- !u!114 &7254417954483924161 MonoBehaviour: m_ObjectHideFlags: 0 @@ -1179,3 +1166,32 @@ MonoBehaviour: obstructLayer: serializedVersion: 2 m_Bits: 0 +--- !u!114 &4917575637238364831 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7501196512915604413} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4a4a8aad8881b4543a0918321e7efe3e, type: 3} + m_Name: + m_EditorClassIdentifier: + _deathPreClip: + _FadeDuration: 0.25 + _Speed: 1 + _Events: + _NormalizedTimes: [] + _Callbacks: [] + _Names: [] + _Clip: {fileID: 0} + _NormalizedStartTime: NaN + _duration: 3 + _hurtBoxesToDisable: + - {fileID: 6064615757515489567} + _stopBehaviorTree: 1 + _stopMovement: 1 + references: + version: 2 + RefIds: [] diff --git a/Assets/_Game/Prefabs/Enemies/E005/ENM_FeiZhi.prefab b/Assets/_Game/Prefabs/Enemies/E005/ENM_FeiZhi.prefab index 6a722bb..d7435fe 100644 --- a/Assets/_Game/Prefabs/Enemies/E005/ENM_FeiZhi.prefab +++ b/Assets/_Game/Prefabs/Enemies/E005/ENM_FeiZhi.prefab @@ -267,6 +267,8 @@ GameObject: - component: {fileID: 7306072729481347792} - component: {fileID: 6593689935047063830} - component: {fileID: 7475404416877533072} + - component: {fileID: 8876688377798292022} + - component: {fileID: 1269485901178456362} m_Layer: 13 m_Name: ENM_FeiZhi m_TagString: Untagged @@ -375,7 +377,7 @@ MonoBehaviour: m_GameObject: {fileID: 3986905312391723074} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f2460e8735a4dc5409fe6b0949bd65c0, type: 3} + m_Script: {fileID: 11500000, guid: 1a2dbfbcc31a4c34cbd3ac893f02e07d, type: 3} m_Name: m_EditorClassIdentifier: _enemyId: @@ -401,22 +403,6 @@ MonoBehaviour: _dbg_LastKnownPos: {x: 0, y: 0} _dbg_BtTickInterval: 0 _autoPlayPhaseAnimation: 1 - _deathPreClip: - _FadeDuration: 0.25 - _Speed: 1 - _Events: - _NormalizedTimes: [] - _Callbacks: [] - _Names: [] - _Clip: {fileID: 0} - _NormalizedStartTime: NaN - _hurtBox: {fileID: 0} - _deathPreDuration: 3 - _spawnCount: 3 - _spawnRadius: 1.5 - references: - version: 2 - RefIds: [] --- !u!114 &1218518018103485926 MonoBehaviour: m_ObjectHideFlags: 0 @@ -669,6 +655,51 @@ MonoBehaviour: obstructLayer: serializedVersion: 2 m_Bits: 0 +--- !u!114 &8876688377798292022 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3986905312391723074} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4a4a8aad8881b4543a0918321e7efe3e, type: 3} + m_Name: + m_EditorClassIdentifier: + _deathPreClip: + _FadeDuration: 0.25 + _Speed: 1 + _Events: + _NormalizedTimes: [] + _Callbacks: [] + _Names: [] + _Clip: {fileID: 0} + _NormalizedStartTime: NaN + _duration: 3 + _hurtBoxesToDisable: + - {fileID: 9041154183844542258} + _stopBehaviorTree: 1 + _stopMovement: 1 + references: + version: 2 + RefIds: [] +--- !u!114 &1269485901178456362 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3986905312391723074} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fd9abafdb6bedcc4b8fd011bde9208b6, type: 3} + m_Name: + m_EditorClassIdentifier: + _payloadKey: spawn_e003 + _poolKey: ENM_YouZhi + _count: 3 + _radius: 1.5 --- !u!1 &5960285064422745315 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/_Game/Scenes/Persistent.unity b/Assets/_Game/Scenes/Persistent.unity index 35c0e58..5d0f799 100644 --- a/Assets/_Game/Scenes/Persistent.unity +++ b/Assets/_Game/Scenes/Persistent.unity @@ -289,8 +289,8 @@ RectTransform: m_Children: [] m_Father: {fileID: 1915171111} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0.5, y: 0.5} - m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 100, y: 100} m_Pivot: {x: 0.5, y: 0.5} @@ -385,8 +385,8 @@ RectTransform: m_Children: [] m_Father: {fileID: 1915171111} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0.5, y: 0.5} - m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 100, y: 100} m_Pivot: {x: 0.5, y: 0.5} @@ -3233,7 +3233,7 @@ MonoBehaviour: m_spriteAsset: {fileID: 0} m_tintAllSprites: 0 m_StyleSheet: {fileID: 0} - m_TextStyleHashCode: 0 + m_TextStyleHashCode: -1183493901 m_overrideHtmlColors: 0 m_faceColor: serializedVersion: 2 @@ -5509,7 +5509,7 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - CustomBlends: {fileID: 0} + CustomBlends: {fileID: 11400000, guid: 9906ca91115b34d498a040782ef36e92, type: 2} --- !u!81 &814710353 AudioListener: m_ObjectHideFlags: 0 @@ -5551,7 +5551,7 @@ Camera: height: 1 near clip plane: 0.1 far clip plane: 5000 - field of view: 40 + field of view: 10 orthographic: 0 orthographic size: 10 m_Depth: 0 @@ -5578,7 +5578,7 @@ Transform: m_GameObject: {fileID: 814710351} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalPosition: {x: -18.954266, y: 5.5, z: -64} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -6659,7 +6659,7 @@ MonoBehaviour: m_spriteAsset: {fileID: 0} m_tintAllSprites: 0 m_StyleSheet: {fileID: 0} - m_TextStyleHashCode: 0 + m_TextStyleHashCode: -1183493901 m_overrideHtmlColors: 0 m_faceColor: serializedVersion: 2 @@ -8402,7 +8402,7 @@ MonoBehaviour: m_spriteAsset: {fileID: 0} m_tintAllSprites: 0 m_StyleSheet: {fileID: 0} - m_TextStyleHashCode: 0 + m_TextStyleHashCode: -1183493901 m_overrideHtmlColors: 0 m_faceColor: serializedVersion: 2 @@ -11736,7 +11736,7 @@ MonoBehaviour: m_spriteAsset: {fileID: 0} m_tintAllSprites: 0 m_StyleSheet: {fileID: 0} - m_TextStyleHashCode: 0 + m_TextStyleHashCode: -1183493901 m_overrideHtmlColors: 0 m_faceColor: serializedVersion: 2 @@ -12817,8 +12817,8 @@ RectTransform: m_Children: [] m_Father: {fileID: 1915171111} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0.5, y: 0.5} - m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 100, y: 100} m_Pivot: {x: 0.5, y: 0.5} @@ -15318,8 +15318,8 @@ RectTransform: m_Children: [] m_Father: {fileID: 1915171111} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0.5, y: 0.5} - m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 100, y: 100} m_Pivot: {x: 0.5, y: 0.5} @@ -15460,7 +15460,7 @@ MonoBehaviour: m_spriteAsset: {fileID: 0} m_tintAllSprites: 0 m_StyleSheet: {fileID: 0} - m_TextStyleHashCode: 0 + m_TextStyleHashCode: -1183493901 m_overrideHtmlColors: 0 m_faceColor: serializedVersion: 2 diff --git a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs index a11e75d..d87d727 100644 --- a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs +++ b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs @@ -10,6 +10,7 @@ using BaseGames.Core.Pool; using BaseGames.Dialogue; using BaseGames.Enemies; using BaseGames.Enemies.Abilities; +using BaseGames.Enemies.Behaviors; using BaseGames.Enemies.Boss; using BaseGames.Enemies.Navigation; using BaseGames.Enemies.Perception; @@ -638,7 +639,7 @@ namespace BaseGames.Editor AnimancerComponent animancer = GetOrAddComponent(visual.gameObject); SpriteRenderer sr3 = SetupSpriteRenderer(visual.gameObject); - E003_YouZhi enemyBase = GetOrAddComponent(go); + EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); @@ -691,12 +692,17 @@ namespace BaseGames.Editor AssignAsset(fallAbility, "_config", report, false, "ABL_E003_Fall"); AssignReference(fallAbility, "_contactDamage", bodyContact, report); + // 出生 / 外部触发执行下坠能力(零代码:替代过去的 E003_YouZhi 专属脚本) + EnemyAbilityTrigger fallTrigger = GetOrAddComponent(go); + AssignString(fallTrigger, "_abilityId", "e003_fall", report); + AssignBool(fallTrigger, "_executeOnSpawn", true); + Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report); SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "los" }, report); - report.Add("★ 将此对象放置于天花板下方,E003_YouZhi 会在 OnSpawn/ActivateFromCeiling 时执行下坠。"); - report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E003_YouZhi.asset。"); + report.Add("★ 将此对象放置于天花板下方;EnemyAbilityTrigger 在出生时(executeOnSpawn)自动、或被场景触发器调用 Trigger() 时执行下坠能力(e003_fall)。"); + report.Add("★ 挂载行为树 BehaviorTree 组件,指定对应外部行为树资产。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; @@ -731,7 +737,7 @@ namespace BaseGames.Editor AnimancerComponent animancer = GetOrAddComponent(visual.gameObject); SpriteRenderer sr4 = SetupSpriteRenderer(visual.gameObject); - E004_ZhiMu enemyBase = GetOrAddComponent(go); + EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyFeedback feedback = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); @@ -819,10 +825,14 @@ namespace BaseGames.Editor AssignReference(slamHitBox, "_defaultSource", dmgSrc, report); } + // 死亡前摇无敌演出(零代码:替代过去的 E004_ZhiMu 专属 Die() 重写) + EnemyDeathSequence deathSeq = GetOrAddComponent(go); + AssignObjectArray(deathSeq, "_hurtBoxesToDisable", new Object[] { hurtBox }, report); + SetupPerceptionSystemSlots(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "sight" }, report); report.Add("★ AppearAbility._appearClip / FacePlayerAbility._faceClip 等动画 Clip 待美术接入后在 Inspector 指定。"); - report.Add("★ 在 E004_ZhiMu._deathPreClip 配置死亡前摇动画(两阶段死亡 Death_Pre 无敌)。"); - report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E004_ZhiMu.asset。"); + report.Add("★ 在 EnemyDeathSequence._deathPreClip 配置死亡前摇动画(两阶段死亡 Death_Pre 无敌)。"); + report.Add("★ 挂载行为树 BehaviorTree 组件,指定对应外部行为树资产。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; @@ -857,7 +867,7 @@ namespace BaseGames.Editor AnimancerComponent animancer = GetOrAddComponent(visual.gameObject); SpriteRenderer sr5 = SetupSpriteRenderer(visual.gameObject); - E005_FeiZhi enemyBase = GetOrAddComponent(go); + EnemyBase enemyBase = GetOrAddComponent(go); EnemyStats enemyStats = GetOrAddComponent(go); EnemyMovement movement = GetOrAddComponent(go); GetOrAddComponent(go); @@ -920,9 +930,16 @@ namespace BaseGames.Editor Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) AssignReference(biteHitBox, "_defaultSource", dmgSrc, report); + // 死亡前摇无敌演出 + 动画事件池生成幼蛭(零代码:替代过去的 E005_FeiZhi 专属 Die()/SpawnProjectile 重写) + EnemyDeathSequence deathSeq = GetOrAddComponent(go); + AssignObjectArray(deathSeq, "_hurtBoxesToDisable", new Object[] { hurtBox }, report); + EnemySpawnerOnEvent spawner = GetOrAddComponent(go); + AssignString(spawner, "_payloadKey", "spawn_e003", report); + AssignString(spawner, "_poolKey", "ENM_YouZhi", 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。"); + report.Add("★ 在 EnemyDeathSequence._deathPreClip 上添加 AnimationEvent 调用 SpawnProjectile(\"spawn_e003\"),由 EnemySpawnerOnEvent 从对象池生成幼蛭(ENM_YouZhi)。"); + report.Add("★ 挂载行为树 BehaviorTree 组件,指定对应外部行为树资产。"); Undo.CollapseUndoOperations(undoGroup); Selection.activeGameObject = go; diff --git a/Assets/_Game/Scripts/Enemies/Behaviors.meta b/Assets/_Game/Scripts/Enemies/Behaviors.meta new file mode 100644 index 0000000..ca0514a --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/Behaviors.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fddc5216bee45cf4188bb31675c201a7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Enemies/Behaviors/EnemyAbilityTrigger.cs b/Assets/_Game/Scripts/Enemies/Behaviors/EnemyAbilityTrigger.cs new file mode 100644 index 0000000..55b0174 --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/Behaviors/EnemyAbilityTrigger.cs @@ -0,0 +1,59 @@ +using UnityEngine; + +namespace BaseGames.Enemies.Behaviors +{ + /// + /// 出生 / 外部时机触发执行指定能力(零代码替代每敌人专属的出生触发脚本)。 + /// + /// 勾选 executeOnSpawn:对象池取出并完成 重置后自动执行能力 + /// (例如精英怪死亡时生成的小怪落地即下坠)。 + /// 公共方法 :供场景战斗触发器 / UnityEvent / 动画事件在外部时机调用 + /// (例如场景预置的天花板敌人被战斗触发器激活下坠)。 + /// + /// 能力本身由挂载的 EnemyAbilityBase 组件(按 EnemyAbilitySO.abilityId 注册)实现, + /// 本组件只负责"在某个时机调用某个能力"。 + /// + [DisallowMultipleComponent] + public class EnemyAbilityTrigger : MonoBehaviour + { + [Header("能力")] + [Tooltip("要执行的能力 Id(须与某个 EnemyAbilityBase 的 EnemyAbilitySO.abilityId 一致)")] + [SerializeField] private string _abilityId = "ability_id"; + + [Header("触发时机")] + [Tooltip("对象池生成 / OnSpawn 重置完成后自动执行(用于被生成出来即触发的敌人)")] + [SerializeField] private bool _executeOnSpawn = true; + + private EnemyBase _enemy; + + private void Awake() + { + _enemy = GetComponentInParent(); + if (_enemy == null) + Debug.LogError($"[EnemyAbilityTrigger] {name} 找不到 EnemyBase。", this); + } + + private void OnEnable() + { + if (_enemy != null) _enemy.Spawned += OnEnemySpawned; + } + + private void OnDisable() + { + if (_enemy != null) _enemy.Spawned -= OnEnemySpawned; + } + + private void OnEnemySpawned() + { + if (_executeOnSpawn) Trigger(); + } + + /// + /// 立即执行配置的能力(外部时机触发:场景触发器 / UnityEvent / 动画事件调用)。 + /// + public void Trigger() + { + _enemy?.Abilities.Get(_abilityId)?.Execute(); + } + } +} diff --git a/Assets/_Game/Scripts/Enemies/E005_FeiZhi.cs.meta b/Assets/_Game/Scripts/Enemies/Behaviors/EnemyAbilityTrigger.cs.meta similarity index 83% rename from Assets/_Game/Scripts/Enemies/E005_FeiZhi.cs.meta rename to Assets/_Game/Scripts/Enemies/Behaviors/EnemyAbilityTrigger.cs.meta index 569788d..b534b00 100644 --- a/Assets/_Game/Scripts/Enemies/E005_FeiZhi.cs.meta +++ b/Assets/_Game/Scripts/Enemies/Behaviors/EnemyAbilityTrigger.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: f2460e8735a4dc5409fe6b0949bd65c0 +guid: 3198979f0bd38e1429478e7937b280b3 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/_Game/Scripts/Enemies/Behaviors/EnemyBehaviorInterfaces.cs b/Assets/_Game/Scripts/Enemies/Behaviors/EnemyBehaviorInterfaces.cs new file mode 100644 index 0000000..bf6f7ed --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/Behaviors/EnemyBehaviorInterfaces.cs @@ -0,0 +1,29 @@ +namespace BaseGames.Enemies.Behaviors +{ + /// + /// 死亡前摇演出组件接口(零代码替代每敌人专属的 Die() 重写)。 + /// + /// 检测到子物体挂载此接口组件时,委托其播放无敌前摇演出, + /// 演出结束后必须回调 (即 ) + /// 执行真正的死亡清理。演出期间 为 true。 + /// + /// + public interface IEnemyDeathSequence + { + /// 开始播放死亡前摇演出;演出结束后必须调用 + void Play(System.Action onComplete); + } + + /// + /// 动画事件生成处理器接口(零代码替代每敌人专属的 SpawnProjectile() 重写)。 + /// + /// 会把 payload 路由到所有挂载的此接口组件, + /// 由组件自行按 payload 匹配决定是否生成(如从对象池生成小怪 / 弹幕)。 + /// + /// + public interface IEnemySpawnEventHandler + { + /// 处理一次动画事件生成请求。 为动画事件携带的配置 Id。 + void HandleSpawn(string payload); + } +} diff --git a/Assets/_Game/Scripts/Enemies/E003_YouZhi.cs.meta b/Assets/_Game/Scripts/Enemies/Behaviors/EnemyBehaviorInterfaces.cs.meta similarity index 83% rename from Assets/_Game/Scripts/Enemies/E003_YouZhi.cs.meta rename to Assets/_Game/Scripts/Enemies/Behaviors/EnemyBehaviorInterfaces.cs.meta index ad0941f..7b66bb6 100644 --- a/Assets/_Game/Scripts/Enemies/E003_YouZhi.cs.meta +++ b/Assets/_Game/Scripts/Enemies/Behaviors/EnemyBehaviorInterfaces.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: d86a36c2999f88842a212d095749c349 +guid: 6757468022586aa41aab08314ca5a24f MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/_Game/Scripts/Enemies/Behaviors/EnemyDeathSequence.cs b/Assets/_Game/Scripts/Enemies/Behaviors/EnemyDeathSequence.cs new file mode 100644 index 0000000..ccc9011 --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/Behaviors/EnemyDeathSequence.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections; +using Animancer; +using BaseGames.Combat; +using UnityEngine; + +namespace BaseGames.Enemies.Behaviors +{ + /// + /// 死亡前摇无敌演出(零代码替代每敌人专属的 Die() 重写)。 + /// + /// 会委托本组件:停行为树 / 停移动、停用受击框,播放前摇动画并等待, + /// 期间敌人处于无敌(),演出结束后回调真正的死亡清理。 + /// 前摇动画上可放置 SpawnProjectile 动画事件,配合 在演出中生成小怪。 + /// + /// + [DisallowMultipleComponent] + public class EnemyDeathSequence : MonoBehaviour, IEnemyDeathSequence + { + [Header("前摇动画")] + [Tooltip("死亡前摇动画(无敌演出);为空则跳过演出直接进入死亡清理")] + [SerializeField] private ClipTransition _deathPreClip; + [Tooltip("前摇演出时长(秒)")] + [Min(0f)][SerializeField] private float _duration = 3f; + + [Header("演出期间")] + [Tooltip("演出期间停用的受击框(防止演出中被打断或二次受伤);对象池复用时 OnSpawn 自动恢复")] + [SerializeField] private HurtBox[] _hurtBoxesToDisable; + [Tooltip("演出开始时停止行为树(防止 BT 继续 Tick 覆盖演出动画)")] + [SerializeField] private bool _stopBehaviorTree = true; + [Tooltip("演出开始时停止移动")] + [SerializeField] private bool _stopMovement = true; + + private EnemyBase _enemy; + private AnimancerComponent _animancer; + + private void Awake() + { + _enemy = GetComponentInParent(); + _animancer = _enemy != null ? _enemy.Animancer : GetComponentInParent(); + if (_enemy == null) + Debug.LogError($"[EnemyDeathSequence] {name} 找不到 EnemyBase。", this); + } + + // 对象池复用:出生时恢复受击框(演出中曾被停用) + private void OnEnable() + { + if (_enemy != null) _enemy.Spawned += RestoreHurtBoxes; + } + + private void OnDisable() + { + if (_enemy != null) _enemy.Spawned -= RestoreHurtBoxes; + } + + public void Play(Action onComplete) + { + StartCoroutine(Sequence(onComplete)); + } + + private IEnumerator Sequence(Action onComplete) + { + if (_stopBehaviorTree) _enemy?.StopBehaviorTree(); + if (_stopMovement) _enemy?.StopMovement(); + + SetHurtBoxesEnabled(false); + + if (_deathPreClip.Clip != null && _animancer != null) + { + _animancer.Play(_deathPreClip); + if (_duration > 0f) yield return new WaitForSeconds(_duration); + } + + onComplete?.Invoke(); + } + + private void RestoreHurtBoxes() => SetHurtBoxesEnabled(true); + + private void SetHurtBoxesEnabled(bool enabled) + { + if (_hurtBoxesToDisable == null) return; + foreach (var hb in _hurtBoxesToDisable) + if (hb != null) hb.enabled = enabled; + } + } +} diff --git a/Assets/_Game/Scripts/Enemies/E004_ZhiMu.cs.meta b/Assets/_Game/Scripts/Enemies/Behaviors/EnemyDeathSequence.cs.meta similarity index 83% rename from Assets/_Game/Scripts/Enemies/E004_ZhiMu.cs.meta rename to Assets/_Game/Scripts/Enemies/Behaviors/EnemyDeathSequence.cs.meta index f0db74a..3bf05e5 100644 --- a/Assets/_Game/Scripts/Enemies/E004_ZhiMu.cs.meta +++ b/Assets/_Game/Scripts/Enemies/Behaviors/EnemyDeathSequence.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: cf8f8c7225dca9c42b5a451b177319b9 +guid: 4a4a8aad8881b4543a0918321e7efe3e MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/_Game/Scripts/Enemies/Behaviors/EnemySpawnerOnEvent.cs b/Assets/_Game/Scripts/Enemies/Behaviors/EnemySpawnerOnEvent.cs new file mode 100644 index 0000000..e9e306b --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/Behaviors/EnemySpawnerOnEvent.cs @@ -0,0 +1,46 @@ +using BaseGames.Core; +using BaseGames.Core.Pool; +using UnityEngine; + +namespace BaseGames.Enemies.Behaviors +{ + /// + /// 动画事件触发的对象池生成(零代码替代每敌人专属的 SpawnProjectile() 重写)。 + /// + /// 会把 payload 路由到本组件;当 payload 与 + /// payloadKey 匹配时,在自身周围随机位置从对象池生成 count 个指定 key 的对象 + /// (例如精英怪死亡前摇中生成 N 个小怪)。 + /// + /// 使用方式:在动画(如死亡前摇)相应帧放置 SpawnProjectile 动画事件,payload 填 payloadKey。 + /// 同一敌人可挂多个本组件(不同 payloadKey)以生成不同对象。 + /// + public class EnemySpawnerOnEvent : MonoBehaviour, IEnemySpawnEventHandler + { + [Header("触发匹配")] + [Tooltip("匹配的动画事件 payload(如 \"spawn_e003\");留空则匹配任意 payload")] + [SerializeField] private string _payloadKey = ""; + + [Header("生成")] + [Tooltip("对象池 key(IObjectPoolService.Spawn 的 key,须经 AddressKeys 常量约定,如 \"ENM_YouZhi\")")] + [SerializeField] private string _poolKey = ""; + [Tooltip("生成数量")] + [Min(1)][SerializeField] private int _count = 3; + [Tooltip("生成点相对自身的随机半径")] + [Min(0f)][SerializeField] private float _radius = 1.5f; + + public void HandleSpawn(string payload) + { + if (!string.IsNullOrEmpty(_payloadKey) && payload != _payloadKey) return; + if (string.IsNullOrEmpty(_poolKey)) return; + + var pool = ServiceLocator.GetOrDefault(); + if (pool == null) return; + + for (int i = 0; i < _count; i++) + { + Vector2 offset = Random.insideUnitCircle * _radius; + pool.Spawn(_poolKey, (Vector2)transform.position + offset, Quaternion.identity); + } + } + } +} diff --git a/Assets/_Game/Scripts/Enemies/Behaviors/EnemySpawnerOnEvent.cs.meta b/Assets/_Game/Scripts/Enemies/Behaviors/EnemySpawnerOnEvent.cs.meta new file mode 100644 index 0000000..7a12d62 --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/Behaviors/EnemySpawnerOnEvent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fd9abafdb6bedcc4b8fd011bde9208b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Enemies/E003_YouZhi.cs b/Assets/_Game/Scripts/Enemies/E003_YouZhi.cs deleted file mode 100644 index 6313e0c..0000000 --- a/Assets/_Game/Scripts/Enemies/E003_YouZhi.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections; -using Animancer; -using UnityEngine; - -namespace BaseGames.Enemies -{ - /// - /// E003 幼蛭。HP=1,一击即死。 - /// 支持双路初始化: - /// - 预置路径:场景战斗触发器调用 - /// - 对象池路径:E005 死亡时通过 自动触发 - /// 能力脚本 AnimatedCeilingDropAbility 负责 Fall 动画 + 物理下落 + SetAiPhase(Patrol)。 - /// - public class E003_YouZhi : EnemyBase - { - [Tooltip("对象池生成时是否立即执行下落能力(E005 触发生成路径)")] - [SerializeField] private bool _activateOnSpawn = true; - - public override void OnSpawn() - { - base.OnSpawn(); - if (_activateOnSpawn) - Abilities.Get("e003_fall")?.Execute(); - } - - /// - /// 场景预置路径:由场景触发器(EventTrigger / Animator Event)调用,触发天花板跌落。 - /// - public void ActivateFromCeiling() - { - Abilities.Get("e003_fall")?.Execute(); - } - } -} diff --git a/Assets/_Game/Scripts/Enemies/E004_ZhiMu.cs b/Assets/_Game/Scripts/Enemies/E004_ZhiMu.cs deleted file mode 100644 index c9044e0..0000000 --- a/Assets/_Game/Scripts/Enemies/E004_ZhiMu.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections; -using Animancer; -using BaseGames.Combat; -using UnityEngine; - -namespace BaseGames.Enemies -{ - /// - /// E004 蛭母(小Boss)。 - /// 特性: - /// - 出场演出(AppearAbility) - /// - 三技能(撕咬/头槌/酸液)+ 翻身(FacePlayerAbility) - /// - 死亡两阶段:Death_Pre 无敌演出 → base.Die() - /// - public class E004_ZhiMu : EnemyBase - { - [Header("死亡演出")] - [SerializeField] private ClipTransition _deathPreClip; - [SerializeField] private HurtBox _hurtBox; - [SerializeField] private float _deathPreDuration = 3f; - - protected override void Die() - { - StartCoroutine(DeathSequence()); - } - - private IEnumerator DeathSequence() - { - // 停止行为树防止覆盖演出动画 - StopBehaviorTree(); - StopMovement(); - - if (_hurtBox != null) - _hurtBox.enabled = false; - - if (_deathPreClip.Clip != null) - { - Animancer.Play(_deathPreClip); - yield return new WaitForSeconds(_deathPreDuration); - } - - base.Die(); - } - } -} diff --git a/Assets/_Game/Scripts/Enemies/E005_FeiZhi.cs b/Assets/_Game/Scripts/Enemies/E005_FeiZhi.cs deleted file mode 100644 index c67418c..0000000 --- a/Assets/_Game/Scripts/Enemies/E005_FeiZhi.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Collections; -using Animancer; -using BaseGames.Combat; -using BaseGames.Core; -using BaseGames.Core.Pool; -using UnityEngine; - -namespace BaseGames.Enemies -{ - /// - /// E005 肥蛭(精英怪)。 - /// 特性: - /// - 撕咬(MeleeVulnerabilityAbility)含后摇脆弱窗口 - /// - 死亡两阶段:Death_Pre 无敌演出(含 AnimationEvent spawn_e003)→ base.Die() - /// - public class E005_FeiZhi : EnemyBase - { - [Header("死亡演出")] - [SerializeField] private ClipTransition _deathPreClip; - [SerializeField] private HurtBox _hurtBox; - [SerializeField] private float _deathPreDuration = 3f; - - [Header("生成 E003(Death_Pre AnimationEvent 触发)")] - [SerializeField] private int _spawnCount = 3; - [SerializeField] private float _spawnRadius = 1.5f; - - /// - /// Death_Pre 动画适当帧的 AnimationEvent 调用 SpawnProjectile("spawn_e003") 生成幼蛭。 - /// - public override void SpawnProjectile(string payload) - { - if (payload != "spawn_e003") return; - - var pool = BaseGames.Core.ServiceLocator.GetOrDefault(); - if (pool == null) return; - - for (int i = 0; i < _spawnCount; i++) - { - Vector2 offset = Random.insideUnitCircle * _spawnRadius; - pool.Spawn("ENM_YouZhi", (Vector2)transform.position + offset, Quaternion.identity); - } - } - - protected override void Die() - { - StartCoroutine(DeathSequence()); - } - - private IEnumerator DeathSequence() - { - StopBehaviorTree(); - StopMovement(); - - if (_hurtBox != null) - _hurtBox.enabled = false; - - if (_deathPreClip.Clip != null) - { - Animancer.Play(_deathPreClip); - yield return new WaitForSeconds(_deathPreDuration); - } - - base.Die(); - } - } -} diff --git a/Assets/_Game/Scripts/Enemies/EnemyBase.cs b/Assets/_Game/Scripts/Enemies/EnemyBase.cs index 11f9883..0c7baf6 100644 --- a/Assets/_Game/Scripts/Enemies/EnemyBase.cs +++ b/Assets/_Game/Scripts/Enemies/EnemyBase.cs @@ -27,6 +27,12 @@ namespace BaseGames.Enemies /// 死亡时触发(ChallengeRoomManager 波次结算用)。 public event System.Action OnDied; + /// + /// 对象池取出并完成 重置后触发。 + /// 配置型出生行为组件(如 EnemyAbilityTrigger)订阅此事件,实现零代码出生触发。 + /// + public event System.Action Spawned; + [Header("配置 SO")] [SerializeField] protected EnemyStatsSO _statsSO; [SerializeField] protected EnemyAnimationConfigSO _animConfig; @@ -83,6 +89,14 @@ namespace BaseGames.Enemies /// private PooledObject _pooledObject; + // ── 配置型行为模块(零代码)───────────────────────────────────────── + // 可选挂载的通用行为组件,替代过去的每敌人专属子类。Awake 时收集一次。 + private Behaviors.IEnemyDeathSequence _deathSequence; + private readonly System.Collections.Generic.List _spawnHandlers + = new System.Collections.Generic.List(2); + // 死亡前摇演出进行中:纳入 IsInvincible,并阻止重复触发 Die。 + private bool _deathSequenceActive; + // ── 状态 ────────────────────────────────────────────────────────── private EnemyStateType _currentState; public EnemyStateType CurrentState => _currentState; @@ -129,7 +143,7 @@ namespace BaseGames.Enemies = new System.Collections.Generic.Dictionary(); // ── IDamageable ─────────────────────────────────────────────────── public bool IsAlive => _currentState != EnemyStateType.Dead; - public virtual bool IsInvincible => _currentState == EnemyStateType.Dead; + public virtual bool IsInvincible => _currentState == EnemyStateType.Dead || _deathSequenceActive; public int Defense => _stats != null ? _stats.Defense : 0; public void TakeDamage(DamageInfo info) @@ -478,8 +492,17 @@ namespace BaseGames.Enemies // ── 动画事件钩子(由 EnemyAnimationEvents 调用)──────────────────── - /// 生成弹幕 / 技能投射物。payload 为配置 Id,由子类查表实现。 - public virtual void SpawnProjectile(string payload) { } + /// + /// 生成弹幕 / 技能投射物(由动画事件 SpawnProjectile 触发)。 + /// 基类实现:路由到所有挂载的 组件 + /// (如 EnemySpawnerOnEvent),由组件按 payload 自行匹配并生成——实现零代码生成配置。 + /// 子类(如 RangedEnemy / ChaoFengBoss)可重写以自定义发射逻辑。 + /// + public virtual void SpawnProjectile(string payload) + { + for (int i = 0; i < _spawnHandlers.Count; i++) + _spawnHandlers[i]?.HandleSpawn(payload); + } /// 切换二阶段形态(Boss 等特殊敌人重写此方法)。 public virtual void TriggerPhaseTwo() { } @@ -560,6 +583,10 @@ namespace BaseGames.Enemies _abilities.CollectFrom(gameObject); _colliders = GetComponentsInChildren(true); + // 收集配置型行为模块(零代码扩展点) + _deathSequence = GetComponentInChildren(true); + GetComponentsInChildren(true, _spawnHandlers); + Debug.Assert(_statsSO != null, "[EnemyBase] _statsSO 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this); Debug.Assert(_stats != null, "[EnemyBase] _stats 未绑定,请在 Prefab Inspector 中绑定 EnemyStats 组件。", this); Debug.Assert(_movement != null, "[EnemyBase] _movement 未找到,请确保同 GameObject 上挂有 EnemyMovement 组件。", this); @@ -667,19 +694,46 @@ namespace BaseGames.Enemies #endif /// - /// 停止行为树(子类 Die() 预演出阶段可调用,防止 BT 继续 Tick 覆盖演出逻辑)。 - /// 内部使用 #if GRAPH_DESIGNER 保护,子类无需处理条件编译。 + /// 停止行为树(死亡演出 / 出场演出等期间调用,防止 BT 继续 Tick 覆盖演出逻辑)。 + /// 内部使用 #if GRAPH_DESIGNER 保护,调用方无需处理条件编译。 + /// 供配置型行为组件(如 EnemyDeathSequence)调用,故为 public。 /// - protected void StopBehaviorTree() + public void StopBehaviorTree() { #if GRAPH_DESIGNER _behaviorTree?.StopBehavior(); #endif } + /// + /// 死亡入口。若挂载了 死亡演出组件, + /// 则先委托其播放无敌前摇(期间 为 true),演出结束后回调 + /// 执行真正的死亡清理;否则直接清理。 + /// 子类(如 BossBase)重写时仍调用 base.Die() 即可获得此委托行为。 + /// protected virtual void Die() + { + if (_currentState == EnemyStateType.Dead || _deathSequenceActive) return; + + if (_deathSequence != null) + { + _deathSequenceActive = true; // 演出期间纳入 IsInvincible,阻止重复 Die + _deathSequence.Play(PerformDeath); + return; + } + + PerformDeath(); + } + + /// + /// 实际死亡清理:切 Dead 终态、清状态效果、中断能力、关碰撞体、播死亡动画、 + /// 归还对象池 / 销毁、广播死亡事件。由 直接调用, + /// 或由死亡演出组件在前摇结束后回调。 + /// + protected void PerformDeath() { if (_currentState == EnemyStateType.Dead) return; + _deathSequenceActive = false; ForceState(EnemyStateType.Dead); // 死亡时清除所有状态效果 @@ -733,6 +787,7 @@ namespace BaseGames.Enemies // 注意:_playerTransform 不重置(场景中玩家仍存在),只重置追踪历史 LastKnownPlayerPosition = transform.position; _wasParried = false; + _deathSequenceActive = false; // 重置生命值 if (_stats != null && _statsSO != null) @@ -744,6 +799,9 @@ namespace BaseGames.Enemies #if GRAPH_DESIGNER _behaviorTree?.StartBehavior(); #endif + + // 通知配置型出生行为组件(如 EnemyAbilityTrigger)执行出生触发逻辑 + Spawned?.Invoke(); } /// diff --git a/Docs/Architecture/07_EnemyModule.md b/Docs/Architecture/07_EnemyModule.md index da10849..30db45c 100644 --- a/Docs/Architecture/07_EnemyModule.md +++ b/Docs/Architecture/07_EnemyModule.md @@ -92,9 +92,12 @@ public class EnemyBase : MonoBehaviour, IDamageable // ──── EnemyAnimationEvents 接口(虚方法,Boss 子类覆盖)────────── // 由 EnemyAnimationEvents.OnAnimationEvent 在对应动画帧触发时调用(见 24_AnimEventModule §6) - public virtual void SpawnProjectile(string data) { } + // SpawnProjectile 基类实现:路由到挂载的 IEnemySpawnEventHandler 组件(见 §8.5 配置型行为组件) + public virtual void SpawnProjectile(string data) { /* routes to IEnemySpawnEventHandler */ } public virtual void TriggerPhaseTwo() { } public virtual void OnAnimationComplete(string data) { } + // 对象池取出并完成 OnSpawn 重置后触发;配置型出生行为组件(EnemyAbilityTrigger)订阅 + public event System.Action Spawned; // ⚡ BD 每帧 Tick 调用 Blackboard 读写 SharedVariable;Awake 缓存、消除 GetComponent 热开销 public BehaviorTree Blackboard => _behaviorTree; @@ -426,6 +429,20 @@ public class EnemyAnimationConfigSO : ScriptableObject --- +## 8.5 配置型行为组件(零代码扩展点) + +过去 E003/E004/E005 等敌人各写一个继承 `EnemyBase` 的专属子类,只为挂接少量生命周期钩子。现已抽成**通用、可配置的行为组件**——策划在 Prefab 上加组件、填字段即可,不再为单个敌人写脚本。`EnemyBase` 在 `Awake` 收集这些组件并在对应时机回调。 + +| 组件 | 命名空间 | 作用 | 关键字段 | 接入的 EnemyBase 钩子 | +|------|---------|------|---------|---------------------| +| `EnemyAbilityTrigger` | `BaseGames.Enemies.Behaviors` | 出生时(对象池)或外部时机执行某个能力 | `_abilityId`、`_executeOnSpawn` | 订阅 `Spawned` 事件;`public Trigger()` 供场景触发器/UnityEvent/动画事件调用 | +| `EnemyDeathSequence` | 同上 | 死亡前摇无敌演出(停 BT/移动、关 HurtBox、播放前摇、等待后再真正死亡) | `_deathPreClip`、`_duration`、`_hurtBoxesToDisable` | 实现 `IEnemyDeathSequence`;`Die()` 委托其播放,期间 `IsInvincible=true`,结束回调 `PerformDeath()` | +| `EnemySpawnerOnEvent` | 同上 | 动画事件触发的对象池生成(如死亡演出中生成小怪) | `_payloadKey`、`_poolKey`、`_count`、`_radius` | 实现 `IEnemySpawnEventHandler`;`SpawnProjectile(payload)` 路由匹配 | + +扩展原则:新的"某时机做某事"需求优先做成此类通用组件(参考现有 `AssignReference`/`AssignAsset` 脚手架绑定模式接入 `SceneObjectPlacerTool`),而非新建 `EnemyBase` 子类——使敌人尽量通过配置而非代码创建。`RangedEnemy` / `BossBase` / `ChaoFengBoss` 等仍可重写 `SpawnProjectile`/`Die` 自定义逻辑(重写不调用 base 时即绕过路由/委托)。 + +--- + ## 9. BossBase ```csharp diff --git a/Docs/Guides/02_Enemy_Boss_Setup_Guide.md b/Docs/Guides/02_Enemy_Boss_Setup_Guide.md index f1439a0..ad25f7b 100644 --- a/Docs/Guides/02_Enemy_Boss_Setup_Guide.md +++ b/Docs/Guides/02_Enemy_Boss_Setup_Guide.md @@ -404,7 +404,7 @@ Selector ``` ENM_YouZhi (根节点) -├── 组件:E003_YouZhi(C# 脚本)、EnemyMovement、EnemyNavAgent、NavAgent、TransformBasedMovement +├── 组件:EnemyBase、EnemyAbilityTrigger、EnemyMovement、EnemyNavAgent、NavAgent、TransformBasedMovement ├── 组件:EnemySensorHub ├── 组件:Rigidbody2D(初始 Body Type=Kinematic,下落能力会自动切换为 Dynamic) ├── HurtBox/ @@ -426,8 +426,10 @@ ENM_YouZhi (根节点) | `_recoveryTime` | `0.1`(落地后短暂停顿) | | `_contactDamage` | 拖入 `ContactDamageZone/` 的 BodyContactDamage | -**E003_YouZhi 组件配置**: -- `_activateOnSpawn`:勾选 ✓(对象池生成时自动触发下落;若是场景预置则由触发器调用 `ActivateFromCeiling()` 方法) +**EnemyAbilityTrigger 组件配置**(零代码出生/触发执行能力,替代过去的 E003_YouZhi 专属脚本): +- `_abilityId`:`e003_fall`(与 `ABL_E003_Fall.asset` 的 `abilityId` 一致) +- `_executeOnSpawn`:勾选 ✓(对象池生成时自动触发下落) +- 若是场景预置(非对象池),由场景战斗触发器 / UnityEvent / 动画事件调用本组件的 `Trigger()` 方法激活下落 **EnemySensorHub**: @@ -481,7 +483,7 @@ Selector | `Skill02_End.anim` | 头槌收招(单次) | `RepeatSlamAbility._endClip` | | `Skill03.anim` | 吐酸液(单次) | `ProjectileAttackAbility.attackSequence[0/1].clip` | | `Flip.anim` | 翻身转向(单次) | `FacePlayerAbility._faceClip` | -| `Death_Pre.anim` | 死亡前摇(循环,约3秒) | `E004_ZhiMu._deathPreClip` | +| `Death_Pre.anim` | 死亡前摇(循环,约3秒) | `EnemyDeathSequence._deathPreClip` | | `Death.anim` | 死亡消散(单次) | AnimConfig.Dead | ### Step 2:ScriptableObject @@ -503,7 +505,7 @@ Selector ``` ENM_ZhiMu (根节点) -├── 组件:E004_ZhiMu(C# 脚本)、EnemyMovement、EnemyNavAgent、NavAgent、TransformBasedMovement +├── 组件:EnemyBase、EnemyDeathSequence、EnemyMovement、EnemyNavAgent、NavAgent、TransformBasedMovement ├── 组件:EnemySensorHub ├── 组件:EnemyFeedback、Rigidbody2D ├── HurtBox/ → 组件:HurtBox、Collider2D @@ -518,10 +520,11 @@ ENM_ZhiMu (根节点) └── Flip_Ability → 组件:FacePlayerAbility ``` -**E004_ZhiMu 组件配置**: +**EnemyDeathSequence 组件配置**(零代码死亡前摇无敌演出,替代过去的 E004_ZhiMu 专属 Die() 重写): - `_deathPreClip`:拖入 `Death_Pre.anim` -- `_hurtBox`:拖入 `HurtBox/` 组件 -- `_deathPreDuration`:`3`(与 Death_Pre 动画时长一致,策划调整) +- `_duration`:`3`(与 Death_Pre 动画时长一致,策划调整) +- `_hurtBoxesToDisable`:拖入 `HurtBox/` 组件(演出期间停用,对象池复用时出生自动恢复) +- `_stopBehaviorTree` / `_stopMovement`:默认勾选(演出期间停 BT 与移动) **AppearAbility**: - `_config`:`ABL_E004_Appear.asset` @@ -623,7 +626,7 @@ Selector | `Move.anim` | 移动(循环) | AnimConfig.Walk/Run | | `Skill01.anim` | 撕咬(单次) | `MeleeVulnerabilityAbility._attackClip` | | `Skill02.anim` | 吐酸液(单次,连续两轮复用) | `ProjectileAttackAbility.attackSequence[0/1].clip` | -| `Death_Pre.anim` | 死亡前摇(含 AnimationEvent) | `E005_FeiZhi._deathPreClip` | +| `Death_Pre.anim` | 死亡前摇(含 AnimationEvent) | `EnemyDeathSequence._deathPreClip` | | `Death.anim` | 死亡 | AnimConfig.Dead | > ⚠️ **Death_Pre AnimationEvent 设置(关键步骤)**: @@ -647,7 +650,7 @@ Selector ``` ENM_FeiZhi (根节点) -├── 组件:E005_FeiZhi(C# 脚本)、EnemyMovement、EnemyNavAgent、NavAgent、TransformBasedMovement +├── 组件:EnemyBase、EnemyDeathSequence、EnemySpawnerOnEvent、EnemyMovement、EnemyNavAgent、NavAgent、TransformBasedMovement ├── 组件:EnemySensorHub ├── 组件:Rigidbody2D ├── HurtBox/ → 组件:HurtBox @@ -658,12 +661,16 @@ ENM_FeiZhi (根节点) └── Acid_Ability → 组件:ProjectileAttackAbility ``` -**E005_FeiZhi 配置**: +**EnemyDeathSequence 配置**(零代码死亡前摇无敌演出): - `_deathPreClip`:`Death_Pre.anim` -- `_hurtBox`:`HurtBox/` 组件 -- `_deathPreDuration`:与动画时长一致 -- `_spawnCount`:`3`(生成 E003 数量,策划调整) -- `_spawnRadius`:`1.5`(生成散布半径) +- `_duration`:与动画时长一致 +- `_hurtBoxesToDisable`:`HurtBox/` 组件 + +**EnemySpawnerOnEvent 配置**(零代码动画事件池生成,替代过去的 E005_FeiZhi 专属 SpawnProjectile 重写): +- `_payloadKey`:`spawn_e003`(与 Death_Pre 动画事件 payload 一致) +- `_poolKey`:`ENM_YouZhi`(对象池 key) +- `_count`:`3`(生成幼蛭数量,策划调整) +- `_radius`:`1.5`(生成散布半径) **MeleeVulnerabilityAbility(撕咬)**: - `_config`:`ABL_E005_Bite.asset` @@ -1048,18 +1055,18 @@ Selector [嘲风根节点] **原因 A**:`AnimatedCeilingDropAbility._groundMask` 未设置,导致落地检测失败,能力超时后强制结束。 **修复**:将 Ground Layer 加入 `_groundMask`。 -**原因 B**:`E003_YouZhi._activateOnSpawn` 未勾选(对象池路径无法触发下落)。 -**修复**:在 Inspector 勾选 `_activateOnSpawn`。 +**原因 B**:`EnemyAbilityTrigger._executeOnSpawn` 未勾选(对象池路径无法触发下落),或 `_abilityId` 与 `ABL_E003_Fall.asset` 的 `abilityId`(`e003_fall`)不一致。 +**修复**:在 Inspector 勾选 `_executeOnSpawn` 并确认 `_abilityId` 拼写正确。 --- ### ❌ E005 死亡后没有生成 E003 **原因 A**:`Death_Pre.anim` 中没有 AnimationEvent,或函数名/参数拼写错误。 -**修复**:打开动画编辑器,确认 AnimationEvent 的函数为 `SpawnProjectile`,参数为 `"spawn_e003"`(区分大小写)。 +**修复**:打开动画编辑器,确认 AnimationEvent 的函数为 `SpawnProjectile`,参数为 `"spawn_e003"`(区分大小写),且与 `EnemySpawnerOnEvent._payloadKey` 一致。 -**原因 B**:`ENM_YouZhi` 未注册到 Addressables 或 Pool Key 拼写错误。 -**修复**:确认 Addressable Address 和对象池 Pool Key 均为 `ENM_YouZhi`(完全一致)。 +**原因 B**:`EnemySpawnerOnEvent._poolKey` 拼写错误,或 `ENM_YouZhi` 未注册到 Addressables。 +**修复**:确认 Addressable Address、对象池 Pool Key 与 `EnemySpawnerOnEvent._poolKey` 均为 `ENM_YouZhi`(完全一致)。 --- diff --git a/Docs/Guides/08_BehaviorTree_Authoring_Guide.md b/Docs/Guides/08_BehaviorTree_Authoring_Guide.md index f57270f..a024f78 100644 --- a/Docs/Guides/08_BehaviorTree_Authoring_Guide.md +++ b/Docs/Guides/08_BehaviorTree_Authoring_Guide.md @@ -99,7 +99,7 @@ Selector ├── Sequence [追击]: BD_IsSensorDetecting("aggro") → BD_ChasePlayer └── BD_Patrol ``` -> 预置于天花板的 E003 由场景战斗触发器调用 `E003_YouZhi.ActivateFromCeiling()`;由 E005 死亡生成的走 `OnSpawn()`,均自动触发 `e003_fall`。 +> 预置于天花板的 E003 由场景战斗触发器调用 `EnemyAbilityTrigger.Trigger()`;由 E005 死亡生成的(对象池路径)经 `_executeOnSpawn` 在出生时自动触发,均执行 `e003_fall` 能力。 ### E004 蛭母(出场→战斗循环:Flip/撕咬/头槌/酸液/靠近) ``` @@ -120,7 +120,7 @@ Selector ### E005 肥蛭(近撕咬+后摇脆弱 / 远酸液 / 追击;死亡生成 E003) ``` Selector -├── Sequence [死亡]: BD_IsStateMatch(Dead) → BD_StopMovement // 死亡生成 E003 由 E005_FeiZhi.SpawnProjectile(AnimationEvent) 处理,不走 BT +├── Sequence [死亡]: BD_IsStateMatch(Dead) → BD_StopMovement // 死亡生成 E003 由 EnemySpawnerOnEvent(SpawnProjectile 动画事件路由)处理,不走 BT ├── Sequence [撕咬]: BD_CanUseAbility("e005_bite", CheckRange) → BD_UseAbility("e005_bite") ├── Sequence [酸液]: BD_CanUseAbility("e005_acid") → BD_UseAbility("e005_acid") └── BD_ChasePlayer diff --git a/zeling_v2.sln b/zeling_v2.sln index 1a0b9d5..ec961d2 100644 --- a/zeling_v2.sln +++ b/zeling_v2.sln @@ -43,12 +43,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Core", "BaseGames EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp-Editor", "Assembly-CSharp-Editor.csproj", "{278D6C47-C52B-D206-DB1C-429D79FFAD5A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Enemies", "BaseGames.Enemies.csproj", "{5E00F025-ED00-233A-3B2F-BAFF76D883F0}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Assembly-CSharp", "Assembly-CSharp.csproj", "{BDE6E0A0-CE2D-39A5-53EB-DCA516DEF547}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Player.States", "BaseGames.Player.States.csproj", "{137EBC35-6D54-8E27-0DF7-C9F5F0E63705}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Enemies", "BaseGames.Enemies.csproj", "{5E00F025-ED00-233A-3B2F-BAFF76D883F0}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Player", "BaseGames.Player.csproj", "{D9C8466E-50C7-502B-E60F-ECEEEE6609E5}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BaseGames.Audio", "BaseGames.Audio.csproj", "{F9D314BA-E5AD-4A05-04AB-93799B5E95AE}" @@ -215,10 +215,6 @@ Global {278D6C47-C52B-D206-DB1C-429D79FFAD5A}.Debug|Any CPU.Build.0 = Debug|Any CPU {278D6C47-C52B-D206-DB1C-429D79FFAD5A}.Release|Any CPU.ActiveCfg = Release|Any CPU {278D6C47-C52B-D206-DB1C-429D79FFAD5A}.Release|Any CPU.Build.0 = Release|Any CPU - {5E00F025-ED00-233A-3B2F-BAFF76D883F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5E00F025-ED00-233A-3B2F-BAFF76D883F0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5E00F025-ED00-233A-3B2F-BAFF76D883F0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5E00F025-ED00-233A-3B2F-BAFF76D883F0}.Release|Any CPU.Build.0 = Release|Any CPU {BDE6E0A0-CE2D-39A5-53EB-DCA516DEF547}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BDE6E0A0-CE2D-39A5-53EB-DCA516DEF547}.Debug|Any CPU.Build.0 = Debug|Any CPU {BDE6E0A0-CE2D-39A5-53EB-DCA516DEF547}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -227,6 +223,10 @@ Global {137EBC35-6D54-8E27-0DF7-C9F5F0E63705}.Debug|Any CPU.Build.0 = Debug|Any CPU {137EBC35-6D54-8E27-0DF7-C9F5F0E63705}.Release|Any CPU.ActiveCfg = Release|Any CPU {137EBC35-6D54-8E27-0DF7-C9F5F0E63705}.Release|Any CPU.Build.0 = Release|Any CPU + {5E00F025-ED00-233A-3B2F-BAFF76D883F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E00F025-ED00-233A-3B2F-BAFF76D883F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E00F025-ED00-233A-3B2F-BAFF76D883F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E00F025-ED00-233A-3B2F-BAFF76D883F0}.Release|Any CPU.Build.0 = Release|Any CPU {D9C8466E-50C7-502B-E60F-ECEEEE6609E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D9C8466E-50C7-502B-E60F-ECEEEE6609E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {D9C8466E-50C7-502B-E60F-ECEEEE6609E5}.Release|Any CPU.ActiveCfg = Release|Any CPU