diff --git a/Assets/_Game/Data/Enemies/E001/ENM_E001_Stats.asset b/Assets/_Game/Data/Enemies/E001/ENM_E001_Stats.asset index a4b057cf..254d8dff 100644 --- a/Assets/_Game/Data/Enemies/E001/ENM_E001_Stats.asset +++ b/Assets/_Game/Data/Enemies/E001/ENM_E001_Stats.asset @@ -16,27 +16,13 @@ MonoBehaviour: Defense: 0 WalkSpeed: 2 RunSpeed: 4 - AttackDamage: 10 - AttackRange: 1.5 AttackCooldown: 1 - DetectRange: 6 - HasAlertState: 1 + HasAlertState: 0 MaxChaseDistance: 15 LoseLinkTimeout: 2 - AlertDuration: 0.6 - InvestigateDuration: 3 HomeRadius: 0.5 - KnockbackForce: 5 - HitStunDuration: 0.3 HitTiers: - heavyHitThreshold: 0 launchThreshold: 0 launchUpForce: 0 launchHorzForce: 0 knockUpDuration: 0 - EyeOffset: {x: 0, y: 0.8} - LOSBlockingMask: - serializedVersion: 2 - m_Bits: 1 - DetectAngleDeg: 0 - AlertBroadcastRadius: 0 diff --git a/Assets/_Game/Prefabs/Enemies/E001/ENM_CaoZhi.prefab b/Assets/_Game/Prefabs/Enemies/E001/ENM_CaoZhi.prefab index 62576e59..aae0b871 100644 --- a/Assets/_Game/Prefabs/Enemies/E001/ENM_CaoZhi.prefab +++ b/Assets/_Game/Prefabs/Enemies/E001/ENM_CaoZhi.prefab @@ -60,7 +60,7 @@ Transform: m_GameObject: {fileID: 2378558940863543267} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0.4, z: 0} + m_LocalPosition: {x: 0, y: 0.5, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -140,10 +140,10 @@ SpriteRenderer: m_AutoUVMaxDistance: 0.5 m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} - m_SortingLayerID: 0 - m_SortingLayer: 0 + m_SortingLayerID: 652617267 + m_SortingLayer: -6 m_SortingOrder: 0 - m_Sprite: {fileID: 7482667652216324306, guid: 311925a002f4447b3a28927169b83ea6, type: 3} + m_Sprite: {fileID: 1805018772, guid: 97a250d50f6857443aa7d823bbe36304, type: 3} m_Color: {r: 1, g: 1, b: 1, a: 1} m_FlipX: 0 m_FlipY: 0 @@ -259,7 +259,7 @@ BoxCollider2D: m_IsTrigger: 0 m_UsedByEffector: 0 m_UsedByComposite: 0 - m_Offset: {x: 0, y: 0.4} + m_Offset: {x: -0.06503487, y: 0.33911598} m_SpriteTilingProperty: border: {x: 0, y: 0, z: 0, w: 0} pivot: {x: 0, y: 0} @@ -270,7 +270,7 @@ BoxCollider2D: adaptiveTiling: 0 m_AutoTiling: 0 serializedVersion: 2 - m_Size: {x: 0.6, y: 0.8} + m_Size: {x: 1.6211891, y: 0.67823195} m_EdgeRadius: 0 --- !u!114 &6958071664500836132 MonoBehaviour: @@ -297,10 +297,8 @@ MonoBehaviour: _onEnemyDied: {fileID: 11400000, guid: def849e2c5ec8204eae6b083b02307aa, type: 2} _onPlayerSpawned: {fileID: 11400000, guid: 7e2c7e614f6627b449a244ab44443adf, type: 2} _dbg_CurrentState: 0 - _dbg_AiPhase: 0 _dbg_HasPlayer: 0 _dbg_LastKnownPos: {x: 0, y: 0} - _autoPlayPhaseAnimation: 1 --- !u!114 &3227522604157326171 MonoBehaviour: m_ObjectHideFlags: 0 @@ -418,7 +416,7 @@ MonoBehaviour: fallSpeed: 5 climbSpeed: 5 enableAgentRotation: 1 - enabledFeatures: 2147483647 + enabledFeatures: -1 --- !u!114 &8798631432003224959 MonoBehaviour: m_ObjectHideFlags: 0 @@ -516,6 +514,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: _patrolStrategy: 0 + _waypoints: [] + _pingPong: 0 + _waypointArriveRadius: 0.4 --- !u!114 &8800423420238451973 MonoBehaviour: m_ObjectHideFlags: 0 @@ -574,6 +575,15 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: _config: {fileID: 11400000, guid: 0adeaa8a8508fbd40986dbb71cc85acd, type: 2} + _startClip: + _FadeDuration: 0.25 + _Speed: 1 + _Events: + _NormalizedTimes: [] + _Callbacks: [] + _Names: [] + _Clip: {fileID: 7400000, guid: 14b258b423ec2914bb5385226d1007b6, type: 2} + _NormalizedStartTime: NaN _loopClip: _FadeDuration: 0.25 _Speed: 1 @@ -581,7 +591,7 @@ MonoBehaviour: _NormalizedTimes: [] _Callbacks: [] _Names: [] - _Clip: {fileID: 0} + _Clip: {fileID: 7400000, guid: fcb8ddbfc79cc7246871a5f218b91da2, type: 2} _NormalizedStartTime: NaN _endClip: _FadeDuration: 0.25 @@ -590,7 +600,7 @@ MonoBehaviour: _NormalizedTimes: [] _Callbacks: [] _Names: [] - _Clip: {fileID: 0} + _Clip: {fileID: 7400000, guid: 9f0fea347ba7eb347b0f71b423832808, type: 2} _NormalizedStartTime: NaN _contactDamage: {fileID: 9135205194319985817} references: @@ -605,9 +615,9 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 3621416060650874532} - - component: {fileID: 6286538159834431098} - component: {fileID: 8468978500168207169} - component: {fileID: 9135205194319985817} + - component: {fileID: 1887787146703163998} m_Layer: 25 m_Name: ContactDamageZone m_TagString: Untagged @@ -630,41 +640,6 @@ Transform: m_Children: [] m_Father: {fileID: 1584616613766364365} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!58 &6286538159834431098 -CircleCollider2D: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 6788684755218838016} - m_Enabled: 1 - m_Density: 1 - m_Material: {fileID: 0} - m_IncludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_ExcludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_LayerOverridePriority: 0 - m_ForceSendLayers: - serializedVersion: 2 - m_Bits: 4294967295 - m_ForceReceiveLayers: - serializedVersion: 2 - m_Bits: 4294967295 - m_ContactCaptureLayers: - serializedVersion: 2 - m_Bits: 4294967295 - m_CallbackLayers: - serializedVersion: 2 - m_Bits: 4294967295 - m_IsTrigger: 1 - m_UsedByEffector: 0 - m_UsedByComposite: 0 - m_Offset: {x: 0, y: 0.4} - serializedVersion: 2 - m_Radius: 0.4 --- !u!114 &8468978500168207169 MonoBehaviour: m_ObjectHideFlags: 0 @@ -701,6 +676,51 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: _repeatInterval: 0.5 +--- !u!61 &1887787146703163998 +BoxCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6788684755218838016} + m_Enabled: 1 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 0 + m_UsedByEffector: 0 + m_UsedByComposite: 0 + m_Offset: {x: -0.06503487, y: 0.33911598} + m_SpriteTilingProperty: + border: {x: 0, y: 0, z: 0, w: 0} + pivot: {x: 0, y: 0} + oldSize: {x: 0, y: 0} + newSize: {x: 0, y: 0} + adaptiveTilingThreshold: 0 + drawMode: 0 + adaptiveTiling: 0 + m_AutoTiling: 0 + serializedVersion: 2 + m_Size: {x: 1.6211891, y: 0.67823195} + m_EdgeRadius: 0 --- !u!1 &8344198090396325649 GameObject: m_ObjectHideFlags: 0 @@ -710,8 +730,8 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 8414909081001592724} - - component: {fileID: 4942549498281190624} - component: {fileID: 7843048500329562790} + - component: {fileID: 7955135162290383335} m_Layer: 27 m_Name: HurtBox m_TagString: Untagged @@ -734,8 +754,22 @@ Transform: m_Children: [] m_Father: {fileID: 1584616613766364365} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!70 &4942549498281190624 -CapsuleCollider2D: +--- !u!114 &7843048500329562790 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8344198090396325649} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d7b7a233d7f70aa4f86b473412b826de, type: 3} + m_Name: + m_EditorClassIdentifier: + _onDamageDealt: {fileID: 11400000, guid: eebd58cfef3527940949a8a7655a343b, type: 2} + _onHitConfirmed: {fileID: 11400000, guid: a67d56f5124e0db4f98f326c74be8091, type: 2} +--- !u!61 &7955135162290383335 +BoxCollider2D: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} @@ -763,23 +797,19 @@ CapsuleCollider2D: m_CallbackLayers: serializedVersion: 2 m_Bits: 4294967295 - m_IsTrigger: 1 + m_IsTrigger: 0 m_UsedByEffector: 0 m_UsedByComposite: 0 - m_Offset: {x: 0, y: 0.375} - m_Size: {x: 0.55, y: 0.75} - m_Direction: 0 ---- !u!114 &7843048500329562790 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 8344198090396325649} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: d7b7a233d7f70aa4f86b473412b826de, type: 3} - m_Name: - m_EditorClassIdentifier: - _onDamageDealt: {fileID: 11400000, guid: eebd58cfef3527940949a8a7655a343b, type: 2} - _onHitConfirmed: {fileID: 11400000, guid: a67d56f5124e0db4f98f326c74be8091, type: 2} + m_Offset: {x: -0.06503487, y: 0.33911598} + m_SpriteTilingProperty: + border: {x: 0, y: 0, z: 0, w: 0} + pivot: {x: 0, y: 0} + oldSize: {x: 0, y: 0} + newSize: {x: 0, y: 0} + adaptiveTilingThreshold: 0 + drawMode: 0 + adaptiveTiling: 0 + m_AutoTiling: 0 + serializedVersion: 2 + m_Size: {x: 1.6211891, y: 0.67823195} + m_EdgeRadius: 0 diff --git a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs index a74253f0..6bced0ff 100644 --- a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs +++ b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs @@ -519,6 +519,10 @@ namespace BaseGames.Editor AssignAsset(chaseAbility, "_config", report, false, "ABL_E001_Chase"); AssignReference(chaseAbility, "_contactDamage", bodyContact, report); + // 追击动画:起手/循环/收招(设计文档 Skill_Start/Loop/End) + AssignClipTransition(chaseAbility, "_startClip", "Skill_Start", "Enemies/E001", report); + AssignClipTransition(chaseAbility, "_loopClip", "Skill_Loop", "Enemies/E001", report); + AssignClipTransition(chaseAbility, "_endClip", "Skill_End", "Enemies/E001", report); Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody"); if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report); @@ -2292,6 +2296,29 @@ namespace BaseGames.Editor so.ApplyModifiedPropertiesWithoutUndo(); } + /// + /// 给组件上的 Animancer ClipTransition 字段赋 AnimationClip(写嵌套 _Clip)。 + /// 按 clip 名 + 路径子串定位(同名 clip 各敌人各有一份,须用路径区分,如 "Enemies/E001")。 + /// + private static void AssignClipTransition(Object target, string propName, string clipName, string pathContains, List report) + { + AnimationClip clip = null; + foreach (var g in AssetDatabase.FindAssets(clipName + " t:AnimationClip")) + { + var p = AssetDatabase.GUIDToAssetPath(g); + if (System.IO.Path.GetFileNameWithoutExtension(p) == clipName && p.Contains(pathContains)) + { clip = AssetDatabase.LoadAssetAtPath(p); break; } + } + if (clip == null) { report?.Add($"★ 缺少动画 clip:{clipName}(路径含 {pathContains})"); return; } + + var so = new SerializedObject(target); + var sp = so.FindProperty(propName); + var cp = sp?.FindPropertyRelative("_Clip"); + if (cp == null) { report?.Add($"{target.GetType().Name}.{propName}._Clip 字段不存在,跳过 clip 赋值。"); return; } + cp.objectReferenceValue = clip; + so.ApplyModifiedPropertiesWithoutUndo(); + } + private static void AssignAsset(Object target, string propName, List report, bool required, params string[] candidates) { Object asset = FindFirstAsset(candidates); diff --git a/Assets/_Game/Scripts/Enemies/Abilities/ContactChaseAbility.cs b/Assets/_Game/Scripts/Enemies/Abilities/ContactChaseAbility.cs index 778f2e10..edba583e 100644 --- a/Assets/_Game/Scripts/Enemies/Abilities/ContactChaseAbility.cs +++ b/Assets/_Game/Scripts/Enemies/Abilities/ContactChaseAbility.cs @@ -5,16 +5,17 @@ using UnityEngine; namespace BaseGames.Enemies.Abilities { /// - /// 循环追击 + 体接触伤害能力(纯执行器)。 - /// 每帧 MoveTo 追向玩家、开接触伤害;**何时停止由 AI 决策层负责**——AI 在"脱离视野"时 - /// 中断本能力(AI decides when to stop, ability implements the chase)。 - /// 可复用于任何需要"追击+接触伤害循环"的敌人。 + /// 追击 + 体接触伤害能力:起手(Skill_Start 张口) → 循环追击(Skill_Loop 快速爬行 + 接触伤害) + /// → 收招(Skill_End)。追击动画由本能力全权驱动(Approach 模式不驱动步态,避免抢动画)。 + /// 何时停止由 AI 决策层负责:AI 在"脱离感知"时中断本能力(ExternalRequest),本能力播 Skill_End 收招。 + /// 依据 Docs/Game/Design/小怪设计-程序开发文档-01.md(E001 状态机 Skill_Start/Loop/End)。 /// public class ContactChaseAbility : EnemyAbilityBase { - [Header("动画")] - [SerializeField] private ClipTransition _loopClip; - [SerializeField] private ClipTransition _endClip; + [Header("动画(起手 / 循环 / 收招)")] + [SerializeField] private ClipTransition _startClip; // Skill_Start:张口(一次性) + [SerializeField] private ClipTransition _loopClip; // Skill_Loop:快速爬行追击(循环) + [SerializeField] private ClipTransition _endClip; // Skill_End:收招(一次性) [Header("接触伤害")] [SerializeField] private BodyContactDamage _contactDamage; @@ -24,37 +25,63 @@ namespace BaseGames.Enemies.Abilities Phase = AbilityRunState.Active; _enemy.SetEngaged(true); - // 追击行为的速度/阶段声明归本能力(AI 只触发能力,不做这些执行) + // 起手:张口(一次性)。期间静止、不伤人;锁住动画防被步态覆盖。 + if (_startClip.Clip != null) + { + _enemy.LockAnim(_startClip.Clip.length); + _animancer.Play(_startClip); + yield return EnemyAbilityWaits.Get(_startClip.Clip.length); + } + + // 循环追击:切追击速度、开接触伤害、朝玩家移动,播 Skill_Loop(Approach 模式不抢步态)。 if (_enemy.Nav != null && _enemy.Stats != null) _enemy.Nav.SetSpeed(_enemy.Stats.RunSpeed); - if (_loopClip.Clip != null) _animancer.Play(_loopClip); - if (_contactDamage != null) _contactDamage.enabled = true; - // 追击直到玩家消失或被 AI 中断(退出追击的决策归 AI,见 EnemyBrainContext.InterruptAbilities)。 - // 小怪走 nav 寻路(EnemyBase.MoveTo → 导航),寻路正确性依赖脚手架把碰撞体底部对齐 y=0。 + // 追击直到玩家消失或被 AI 中断(退出追击的决策归 AI)。小怪走 nav 寻路。 _enemy.Locomotion?.Approach(_enemy.PlayerTransform); while (_enemy.PlayerTransform != null) yield return null; - CleanupChase(); - - if (_endClip.Clip != null) - { - _animancer.Play(_endClip); - yield return EnemyAbilityWaits.Get(_endClip.Clip.length); - } + // 玩家对象消失(正常收尾):收招 Skill_End。 + DisableContact(); + yield return PlaySkillEnd(); + Cleanup(); } - protected override void OnInterrupted(InterruptReason reason) => CleanupChase(); + protected override void OnInterrupted(InterruptReason reason) + { + DisableContact(); + // 丢失目标 = AI 主动退出追击态(ExternalRequest) → 播收招 Skill_End; + // 受击/死亡/硬直等硬打断不播收招。协程已停,fire-and-forget;动画锁保证 Skill_End + // 在其时长内不被巡逻步态盖掉。 + if (reason == InterruptReason.ExternalRequest && _endClip.Clip != null) + { + _enemy.LockAnim(_endClip.Clip.length); + _animancer.Play(_endClip); + } + Cleanup(); + } - private void CleanupChase() + private IEnumerator PlaySkillEnd() + { + if (_endClip.Clip == null) yield break; + _enemy.LockAnim(_endClip.Clip.length); + _animancer.Play(_endClip); + yield return EnemyAbilityWaits.Get(_endClip.Clip.length); + } + + private void DisableContact() { if (_contactDamage != null) _contactDamage.enabled = false; + } + + private void Cleanup() + { _enemy.SetEngaged(false); _enemy.Locomotion?.Stop(); } diff --git a/Assets/_Game/Scripts/Enemies/EnemyBase.cs b/Assets/_Game/Scripts/Enemies/EnemyBase.cs index 8bfae856..85f5b86f 100644 --- a/Assets/_Game/Scripts/Enemies/EnemyBase.cs +++ b/Assets/_Game/Scripts/Enemies/EnemyBase.cs @@ -416,16 +416,27 @@ namespace BaseGames.Enemies ScheduleStateRecovery(EnemyStateType.Stagger, staggerDuration); } - /// 按当前移动模式播放步态动画(由 EnemyLocomotion 在模式变化时调用)。 + // 动画锁:一次性招式动画(如追击 Skill_Start/Skill_End)播放期间,禁止步态动画覆盖它。 + private float _animLockUntil; + /// 是否处于一次性招式动画锁定中(步态不应覆盖)。 + public bool IsAnimLocked => Time.time < _animLockUntil; + /// 锁定动画 秒,期间步态动画不覆盖(供能力播放起手/收招等一次性动作)。 + public void LockAnim(float duration) => _animLockUntil = Time.time + Mathf.Max(0f, duration); + + /// + /// 按当前移动模式播放步态动画(由 EnemyLocomotion 在模式变化时调用)。 + /// Approach(追击)不在此驱动——追击动画由能力(Skill_Start/Loop/End)全权驱动,避免抢动画。 + /// 招式动画锁定期间()不覆盖。 + /// public void PlayLocomotionClip(LocomotionMode mode) { - if (_animancer == null || _animConfig == null) return; + if (_animancer == null || _animConfig == null || IsAnimLocked) return; var clip = mode switch { LocomotionMode.Idle => _animConfig.Idle, LocomotionMode.Patrol => _animConfig.Walk, LocomotionMode.Face => _animConfig.Alert, - LocomotionMode.Approach => _animConfig.Run, + LocomotionMode.Approach => null, // 追击动画归能力驱动 _ => null, }; if (clip != null) _animancer.Play(clip); diff --git a/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs b/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs index 5a464aa7..b1746cf4 100644 --- a/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs +++ b/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs @@ -86,8 +86,8 @@ namespace BaseGames.Enemies if (!_gaitInit || _gaitMode != _mode) { _enemy.PlayLocomotionClip(_mode); - _gaitMode = _mode; - _gaitInit = true; + // 招式动画锁定期内不算"已应用",解锁后重新同步步态(否则 Skill_End 后卡在收招动画) + if (!_enemy.IsAnimLocked) { _gaitMode = _mode; _gaitInit = true; } } switch (_mode) {