using System; using System.Collections; using System.Collections.Generic; using Cielonos.MainGame.Characters; using UniRx; using UnityEngine; namespace Cielonos.MainGame.Characters { public partial class SelfTimeSubmodule : SubmoduleBase { public FloatReactiveProperty timeScaleCoefficient; public float TimeScale => timeScaleCoefficient.Value * Time.timeScale; public float DeltaTime => timeScaleCoefficient.Value * Time.deltaTime; public SelfTimeSubmodule(CharacterBase entity) : base(entity) { timeScaleCoefficient = new FloatReactiveProperty(1); if (entity.animationSc != null) { timeScaleCoefficient.Subscribe(x => { entity.animationSc.fullBodyFuncAnimSm.currentPlaySpeedMultiplier = x; }); } if (entity.animationSc.animator != null) { timeScaleCoefficient.Subscribe(x => { entity.animationSc.animator.speed = x; }); } } } 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 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. 设置当前的缩放倍率 timeScaleCoefficient.Value = targetScale; // 3. 开启计时器 // 注意:这里使用 Scheduler.MainThread,它是基于 Time.time (全局时间) 的。 // 这意味着: // - 它会受到 Time.timeScale (全局暂停) 的影响(符合预期,游戏暂停时顿帧也该暂停)。 // - 它 *不会* 受到 timeScaleCoefficient (我们自己改的本地时间) 的影响(关键!)。 hitStopDisposable = Observable.Timer(TimeSpan.FromSeconds(duration), Scheduler.MainThread) .Subscribe(_ => { // 计时结束,恢复为 1 timeScaleCoefficient.Value = 1f; hitStopDisposable = null; }) .AddTo(owner); // 安全性:如果角色在顿帧期间死亡/销毁,自动取消计时器 } /// /// 使用曲线动态修改本地时间流速 /// /// 持续时间(秒) /// 曲线值为0时对应的时间倍率(通常是初始值) /// 曲线值为1时对应的时间倍率(通常是极值) /// 时间变化曲线(归一化:X轴0~1,Y轴通常0~1)。如果为null,则使用默认的“先升后降”抛物线。 public void ModifyTimeScale(float duration, float start, float peak, AnimationCurve curve = null) { // 1. 清理旧的计时器 hitStopDisposable?.Dispose(); // 2. 处理默认曲线逻辑 curve ??= DefaultParabola; // 3. 记录开始时的累计时间 float timer = 0f; // 4. 开启每帧更新的流 hitStopDisposable = Observable.EveryUpdate() .TakeWhile(_ => timer < duration) // 当时间超过 duration 时结束流 .Subscribe( _ => { // 累加时间 (使用 Time.deltaTime 以响应全局暂停) timer += Time.deltaTime; // 计算归一化进度 (0.0 ~ 1.0) float progress = Mathf.Clamp01(timer / duration); // 核心逻辑: // A. 从曲线获取当前的“强度” (Y轴值) float curveValue = curve.Evaluate(progress); // B. 在 start 和 peak 之间根据强度进行插值 // 当 curveValue = 0 时,结果为 start // 当 curveValue = 1 时,结果为 peak float currentScale = Mathf.Lerp(start, peak, curveValue); // C. 应用到响应式属性 timeScaleCoefficient.Value = currentScale; }, () => { // 5. 计时结束后的收尾工作 // 通常为了安全,结束后我们会强制恢复到 1.0 (正常速度) // 或者你可以恢复到 start,视具体需求而定 timeScaleCoefficient.Value = 1f; hitStopDisposable = null; } ) .AddTo(owner); // 绑定生命周期 } // 可选:提供一个强制恢复的方法,用于因为某些逻辑需要立刻打断顿帧时调用 public void ResetTimeScale() { hitStopDisposable?.Dispose(); timeScaleCoefficient.Value = 1f; } } }