- Implemented WeaponFeedback class for handling weapon-related feedbacks such as hit effects and attack sounds. - Added meta file for AddressableManagerWindow to manage addressable assets. - Included a new jump.data file for profiler data.
426 lines
20 KiB
C#
426 lines
20 KiB
C#
using UnityEngine;
|
||
using System.Linq;
|
||
using Animancer;
|
||
using BaseGames.Core.Events;
|
||
using BaseGames.Input;
|
||
using BaseGames.Combat;
|
||
using BaseGames.Feedback;
|
||
using BaseGames.Parry;
|
||
using BaseGames.Skills;
|
||
|
||
namespace BaseGames.Player.States
|
||
{
|
||
/// <summary>
|
||
/// 玩家主控制器(协调器)。位于 Player/States/ 程序集,以便引用所有具体状态类型。
|
||
/// 实现 IDamageable + IPoiseSource(架构 06_CombatModule §13)。
|
||
/// 依赖注入:同节点组件由 RequireComponent + Awake 自动获取;跨节点引用通过 [SerializeField] 绑定。
|
||
/// </summary>
|
||
[DefaultExecutionOrder(-100)]
|
||
[RequireComponent(typeof(InputBuffer))]
|
||
[RequireComponent(typeof(PlayerMovement))]
|
||
[RequireComponent(typeof(PlayerStats))]
|
||
[RequireComponent(typeof(AnimancerComponent))]
|
||
public class PlayerController : MonoBehaviour, IDamageable, IPoiseSource
|
||
{
|
||
// ── 同节点组件(由 RequireComponent 保证存在,Awake 中自动获取)─────
|
||
private PlayerMovement _movement;
|
||
private PlayerStats _stats;
|
||
private AnimancerComponent _animancer;
|
||
|
||
// ── 配置 SO ───────────────────────────────────────────────────────────
|
||
[Header("配置")]
|
||
[SerializeField] private PlayerMovementConfigSO _movementConfig;
|
||
[SerializeField] private PlayerAnimationConfigSO _animConfig;
|
||
[SerializeField] private InputReaderSO _inputReader;
|
||
[SerializeField] private FormConfigSO _formConfig;
|
||
|
||
// ── 战斗组件 ──────────────────────────────────────────────────────────
|
||
[Header("战斗")]
|
||
[SerializeField] private PlayerFeedback _feedback;
|
||
[SerializeField] private PlayerCombat _combat;
|
||
[SerializeField] private FormController _formController;
|
||
[SerializeField] private WeaponManager _weaponManager;
|
||
[SerializeField] private SkillManager _skillManager;
|
||
[SerializeField] private SpringSystem _springSystem;
|
||
[SerializeField] private ParrySystem _parrySystem;
|
||
[SerializeField] private HurtBox _hurtBox;
|
||
[SerializeField] private ShieldComponent _shield;
|
||
[SerializeField] private PlayerWallDetector _wallDetector;
|
||
|
||
// ── 事件频道 ──────────────────────────────────────────────────────────
|
||
[Header("事件频道")]
|
||
[SerializeField] private VoidEventChannelSO _onPlayerDied;
|
||
/// <summary>
|
||
/// Start() 时广播玩家 Transform(EnemyBase / ProjectileManager 等订阅此频道)。
|
||
/// 替代每个敌人在 Awake 中独立 FindWithTag 的 O(n) 全场景扫描。
|
||
/// </summary>
|
||
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
|
||
// ── 运行时 ────────────────────────────────────────────────────────────
|
||
private InputBuffer _inputBuffer;
|
||
private bool _missingDependencyLogged;
|
||
private bool _dependenciesReady;
|
||
// DashState 在 Update 每帧访问(TickCooldown + CanDash),提前缓存避免重复 Dictionary 查找
|
||
private DashState _dashState;
|
||
/// <summary>
|
||
/// 当前腾空可用的额外跳跃次数(二段跳)。
|
||
/// 由 IdleState/RunState.OnStateEnter 落地时通过 ResetAirJumps() 重置;
|
||
/// JumpState/FallState 判断 HasAbility(DoubleJump) 后消耗。
|
||
/// </summary>
|
||
private int _airJumpsLeft;
|
||
#if UNITY_EDITOR
|
||
[Header("调试")]
|
||
[SerializeField] private bool _debugValidateTransitions = true;
|
||
|
||
[Header("── 运行时状态 ──")]
|
||
[SerializeField] private string _dbg_CurrentState;
|
||
[SerializeField] private bool _dbg_IsGrounded;
|
||
[SerializeField] private int _dbg_AirJumpsLeft;
|
||
[SerializeField] private bool _dbg_CanDash;
|
||
[SerializeField] private bool _dbg_IsInvincible;
|
||
#endif
|
||
|
||
// Overlay Layer(Layer 1):供 SpringState / SoulSkill 等叠加层动画使用
|
||
// Base Layer(Layer 0):移动/攻击/受伤/死亡等全身状态动画
|
||
private AnimancerLayer _overlayLayer;
|
||
|
||
// ── 状态实例 ──────────────────────────────────────────────────────────
|
||
private PlayerStateBase _currentState;
|
||
private readonly System.Collections.Generic.Dictionary<System.Type, PlayerStateBase> _states = new();
|
||
|
||
// ── IDamageable 实现 ──────────────────────────────────────────────────
|
||
public bool IsAlive => _stats != null && _stats.IsAlive;
|
||
public bool IsInvincible => _stats != null && _stats.IsInvincible;
|
||
public int Defense => 0;
|
||
|
||
public void TakeDamage(DamageInfo info)
|
||
{
|
||
if (_stats == null) return;
|
||
_stats.TakeDamage(info.FinalDamage);
|
||
|
||
// 当前状态标记为无敌(如旧版冲刺状态),或 Stats 层无敌窗口仍激活
|
||
// (冲刺无敌帧 DashInvincibilityDuration 内:跳过受击硬直;窗口过期后可被打断)
|
||
if (_currentState?.IsInvincible == true || (_stats != null && _stats.IsInvincible)) return;
|
||
|
||
if (_stats.IsAlive)
|
||
{
|
||
GetState<HurtState>()?.Initialize(info);
|
||
TransitionTo(GetState<HurtState>());
|
||
}
|
||
else
|
||
{
|
||
TransitionTo(GetState<DeadState>());
|
||
_onPlayerDied?.Raise();
|
||
}
|
||
}
|
||
|
||
// ── IPoiseSource 实现(架构 06_CombatModule §13)─────────────────────
|
||
/// <summary>
|
||
/// 玩家不拥有霸体,始终返回 <see cref="PoiseLevel.None"/>。
|
||
/// 设计决策:玩家不拥有霸体,始终返回 <see cref="PoiseLevel.None"/>。
|
||
/// 玩家依靠走位和弹反规避伤害而非硬吃,以保持战斗的负担感和张力。
|
||
/// 若未来需要临时霸体(如特定技能动作),请通过独立的覆盖标记实现,
|
||
/// 而非在此处引入状态,以保持接口语义清晰。
|
||
/// </summary>
|
||
public PoiseLevel GetCurrentPoiseLevel() => PoiseLevel.None;
|
||
|
||
// ── 公开属性(供状态类访问)──────────────────────────────────────────
|
||
public PlayerMovement Movement => _movement;
|
||
public PlayerStats Stats => _stats;
|
||
public AnimancerComponent Animancer => _animancer;
|
||
public PlayerMovementConfigSO MovConfig => _movementConfig;
|
||
public PlayerAnimationConfigSO AnimConfig => _animConfig;
|
||
public InputReaderSO Input => _inputReader;
|
||
public InputBuffer Buffer => _inputBuffer;
|
||
|
||
public IFeedbackPlayer Feedback => _feedback != null ? (IFeedbackPlayer)_feedback : NullFeedbackPlayer.Instance;
|
||
public PlayerCombat Combat => _combat;
|
||
public FormController Form => _formController;
|
||
public WeaponManager Weapon => _weaponManager;
|
||
public SkillManager Skill => _skillManager;
|
||
public SpringSystem Spring => _springSystem;
|
||
public ParrySystem Parry => _parrySystem;
|
||
public HurtBox HurtBox => _hurtBox;
|
||
public ShieldComponent Shield => _shield;
|
||
public PlayerWallDetector WallDetector => _wallDetector;
|
||
|
||
public bool IsGrounded => _movement != null && _movement.IsGrounded;
|
||
public int FacingDirection => _movement != null ? _movement.FacingDirection : 1;
|
||
|
||
// ── 空中跳跃 API(二段跳)────────────────────────────────────────────
|
||
public int AirJumpsLeft => _airJumpsLeft;
|
||
public void UseAirJump() => _airJumpsLeft = Mathf.Max(0, _airJumpsLeft - 1);
|
||
/// <summary>
|
||
/// 落地时重置空中跳跃次数(由 IdleState/RunState.OnStateEnter 调用)。
|
||
/// 若解锁 DoubleJump 则重置为 MovConfig.MaxAirJumps,否则为 0。
|
||
/// </summary>
|
||
public void ResetAirJumps() =>
|
||
_airJumpsLeft = (_stats != null && _stats.HasAbility(AbilityType.DoubleJump))
|
||
? (_movementConfig != null ? _movementConfig.MaxAirJumps : 1)
|
||
: 0;
|
||
|
||
// ── 抓墙高度记忆 API ──────────────────────────────────────────────────
|
||
// wallGrabY:首次抓住某面墙壁时记录的 Y 坐标;用于防止单面墙无限向上爬。
|
||
// wallGrabDir:当前记录所属墙壁方向(+1=右墙,-1=左墙,0=无)。
|
||
// isPostWallJump:蹬墙跳后未落地标记,允许靠近墙壁时自动抓墙。
|
||
private float _wallGrabY = float.NegativeInfinity;
|
||
private int _wallGrabDir = 0;
|
||
private bool _isPostWallJump;
|
||
|
||
public float WallGrabY => _wallGrabY;
|
||
public int WallGrabDir => _wallGrabDir;
|
||
public bool IsPostWallJump => _isPostWallJump;
|
||
|
||
/// <summary>
|
||
/// 进入 WallSlideState 时调用。
|
||
/// 不同墙壁(dir != _wallGrabDir)→ 重置并记录新高度;
|
||
/// 同一面墙壁 → 保留原记录(防止攀爬重置)。
|
||
/// </summary>
|
||
public void RecordWallGrab(int dir, float y)
|
||
{
|
||
if (dir != _wallGrabDir)
|
||
{
|
||
_wallGrabDir = dir;
|
||
_wallGrabY = y;
|
||
}
|
||
// 同一面墙:保留 _wallGrabY,不更新
|
||
}
|
||
|
||
/// <summary>落地时重置抓墙记录(由 IdleState/RunState.OnStateEnter 调用)。</summary>
|
||
public void ResetWallGrab()
|
||
{
|
||
_wallGrabY = float.NegativeInfinity;
|
||
_wallGrabDir = 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置蹬墙跳后自动抓墙标记。
|
||
/// true:由 WallJumpState.OnStateEnter 设置;
|
||
/// false:由 IdleState/RunState.OnStateEnter(落地)或 WallSlideState.OnStateEnter(已消耗)清除。
|
||
/// </summary>
|
||
public void SetPostWallJump(bool value) => _isPostWallJump = value;
|
||
|
||
// ── Overlay Layer API(供 SpringState / SoulSkill 等叠加动画使用)─────
|
||
/// <summary>
|
||
/// 在 Overlay Layer(Layer 1)播放动画,叠加于当前 Base Layer 动画之上。
|
||
/// 适用于灵泉使用、魂技能等需要与移动动画并行的上半身动作。
|
||
/// </summary>
|
||
public AnimancerState PlayOnOverlay(AnimationClip clip)
|
||
=> _overlayLayer?.Play(clip);
|
||
|
||
/// <summary>停止 Overlay Layer 动画,淡出回 Base Layer。</summary>
|
||
public void StopOverlay(float fadeDuration = 0.1f)
|
||
=> _overlayLayer?.StartFade(0f, fadeDuration);
|
||
|
||
// ── Unity Lifecycle ───────────────────────────────────────────────────
|
||
private void Awake()
|
||
{
|
||
Debug.Assert(_movementConfig != null, "[PlayerController] _movementConfig 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
||
ResolveDependencies();
|
||
|
||
// 初始化 Animancer 双层动画:Layer 0 = Base(全身状态),Layer 1 = Overlay(叠加层)
|
||
if (_animancer != null)
|
||
{
|
||
// 确保 Layer 1 存在;AnimancerComponent 默认只有 Layer 0
|
||
while (_animancer.Layers.Count <= 1)
|
||
_animancer.Layers.Add();
|
||
_overlayLayer = _animancer.Layers[1];
|
||
}
|
||
|
||
// 注入 HurtBox 依赖
|
||
if (_hurtBox != null)
|
||
{
|
||
if (_shield != null) _hurtBox.SetShieldable(_shield);
|
||
if (_parrySystem != null) _hurtBox.SetParrySystem(_parrySystem);
|
||
_hurtBox.SetPoiseSource(this);
|
||
}
|
||
|
||
// 将唯一配置点(_inputReader)注入到 ParrySystem。
|
||
// ParrySystem 不再需要在 Inspector 单独配置 InputReaderSO。
|
||
if (_parrySystem != null)
|
||
_parrySystem.SetInputReader(_inputReader);
|
||
|
||
InitializeStates();
|
||
|
||
// 订阅 ParrySystem C# 事件
|
||
if (_parrySystem != null)
|
||
{
|
||
_parrySystem.OnParryActivated += OnParryActivated;
|
||
_parrySystem.OnParryConsumed += OnParryConsumedHandler;
|
||
}
|
||
|
||
// 订阅灵泉使用输入
|
||
if (_inputReader != null)
|
||
_inputReader.UseSpringEvent += OnUseSpring;
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (_parrySystem != null)
|
||
{
|
||
_parrySystem.OnParryActivated -= OnParryActivated;
|
||
_parrySystem.OnParryConsumed -= OnParryConsumedHandler;
|
||
}
|
||
|
||
if (_inputReader != null)
|
||
_inputReader.UseSpringEvent -= OnUseSpring;
|
||
}
|
||
|
||
/// <summary>弹反输入激活时由 ParrySystem 触发 → 转换到 ParryState。</summary>
|
||
private void OnParryActivated()
|
||
{
|
||
if (_states.ContainsKey(typeof(ParryState)))
|
||
TransitionTo(GetState<ParryState>());
|
||
}
|
||
|
||
/// <summary>弹反命中成功时由 ParrySystem 触发 → 发放灵力并恢复护盾。</summary>
|
||
private void OnParryConsumedHandler(BaseGames.Parry.ParryInfo info)
|
||
{
|
||
_stats?.AddSoul(info.SoulGained);
|
||
_shield?.OnParrySuccess();
|
||
Feedback.PlayParrySuccess();
|
||
}
|
||
|
||
/// <summary>灵泉输入:地面且有剩余充能时转入 SpringState 使用一次。</summary>
|
||
private void OnUseSpring()
|
||
{
|
||
if (_stats == null || _stats.CurrentSpringCharges <= 0) return;
|
||
if (_movement != null && !_movement.IsGrounded) return;
|
||
if (_states.ContainsKey(typeof(SpringState)))
|
||
TransitionTo(GetState<SpringState>());
|
||
}
|
||
|
||
private void Start()
|
||
{
|
||
if (!HasRequiredStateDependencies())
|
||
return;
|
||
|
||
// 广播玩家 Transform:EnemyBase / ProjectileManager 等订阅者将通过事件接收引用
|
||
// (必须在 Start 中调用,确保所有 Awake/OnEnable 订阅已就绪)
|
||
_onPlayerSpawned?.Raise(transform);
|
||
|
||
TransitionTo(GetState<IdleState>());
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
if (!HasRequiredStateDependencies())
|
||
return;
|
||
|
||
// 冲刺冷却计时
|
||
_dashState?.TickCooldown(Time.deltaTime);
|
||
|
||
_currentState?.OnStateUpdate();
|
||
|
||
#if UNITY_EDITOR
|
||
_dbg_CurrentState = _currentState?.GetType().Name ?? "None";
|
||
_dbg_IsGrounded = _movement != null && _movement.IsGrounded;
|
||
_dbg_AirJumpsLeft = _airJumpsLeft;
|
||
_dbg_CanDash = _dashState?.CanDash ?? false;
|
||
_dbg_IsInvincible = _stats != null && _stats.IsInvincible;
|
||
#endif
|
||
}
|
||
|
||
private void FixedUpdate()
|
||
{
|
||
_currentState?.OnStateFixedUpdate();
|
||
}
|
||
|
||
private void LateUpdate()
|
||
{
|
||
_movement?.UpdateFacing();
|
||
}
|
||
|
||
// ── 状态机 ────────────────────────────────────────────────────────────
|
||
public void TransitionTo(PlayerStateBase newState)
|
||
{
|
||
#if UNITY_EDITOR
|
||
if (_debugValidateTransitions && _currentState != null && newState != null)
|
||
{
|
||
var allowed = _currentState.ValidTransitions;
|
||
if (allowed.Count > 0 && !allowed.Contains(newState.GetType()))
|
||
Debug.LogWarning(
|
||
$"[PlayerController] 非预期转换: {_currentState.GetType().Name} → {newState.GetType().Name}",
|
||
this);
|
||
}
|
||
#endif
|
||
_currentState?.OnStateExit();
|
||
_currentState = newState;
|
||
_currentState?.OnStateEnter();
|
||
}
|
||
|
||
private void InitializeStates()
|
||
{
|
||
_states[typeof(IdleState)] = new IdleState(this);
|
||
_states[typeof(RunState)] = new RunState(this);
|
||
_states[typeof(JumpState)] = new JumpState(this);
|
||
_states[typeof(FallState)] = new FallState(this);
|
||
_states[typeof(AttackState)] = new AttackState(this);
|
||
_states[typeof(DashState)] = new DashState(this);
|
||
_states[typeof(DownDashState)] = new DownDashState(this);
|
||
_states[typeof(WallSlideState)] = new WallSlideState(this);
|
||
_states[typeof(WallJumpState)] = new WallJumpState(this);
|
||
_states[typeof(AirAttackState)] = new AirAttackState(this);
|
||
_states[typeof(DownAttackState)] = new DownAttackState(this);
|
||
_states[typeof(UpAttackState)] = new UpAttackState(this);
|
||
_states[typeof(HurtState)] = new HurtState(this);
|
||
_states[typeof(DeadState)] = new DeadState(this);
|
||
_states[typeof(SpringState)] = new SpringState(this);
|
||
_states[typeof(ParryState)] = new ParryState(this);
|
||
_states[typeof(SwimState)] = new SwimState(this);
|
||
_dashState = (DashState)_states[typeof(DashState)];
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按类型获取状态实例。未注册时返回 null(供可选状态调用方安全使用)。
|
||
/// </summary>
|
||
public T GetState<T>() where T : PlayerStateBase
|
||
=> _states.TryGetValue(typeof(T), out var s) ? (T)s : null;
|
||
|
||
private void ResolveDependencies()
|
||
{
|
||
if (_movement == null)
|
||
_movement = GetComponent<PlayerMovement>();
|
||
if (_stats == null)
|
||
_stats = GetComponent<PlayerStats>();
|
||
if (_animancer == null)
|
||
_animancer = GetComponent<AnimancerComponent>();
|
||
if (_inputBuffer == null)
|
||
_inputBuffer = GetComponent<InputBuffer>();
|
||
|
||
// 将唯一配置点(_inputReader)注入到同一 GameObject 上的 InputBuffer。
|
||
// InputBuffer 不再需要在 Inspector 单独配置 InputReaderSO。
|
||
if (_inputBuffer != null)
|
||
_inputBuffer.Init(_inputReader);
|
||
}
|
||
|
||
private bool HasRequiredStateDependencies()
|
||
{
|
||
if (_dependenciesReady) return true;
|
||
|
||
bool ok = _movement != null && _animancer != null && _inputBuffer != null && _inputReader != null;
|
||
if (ok)
|
||
{
|
||
_dependenciesReady = true;
|
||
_missingDependencyLogged = false;
|
||
return true;
|
||
}
|
||
|
||
if (!_missingDependencyLogged)
|
||
{
|
||
Debug.LogError($"[PlayerController] Missing required dependencies. " +
|
||
$"Movement={(_movement != null ? "OK" : "NULL")}, " +
|
||
$"Animancer={(_animancer != null ? "OK" : "NULL")}, " +
|
||
$"InputBuffer={(_inputBuffer != null ? "OK" : "NULL")}, " +
|
||
$"InputReader={(_inputReader != null ? "OK" : "NULL")}");
|
||
_missingDependencyLogged = true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
|
||
// ── 状态访问器 ────────────────────────────────────────────────────────
|
||
// 使用 GetState<T>() 按类型获取任意状态实例,不再暴露具名属性。
|
||
}
|
||
}
|