- 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.
376 lines
17 KiB
C#
376 lines
17 KiB
C#
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 }
|
||
}
|