Files
zeling_v2/Assets/_Game/Scripts/Parry/ParrySystem.cs
Joywayer b7baf7ad6a Add WeaponFeedback component and AddressableManagerWindow meta file
- 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.
2026-05-22 22:03:32 +08:00

224 lines
8.6 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 System;
using System.Collections;
using UnityEngine;
using BaseGames.Input;
using BaseGames.Core.Events;
namespace BaseGames.Parry
{
/// <summary>
/// 弹反阶段枚举(架构 06_CombatModule §9
/// </summary>
public enum ParryPhase
{
Inactive, // 待机
Startup, // 前摇(按键 → 有效窗口开启)
Active, // 弹反有效窗口
EndLag, // 后摇(窗口关闭 → 动作恢复)
CounterWindow, // 弹反成功后的反击窗口
}
/// <summary>
/// 弹反状态机(架构 06_CombatModule §9
///
/// 组件设计:
/// - 订阅 InputReaderSO.ParryEvent → 触发 TryActivateParry
/// - 触发 OnParryActivated C# 事件 → PlayerController 转换到 ParryState
/// - HurtBox 调用 ConsumeParry() → 弹反成功时触发奖励并进入 CounterWindow
/// - 触发 OnParryConsumed(ParryInfo) C# 事件 → PlayerController 发放灵力 / 恢复护盾
/// - 可选触发 ParryInfoEventChannelSO SO 事件 → UI / 反馈系统
///
/// 程序集约束BaseGames.Parry 不引用 BaseGames.Combat → ConsumeParry() 无 DamageInfo 参数。
/// </summary>
public class ParrySystem : MonoBehaviour
{
[Header("配置")]
[SerializeField] private ParryConfigSO _config;
// _inputReader 由 PlayerController.Awake 注入,无需在 Inspector 单独配置。
private InputReaderSO _inputReader;
[Header("Event Channels - Raise可选")]
[SerializeField] private ParryInfoEventChannelSO _onParrySuccess;
// ── 运行时状态 ────────────────────────────────────────────────────────
private ParryPhase _phase = ParryPhase.Inactive;
private float _phaseTimer;
private float _cooldownTimer;
// 缓存 WaitForSecondsRealtime避免每次完美弹反都触发 GC 分配
private WaitForSecondsRealtime _bulletTimeWait;
public ParryPhase CurrentPhase => _phase;
/// <summary>是否处于弹反有效窗口(供外部检测)。</summary>
public bool IsParrying => _phase == ParryPhase.Active;
public bool IsInCounterWindow => _phase == ParryPhase.CounterWindow;
/// <summary>启用/禁用弹反输入(玩家能力解锁前设为 false。</summary>
public bool IsEnabled { get; set; } = true;
// ── C# 事件 ───────────────────────────────────────────────────────────
/// <summary>
/// 弹反成功时触发ConsumeParry 返回 true 后)。
/// PlayerController 订阅此事件以发放灵力并恢复护盾。
/// </summary>
public event Action<ParryInfo> OnParryConsumed;
/// <summary>
/// 弹反输入激活时触发(进入 Startup 阶段时)。
/// PlayerController 订阅此事件以转换到 ParryState。
/// </summary>
public event Action OnParryActivated;
// ── Unity 生命周期 ────────────────────────────────────────────────────
private void Awake()
{
Debug.Assert(_config != null, "[ParrySystem] _config 未赋值,请在 Inspector 中指定 ParryConfigSO。", this);
}
private void Start()
{
// 在 Start 中初始化,确保 _config 已被赋值Awake 之后)
if (_config != null)
_bulletTimeWait = new WaitForSecondsRealtime(_config.BulletTimeDuration);
}
/// <summary>由 PlayerController 在 Awake 中注入 InputReader无需在 Inspector 单独指定。</summary>
public void SetInputReader(InputReaderSO reader)
{
_inputReader = reader;
}
private void OnEnable()
{
if (_inputReader != null)
_inputReader.ParryEvent += TryActivateParry;
}
private void OnDisable()
{
if (_inputReader != null)
_inputReader.ParryEvent -= TryActivateParry;
}
private void Update()
{
// 冷却计时(使用 unscaledDeltaTime子弹时间期间也正常递减
if (_cooldownTimer > 0f)
_cooldownTimer -= Time.unscaledDeltaTime;
if (_phase == ParryPhase.Inactive) return;
_phaseTimer -= Time.unscaledDeltaTime;
if (_phaseTimer <= 0f) AdvancePhase();
}
// ── 外部 API ──────────────────────────────────────────────────────────
/// <summary>
/// HurtBox.ReceiveDamage 在步骤 2 调用此方法。
/// 若处于 Active 阶段则触发弹反成功流程并返回 true否则返回 false。
/// </summary>
public bool ConsumeParry()
{
if (_phase != ParryPhase.Active) return false;
bool isPerfect = IsInPerfectWindow();
int soulGain = isPerfect
? _config.SoulGainOnParry + _config.SoulGainOnPerfect
: _config.SoulGainOnParry;
// 完美弹反子弹时间
if (isPerfect)
StartCoroutine(ApplyBulletTime());
// 构造负载
var info = new ParryInfo { IsPerfect = isPerfect, SoulGained = soulGain };
// SO 事件UI / 特效 / 音效系统订阅)
_onParrySuccess?.Raise(info);
// C# 事件PlayerController 订阅以添加灵力 + 恢复护盾)
OnParryConsumed?.Invoke(info);
// 进入反击窗口
EnterPhase(ParryPhase.CounterWindow, _config.CounterWindowDuration);
return true;
}
/// <summary>
/// 由状态机ParryState.OnStateEnter或动画事件直接触发跳过 Startup 前摇直接进入 Active 窗口。
/// 若弹反已处于任意活跃阶段则忽略。
/// </summary>
public void OpenParryWindow()
{
if (_phase != ParryPhase.Inactive) return;
EnterPhase(ParryPhase.Active, _config.WindowDuration);
}
/// <summary>
/// 强制中断弹反ParryState.OnStateExit 或被击中打断时调用)。
/// </summary>
public void CloseParryWindow()
{
if (_phase == ParryPhase.Inactive) return;
_cooldownTimer = _config.ParryCooldown;
EnterPhase(ParryPhase.Inactive, 0f);
}
// ── 私有实现 ──────────────────────────────────────────────────────────
private void TryActivateParry()
{
if (!IsEnabled) return;
if (_phase != ParryPhase.Inactive) return;
if (_cooldownTimer > 0f) return;
EnterPhase(ParryPhase.Startup, _config.StartupDuration);
OnParryActivated?.Invoke();
}
private bool IsInPerfectWindow()
{
// Active 阶段计时器从 WindowDuration 倒计elapsed = WindowDuration - _phaseTimer
float elapsed = _config.WindowDuration - _phaseTimer;
return elapsed <= _config.PerfectParryThreshold;
}
private void AdvancePhase()
{
switch (_phase)
{
case ParryPhase.Startup:
EnterPhase(ParryPhase.Active, _config.WindowDuration);
break;
case ParryPhase.Active:
EnterPhase(ParryPhase.EndLag, _config.EndlagDuration);
break;
case ParryPhase.EndLag:
case ParryPhase.CounterWindow:
_cooldownTimer = _config.ParryCooldown;
EnterPhase(ParryPhase.Inactive, 0f);
break;
}
}
private void EnterPhase(ParryPhase phase, float duration)
{
_phase = phase;
_phaseTimer = duration;
}
private IEnumerator ApplyBulletTime()
{
Time.timeScale = _config.BulletTimeScale;
yield return _bulletTimeWait;
Time.timeScale = 1f;
}
}
}