Files
Cielonos/Assets/Scripts/MainGame/Characters/Base/Submodules/SelfTimeSubmodule.cs
SoulliesOfficial f26f9fd374 爆更
2026-03-20 12:07:44 -04:00

302 lines
12 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.Generic;
using UniRx;
using UnityEngine;
using SLSUtilities.General;
namespace Cielonos.MainGame.Characters
{
public partial class SelfTimeSubmodule : SubmoduleBase<CharacterBase>
{
private TimeManager timeManager => TimeManager.Instance;
private IObservable<float> 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<string, Timer> coolDownTimers = new Dictionary<string, Timer>();
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
{
/// <summary>
/// 添加一个基于本地时间Local DeltaTime的计时器
/// </summary>
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;
/// <summary>
/// 应用顿帧Hit Stop
/// </summary>
/// <param name="duration">持续时间(秒,基于全局游戏时间)</param>
/// <param name="targetScale">目标缩放倍率(通常为 0 或 0.1</param>
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); // 安全性:如果角色在顿帧期间死亡/销毁,自动取消计时器
}
/// <summary>
/// 使用曲线动态修改本地时间流速
/// </summary>
/// <param name="duration">持续时间(秒)</param>
/// <param name="zeroValue">曲线起点值</param>
/// <param name="oneValue">曲线终点值</param>
/// <param name="easeType">插值曲线null 则使用默认的 EaseInOut (从0到1) 曲线)</param>
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 Timer
{
public float originalDuration; // 可选:记录最初设置的持续时间,方便重置时使用
public float duration;
public float currentTime;
public float Percentage => duration > 0 ? Mathf.Clamp01(currentTime / duration) : 1f;
public bool IsCompleted => currentTime >= duration;
public Timer(float duration)
{
this.originalDuration = duration;
this.duration = duration;
this.currentTime = 0f;
}
public void Update(float deltaTime)
{
if (!IsCompleted)
{
currentTime += deltaTime;
}
}
/// <summary>
/// 重置计时器,可以选择新的持续时间(如果不提供则使用原始持续时间)
/// </summary>
/// <param name="newDuration"></param>
public void Reset(float newDuration = -1f)
{
currentTime = 0f;
duration = newDuration >= 0f ? newDuration : originalDuration;
}
}
}