Files
zeling_v2/Assets/_Game/Scripts/Player/States/PlayerController.cs
Joywayer 06048c966a feat: Add HurtBoxOwnerGuard to prevent multiple damage registrations from the same HitBox activation
- Implemented HurtBoxOwnerGuard to ensure that multiple HurtBoxes on the same character do not register damage multiple times during a single HitBox activation.
- Added custom editor for HitBox to facilitate the creation of shape colliders with HitBoxColliderProxy.
- Introduced PhysicsPerceptionSystem for enemy perception, supporting multiple detection modes including RangeCircle, BatchLOS, FanCast, and BoxCast.
- Created EnemyPatrolZone to define patrol and chase areas for enemies, allowing for shared zones among multiple enemies.
- Added BD_IsOutsideZone conditional task for Behavior Designer to check if an enemy or player is outside a defined patrol zone.
2026-06-02 16:10:44 +08:00

426 lines
20 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 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() 时广播玩家 TransformEnemyBase / 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 LayerLayer 1供 SpringState / SoulSkill 等叠加层动画使用
// Base LayerLayer 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 LayerLayer 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 依赖(覆盖所有子节点 HurtBox支持多形状受击区
foreach (var hb in GetComponentsInChildren<HurtBox>(true))
{
if (_shield != null) hb.SetShieldable(_shield);
if (_parrySystem != null) hb.SetParrySystem(_parrySystem);
hb.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;
// 广播玩家 TransformEnemyBase / 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>() 按类型获取任意状态实例,不再暴露具名属性。
}
}