Files
zeling_v2/Assets/_Game/Scripts/Enemies/EnemyBase.cs
Joywayer 47bdc67cdf feat: Implement DownDash ability and related systems
- Added DownDash ability with cooldown and speed configuration.
- Introduced DownDashState to handle down dashing mechanics, including gravity manipulation and animation playback.
- Updated PlayerMovement to support DownDash functionality.
- Enhanced PlayerStats to manage spring charge consumption and healing.
- Modified PlayerCombat and WeaponHitBoxInstance to support new hit confirmation events.
- Updated AbilityType to include new form types for character abilities.
- Improved Gizmos for better visualization of enemy detection and attack ranges.
- Added feedback systems for form switching in PlayerFeedback and IFeedbackPlayer.
- Refactored combat and movement states to accommodate new abilities and ensure smooth transitions.
2026-05-22 00:09:50 +08:00

376 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using Animancer;
using BaseGames.Combat;
using BaseGames.Core.Events;
using BaseGames.Enemies.States;
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime;
#endif
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人基类(架构 07_EnemyModule §1
/// 实现 IDamageable为 Behavior Designer 任务提供统一虚方法接口。
/// 包含BD 接口、受击、死亡流程。
/// ⚠️ _nav 字段类型为 IPathAgent在 BaseGames.Enemies.Navigation 中实现具体类)。
/// </summary>
public class EnemyBase : MonoBehaviour, IDamageable, ILOSRequester
{
[Header("标识")]
[SerializeField] private string _enemyId; // 任务系统 / Boss 进程追踪用,如 "Enemy_SpiderGuard"
public string EnemyId => _enemyId;
/// <summary>死亡时触发ChallengeRoomManager 波次结算用)。</summary>
public event System.Action OnDied;
[Header("配置 SO")]
[SerializeField] protected EnemyStatsSO _statsSO;
[SerializeField] protected EnemyAnimationConfigSO _animConfig;
[Header("子组件Prefab Inspector 绑定)")]
[SerializeField] protected EnemyStats _stats;
[SerializeField] protected EnemyMovement _movement;
[SerializeField] protected EnemyCombat _combat;
[SerializeField] protected AnimancerComponent _animancer;
[SerializeField] protected EnemyFeedback _feedback;
[SerializeField] protected HurtBox _hurtBox;
[Header("事件频道")]
[SerializeField] private BaseGames.Core.Events.StringEventChannelSO _onEnemyDied;
/// <summary>
/// 玩家生成事件频道(由 PlayerController.Start() 广播)。
/// 配置后替代 FindWithTag避免 N 个敌人同帧全场景标签扫描。
/// </summary>
[SerializeField] private BaseGames.Core.Events.TransformEventChannelSO _onPlayerSpawned;
// ── 导航代理IPathAgent由 EnemyNavAgent 实现)───────────────────
// 通过接口引用,避免对 Navigation 程序集的直接依赖。
// 由子类 / Inspector 注入,或者运行时 GetComponent<IPathAgent>() 获取。
protected IPathAgent _nav;
// 霸体来源(由 EnemyPoiseComponent.Awake() 自动注入TakeDamage 时读取)
private IPoiseSource _poiseSource;
private readonly CompositeDisposable _subs = new();
// ── 状态 ──────────────────────────────────────────────────────────
private EnemyStateType _currentState;
public EnemyStateType CurrentState => _currentState;
// POCO 状态对象字典:枚举保持对外 API 不变。
// 子类可在 Awake() 重写条目注入自定义状态对象。
protected readonly System.Collections.Generic.Dictionary<EnemyStateType, IEnemyState> _stateObjs
= new System.Collections.Generic.Dictionary<EnemyStateType, IEnemyState>();
// ── IDamageable ───────────────────────────────────────────────────
public bool IsAlive => _currentState != EnemyStateType.Dead;
public bool IsInvincible => _currentState == EnemyStateType.Dead;
public int Defense => _stats != null ? _stats.Defense : 0;
public void TakeDamage(DamageInfo info)
{
if (_currentState == EnemyStateType.Dead) return;
_stats?.TakeDamage(info.FinalDamage);
_feedback?.OnHit(info);
if (_stats != null && _stats.CurrentHP <= 0)
{
Die();
return;
}
// 根据霸体等级选择 Stagger硬直或 Hurt受击
// ForceBreak 标记或 BreakLevel 超过当前霸体等级时触发 Stagger否则触发 Hurt。
PoiseLevel curPoise = _poiseSource?.GetCurrentPoiseLevel() ?? PoiseLevel.None;
bool causesStagger = info.Flags.HasFlag(DamageFlags.ForceBreak)
|| (int)info.Break > (int)curPoise;
ForceState(causesStagger ? EnemyStateType.Stagger : EnemyStateType.Hurt);
}
// BD 任务访问接口(公共只读属性)────────────────────────────────
public IPathAgent Nav => _nav;
public EnemyMovement Movement => _movement;
public EnemyStats Stats => _stats;
public AnimancerComponent Animancer => _animancer;
public EnemyAnimationConfigSO AnimConfig => _animConfig;
/// <summary>由 _onPlayerSpawned 事件缓存的玩家 Transform供 BD 任务读取。</summary>
public Transform PlayerTransform => _playerTransform;
#if GRAPH_DESIGNER
public BehaviorTree BehaviorTree => _behaviorTree;
#endif
// ── BD 行为树接口(虚方法)────────────────────────────────────────
public virtual void MoveTo(Vector2 target)
=> _nav?.RequestMoveTo(target);
public virtual void MoveInDirection(float dir)
=> _movement?.MoveHorizontal(dir);
public virtual void StopMovement()
{
_nav?.StopNavigation();
_movement?.StopHorizontal();
}
public virtual void BeginAttack(AttackType type)
{
_combat?.StartAttack(type);
_stats?.ResetAttackCooldown();
}
public virtual bool CanAttack()
=> _stats != null && _stats.AttackCooldownTimer <= 0f;
public virtual bool IsPlayerInRange(float range)
=> _stats != null && _stats.SqrDistanceToPlayer <= range * range;
public virtual bool IsPlayerVisible()
=> _losResult; // BatchLOSSystem 写入;初始 false未见玩家
public virtual void FacePlayer()
{
if (_playerTransform != null)
_movement?.FaceTarget(_playerTransform.position);
}
public virtual void Knockback(DamageInfo info)
{
if (info.Flags.HasFlag(DamageFlags.NoKnockback)) return;
_movement?.ApplyKnockback(info.KnockbackDirection, info.KnockbackForce);
}
public virtual void JumpTo(Vector2 target)
=> _movement?.JumpToTarget(target);
/// <summary>
/// 调整 BehaviorTree Tick 频率(非警觉=2帧/次,警觉=每帧)。
/// 由 BD_SetAlert 调用(架构 07_EnemyModule §13.5)。
/// </summary>
public void SetAggroTickRate(bool isAggro)
{
#if GRAPH_DESIGNER
// Opsive 运行时当前版本未直接暴露 frameInterval 字段。
// 需升级 Opsive 包或通过自定义 Tick 次数属性实现此功能。
Debug.LogWarning("[EnemyBase] SetAggroTickRate 当前无效Opsive 运行时尚未暴露 frameInterval请升级包后实现。", this);
_ = isAggro;
#endif
}
// ── 动画事件钩子(由 EnemyAnimationEvents 调用)────────────────────
/// <summary>生成弹幕 / 技能投射物。payload 为配置 Id由子类查表实现。</summary>
public virtual void SpawnProjectile(string payload) { }
/// <summary>切换二阶段形态Boss 等特殊敌人重写此方法)。</summary>
public virtual void TriggerPhaseTwo() { }
/// <summary>动画播放完毕回调(用于单次动画后返回 Idle 等逻辑)。</summary>
public virtual void OnAnimationComplete(string payload) { }
/// <summary>设置嘶吼状态(影响 Blackboard / 状态机行为)。</summary>
public virtual void SetRoaring(bool isRoaring) { }
// ── 状态控制 ──────────────────────────────────────────────────────
public void ForceState(EnemyStateType newState)
{
// Exit 当前状态
if (_stateObjs.TryGetValue(_currentState, out var prev))
prev.Exit(this);
_currentState = newState;
// Enter 新状态
if (_stateObjs.TryGetValue(newState, out var next))
next.Enter(this);
}
// ── Unity 生命周期 ────────────────────────────────────────────────
protected virtual void Awake()
{
// 初始化 POCO 状态对象(子类可在调用 base.Awake() 后替换字典条目)
_stateObjs[EnemyStateType.Controlled] = new EnemyControlledState();
_stateObjs[EnemyStateType.Hurt] = new EnemyHurtState();
_stateObjs[EnemyStateType.Stagger] = new EnemyStaggerState();
_stateObjs[EnemyStateType.Dead] = new EnemyDeadState();
_nav = GetComponent<IPathAgent>() ?? new NullPathAgent();
_poiseSource = GetComponent<IPoiseSource>();
Debug.Assert(_statsSO != null, "[EnemyBase] _statsSO 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this);
Debug.Assert(_stats != null, "[EnemyBase] _stats 未绑定,请在 Prefab Inspector 中绑定 EnemyStats 组件。", this);
_stats.Initialize(_statsSO);
// 订阅玩家生成事件PlayerController.Start 广播),避免每个敌人独立 FindWithTag
// 订阅在 OnEnable 中处理
#if GRAPH_DESIGNER
_behaviorTree = GetComponent<BehaviorTree>();
if (_behaviorTree != null)
{
_behaviorTree.StartWhenEnabled = false;
_behaviorTree.PauseWhenDisabled = true;
_behaviorTree.StartBehavior();
}
#endif
}
protected virtual void Update()
{
_stats?.TickAttackCooldown(Time.deltaTime);
// 使用 sqrMagnitude 替代 Vector2.Distance避免每帧开平方计算
if (_playerTransform != null && _stats != null)
_stats.SqrDistanceToPlayer = ((Vector2)_playerTransform.position - (Vector2)transform.position).sqrMagnitude;
}
protected virtual void Start()
{
// 若事件未配置或玩家尚未广播,匹降为一次性查找
if (_playerTransform == null)
{
var playerGO = GameObject.FindWithTag("Player");
if (playerGO != null) _playerTransform = playerGO.transform;
}
// 播放 Idle 动画(若 Animancer 和配置都就绪)
if (_animancer != null && _animConfig != null && _animConfig.Idle != null)
_animancer.Play(_animConfig.Idle);
}
// ── 内部 ──────────────────────────────────────────────────────────
protected Transform _playerTransform;
private void SetPlayerTransform(Transform player) => _playerTransform = player;
protected virtual void OnEnable()
{
_onPlayerSpawned?.Subscribe(SetPlayerTransform).AddTo(_subs);
}
protected virtual void OnDisable()
{
_subs.Clear();
}
protected virtual void OnDestroy() { }
// LOS 缓存BatchLOSSystem 写入;降级时由 3 帧节流 Raycast 写入)
private bool _losResult;
// ── ILOSRequester ──────────────────────────────────────────────────
public Vector2 LOSOrigin => (Vector2)transform.position + _statsSO.EyeOffset;
public Vector2 LOSTarget => _playerTransform != null
? (Vector2)_playerTransform.position
: (Vector2)transform.position;
public LayerMask LOSBlockingMask => _statsSO.LOSBlockingMask;
public void ReceiveLOSResult(bool hasLineOfSight)
{
_losResult = hasLineOfSight;
}
// BehaviorTree 引用(#if GRAPH_DESIGNER 保护,避免空引用)
#if GRAPH_DESIGNER
private BehaviorTree _behaviorTree;
#endif
protected virtual void Die()
{
if (_currentState == EnemyStateType.Dead) return;
ForceState(EnemyStateType.Dead);
// 禁用所有碰撞体
foreach (var col in GetComponentsInChildren<Collider2D>())
col.enabled = false;
// 播放死亡动画
if (_animancer != null && _animConfig != null && _animConfig.Dead != null)
{
var state = _animancer.Play(_animConfig.Dead);
state.Events(this).OnEnd = () => Destroy(gameObject);
}
else
{
Destroy(gameObject, 1.5f);
}
_feedback?.OnDeath();
_onEnemyDied?.Raise(_enemyId);
OnDied?.Invoke();
}
private void OnDrawGizmos()
{
#if UNITY_EDITOR
if (_statsSO == null) return;
// ── 侦测范围(淡橙填充圆,始终可见)+ 攻击范围(淡红填充圆)────────
{
var c = new Vector3(transform.position.x, transform.position.y, 0f);
var prevM = UnityEditor.Handles.matrix;
UnityEditor.Handles.matrix = Matrix4x4.identity;
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.15f);
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.DetectRange);
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.55f);
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.DetectRange);
UnityEditor.Handles.color = new Color(1f, 0.15f, 0.15f, 0.15f);
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.AttackRange);
UnityEditor.Handles.color = new Color(1f, 0.15f, 0.15f, 0.55f);
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.AttackRange);
UnityEditor.Handles.matrix = prevM;
}
// ── 运行时LOS 连线 ────────────────────────────────────────
if (!Application.isPlaying || _playerTransform == null) return;
Vector3 eyeWorld = transform.position + new Vector3(_statsSO.EyeOffset.x, _statsSO.EyeOffset.y, 0f);
Vector3 playerPos = _playerTransform.position;
float sqrDist = (playerPos - transform.position).sqrMagnitude;
bool inRange = sqrDist <= _statsSO.DetectRange * _statsSO.DetectRange;
// 眼睛位置小圆点(金黄)
Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.85f);
Gizmos.DrawWireSphere(eyeWorld, 0.07f);
if (inRange || _losResult)
{
Gizmos.color = _losResult
? new Color(1f, 0.5f, 0f, 0.85f)
: new Color(0.6f, 0.6f, 0.6f, 0.25f);
Gizmos.DrawLine(eyeWorld, playerPos);
}
#endif
}
private void OnDrawGizmosSelected()
{
#if UNITY_EDITOR
if (_statsSO == null) return;
// 选中时加亮范围圆
{
var c = new Vector3(transform.position.x, transform.position.y, 0f);
var prevM = UnityEditor.Handles.matrix;
UnityEditor.Handles.matrix = Matrix4x4.identity;
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.25f);
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.DetectRange);
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.90f);
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.DetectRange);
UnityEditor.Handles.color = new Color(1f, 0.2f, 0.2f, 0.25f);
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.AttackRange);
UnityEditor.Handles.color = new Color(1f, 0.2f, 0.2f, 0.90f);
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.AttackRange);
UnityEditor.Handles.matrix = prevM;
}
#endif
}
}
// ── 枚举(架构 07 §1────────────────────────────────────────────────
public enum EnemyStateType { Controlled, Hurt, Stagger, Dead }
public enum AttackType { Melee, Ranged, Special }
}