using System; using System.Collections.Generic; using UniRx; using UnityEngine; using SLSUtilities.General; namespace Cielonos.MainGame.Characters { public partial class SelfTimeSubmodule : SubmoduleBase { private TimeManager timeManager => TimeManager.Instance; private IObservable timeScaleObservable; public FloatReactiveProperty localTimeScale; private float globalTimeScale => timeManager.globalTimeScale.Value; private float playerTimeScale => timeManager.playerTimeScale.Value; private float alliedMinionTimeScale => timeManager.alliedMinionTimeScale.Value; private float enemyTimeScale => timeManager.enemyTimeScale.Value; private float nonPlayerTimeScale => timeManager.nonPlayerTimeScale.Value; public Dictionary coolDownTimers = new Dictionary(); public float TimeScale => owner.fraction switch { Fraction.Player => localTimeScale.Value * globalTimeScale * playerTimeScale, Fraction.AlliedMinion => localTimeScale.Value * globalTimeScale * alliedMinionTimeScale * nonPlayerTimeScale, Fraction.Enemy => localTimeScale.Value * globalTimeScale * enemyTimeScale * nonPlayerTimeScale, Fraction.Neutral => localTimeScale.Value * globalTimeScale * nonPlayerTimeScale, _ => localTimeScale.Value * globalTimeScale }; public float DeltaTime => owner.fraction switch { Fraction.Player => Time.deltaTime * localTimeScale.Value * globalTimeScale * playerTimeScale, Fraction.AlliedMinion => Time.deltaTime * localTimeScale.Value * globalTimeScale * alliedMinionTimeScale * nonPlayerTimeScale, Fraction.Enemy => Time.deltaTime * localTimeScale.Value * globalTimeScale * enemyTimeScale * nonPlayerTimeScale, Fraction.Neutral => Time.deltaTime * localTimeScale.Value * globalTimeScale * nonPlayerTimeScale, _ => Time.deltaTime * localTimeScale.Value * globalTimeScale }; public SelfTimeSubmodule(CharacterBase entity) : base(entity) { localTimeScale = new FloatReactiveProperty(1); switch (owner.fraction) { case Fraction.Player: // 依赖: local, global, player timeScaleObservable = localTimeScale.CombineLatest( timeManager.globalTimeScale, timeManager.playerTimeScale, (local, global, player) => local * global * player ); break; case Fraction.AlliedMinion: // 依赖: local, global, alliedMinion, nonPlayer timeScaleObservable = localTimeScale.CombineLatest( timeManager.globalTimeScale, timeManager.alliedMinionTimeScale, timeManager.nonPlayerTimeScale, (local, global, minion, nonPlayer) => local * global * minion * nonPlayer ); break; case Fraction.Enemy: // 依赖: local, global, enemy, nonPlayer timeScaleObservable = localTimeScale.CombineLatest( timeManager.globalTimeScale, timeManager.enemyTimeScale, timeManager.nonPlayerTimeScale, (local, global, enemy, nonPlayer) => local * global * enemy * nonPlayer ); break; case Fraction.Neutral: // 依赖: local, global, nonPlayer timeScaleObservable = localTimeScale.CombineLatest( timeManager.globalTimeScale, timeManager.nonPlayerTimeScale, (local, global, nonPlayer) => local * global * nonPlayer ); break; default: throw new ArgumentOutOfRangeException(); } } public void SetUp(CharacterBase entity) { if (entity.animationSc != null) { timeScaleObservable.Subscribe(timeScale => { entity.animationSc.fullBodyFuncAnimSm.currentPlaySpeedMultiplier = timeScale; }).AddTo(entity); } if (entity.animationSc.animator != null) { timeScaleObservable.Subscribe(timeScale => { entity.animationSc.animator.speed = timeScale; }).AddTo(entity); } } public void Update() { // 这里不需要每帧计算 TimeScale,因为它是通过 ReactiveProperty 自动更新的 // 但是我们需要更新所有 Timer 的 currentTime foreach (var timer in coolDownTimers.Values) { timer.Update(DeltaTime); } } } public partial class SelfTimeSubmodule { /// /// 添加一个基于本地时间(Local DeltaTime)的计时器 /// public IDisposable AddLocalTimer(float duration, Action onComplete, Action onUpdate = null) { // 用于记录累积时间 float accumulatedTime = 0f; return Observable.EveryUpdate() .Select(_ => DeltaTime) // 1. 获取每帧的真实 DeltaTime .TakeWhile(dt => { // 2. 累加时间 accumulatedTime += dt; // 3. 如果累积时间小于总时长,继续流;否则停止流并触发 OnCompleted return accumulatedTime < duration; }) .Subscribe( _ => onUpdate?.Invoke(), // 每帧更新时执行 Action () => onComplete?.Invoke() // 4. 流结束时(TakeWhile 返回 false)执行 Action ).AddTo(owner); // 5. 绑定生命周期到角色,防止内存泄漏 } public IDisposable AddGlobalTimer(float duration, Action onComplete, Action onUpdate = null) { // 用于记录累积时间 float accumulatedTime = 0f; return Observable.EveryUpdate() .Select(_ => Time.deltaTime) // 1. 获取每帧的全局 DeltaTime .TakeWhile(dt => { // 2. 累加时间 accumulatedTime += dt; // 3. 如果累积时间小于总时长,继续流;否则停止流并触发 OnCompleted return accumulatedTime < duration; }) .Subscribe( _ => onUpdate?.Invoke(), // 每帧更新时执行 Action () => onComplete?.Invoke() // 4. 流结束时(TakeWhile 返回 false)执行 Action ).AddTo(owner); // 5. 绑定生命周期到角色,防止内存泄漏 } } public partial class SelfTimeSubmodule { // 缓存一个默认的抛物线曲线,避免每次 null 时都 new 一个 // 形状:(0,0) -> (0.5, 1) -> (1, 0) private static readonly AnimationCurve DefaultParabola = new AnimationCurve( new Keyframe(0f, 0f), new Keyframe(0.5f, 1f), new Keyframe(1f, 0f) ); private IDisposable hitStopDisposable; /// /// 应用顿帧(Hit Stop) /// /// 持续时间(秒,基于全局游戏时间) /// 目标缩放倍率(通常为 0 或 0.1) public void ModifyTimeScale(float duration, float targetScale = 0f) { // 1. 如果之前有正在进行的顿帧,先取消它(防止旧的恢复逻辑覆盖新的设置) hitStopDisposable?.Dispose(); // 2. 设置当前的缩放倍率 localTimeScale.Value = targetScale; // 3. 开启计时器 // 注意:这里使用 Scheduler.MainThread,它是基于 Time.time (全局时间) 的。 // 这意味着: // - 它会受到 Time.timeScale (全局暂停) 的影响(符合预期,游戏暂停时顿帧也该暂停)。 // - 它 *不会* 受到 timeScaleCoefficient (我们自己改的本地时间) 的影响(关键!)。 hitStopDisposable = Observable.Timer(TimeSpan.FromSeconds(duration), Scheduler.MainThread) .Subscribe(_ => { // 计时结束,恢复为 1 localTimeScale.Value = 1f; hitStopDisposable = null; }) .AddTo(owner); // 安全性:如果角色在顿帧期间死亡/销毁,自动取消计时器 } /// /// 使用曲线动态修改本地时间流速 /// /// 持续时间(秒) /// 曲线起点值 /// 曲线终点值 /// 插值曲线(null 则使用默认的 EaseInOut (从0到1) 曲线) public void ModifyTimeScale(float duration, EaseType easeType, float zeroValue = 0, float oneValue = 1) { // 1. 清理旧的计时器 hitStopDisposable?.Dispose(); AnimationCurve curve = Ease.GetCurve(easeType); // 3. 记录开始时的累计时间 float timer = 0f; // 4. 开启每帧更新的流 hitStopDisposable = Observable.EveryUpdate() .TakeWhile(_ => timer < duration) // 当时间超过 duration 时结束流 .Subscribe( _ => { // 累加时间 timer += DeltaTime; // 计算归一化时间(0 到 1) float normalizedTime = Mathf.Clamp01(timer / duration); // 根据曲线获取当前的插值系数 float curveValue = curve.Evaluate(normalizedTime); // 计算当前的时间缩放值 float currentScale = curveValue * (oneValue - zeroValue) + zeroValue; // 应用到 timeScaleCoefficient localTimeScale.Value = currentScale; }, () => { // 5. 计时结束后的收尾工作 // 通常为了安全,结束后我们会强制恢复到 1.0 (正常速度) // 或者你可以恢复到 start,视具体需求而定 localTimeScale.Value = 1f; hitStopDisposable = null; } ) .AddTo(owner); // 绑定生命周期 } // 可选:提供一个强制恢复的方法,用于因为某些逻辑需要立刻打断顿帧时调用 public void ResetTimeScale() { hitStopDisposable?.Dispose(); localTimeScale.Value = 1f; } } public class CooldownTimer : Timer { public float originalDuration; // 可选:记录最初设置的持续时间,方便重置时使用 public CooldownTimer(float duration) : base(duration) { this.originalDuration = duration; } /// /// 重置计时器,可以选择新的持续时间(如果不提供则使用原始持续时间) /// /// public override void Reset(float newDuration = -1f) { currentTime = 0f; duration = newDuration >= 0f ? newDuration : originalDuration; } } }