摄像机区域的架构改动

This commit is contained in:
2026-05-15 14:47:24 +08:00
parent 1b37297585
commit f264329751
3591 changed files with 1687228 additions and 446503 deletions

View File

@@ -0,0 +1,214 @@
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;
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);
}
/// <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 new WaitForSecondsRealtime(_config.BulletTimeDuration);
Time.timeScale = 1f;
}
}
}