时间速率拆分系统TimeScaleManager

背景

  控制时间速率是非常重要的功能,传统做法是修改Time.Scale,这会影响游戏的整体时间速率流逝,但是更多的需求则是单独控制各模块的时间速率,尤其是在有多种游戏机制同时运作的场景下,玩家或系统可能需要通过不同的时间速率处理各个部分的逻辑。
  通过时间速率拆分系统,不同部分的逻辑可以在不同时间速率下独立运行,比如动画、物理、特定事件等,TimeScaleManager将常用unity组件进行了封装,开发者无需关心其他组件对时间速率值的应用。
  目前TimeScaleManager继承mono,方便进行调试,实际上该系统可以脱离mono运行。

使用

解决方案说明

时间速率拆分系统的核心由以下三个主要类组成:

  1. TimeScaleManager:时间速率的管理器,用于管理所有时间相关的逻辑。
  2. TimeHolder:时间控制器,负责控制每个时间块的速率及其父子关系。
  3. TimeUser:时间使用者,绑定特定的TimeHolder,并根据时间速率进行自身组件的更新。
  4. 时间速率继承结构举例如下图:

添加时间控制器TimeHolder

开发者可以直接在GameObject挂载TimeHolder组件,也可以通过TimeScaleManager添加TimeHolder:

1
2
3
// 由于整套解决方案可以脱离mono运行,所以可以通过代码直接添加
// TimeScaleManager.Instance.AddTimeHolder(持有者名id,TimeHolder实例);
TimeScaleManager.Instance.AddTimeHolder("UI",holder);

TimeHolder

  TimeHolder 是每个时间控制块的核心类。它能够控制自己以及子TimeHolder的时间速率,可以用于实现更复杂的时间速率嵌套机制。TimeHolder 通过局部时间速率和父级时间速率的混合计算出实际的时间速率。

1
2
3
4
5
6
// 父子时间速率的混合计算方式
public enum TimeBlendMode
{
Multiplicative = 0, // 相乘
Additive = 1, // 相加
}

在Inspector中可以很直观地看出各个TimeHolder的父子关系,这也是为什么这套解决方案可以脱离mono但还是继承mono的原因:

TimeUser

TimeUser 是 TimeHolder 的 消费者,通常是某个游戏对象或组件,它们需要依据 TimeHolder 的时间速率进行更新,比如动画、物理、音效等。
以暂停界面PauseView的动画播放速度举例,其使用名为 “UI” 的TimeHolder的速率:

角色则使用以名为 “Game” 的TimeHolder的速率:

自动使用TimeUser的速率

  挂载了TimeUser组件的GameObject,会自动检索这个GameObject挂载的其他组件,对于已封装的unity内置组件,TimeUser会更改该组件的一系列使用到时间速率的行为。而开发者并不需要关心内部实现逻辑,也不需要手动修改组件的函数实现。
以动画播放器Animator举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 代码内部重写Animator的函数
public class AnimatorTimeUser: ComponentTimeUser<Animator>
{
public AnimatorTimeUser(TimeUser timeUser, Animator component) : base(timeUser, component) { }
private float _speed;

protected override void CopyProperties(Animator source)
{
_speed = source.speed;
}

protected override void AdjustProperties(float timeScale)
{
if (timeScale > 0)
{
Component.speed = _speed * timeScale;
return;
}
Component.speed = 0;
}
}

手动使用TimeUser的速率

以手动计算角色GameObject的移动速度举例:

1
2
3
4
5
6
7
8
9
10
11
12
public void Player
{
// 获取TimeUser
var _timerUser = transform.GetComponent<TimeUser>();

// 计算角色GameObject的当前速度
private void Update()
{
// 计算当前帧与上一帧的位移量,除以TimeUser的deltaTime,得出角色速度
speed = (currentFramePosition - lastFramePosition) / _timeUser.deltaTime;
}
}

内部实现

  • 通过父子继承,类似树形结构实现TimeHolder的嵌套计算。
  • 方案实现主要运用了装饰器的设计模式,对unity的内置组件往上再封装一层,在该层中应用TimeUser的速率,并重写组件的原函数实现。以装饰Rigibody举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 封装unity内置的Rigibody组件
    public abstract class RigidbodyTimeUser<TComponent>: ComponentTimeUser<TComponent>, IRigidbodyTimeUser where TComponent : Component
    {
    // 重写组件的 Update()
    public override void Update()
    {
    // 应用 TimeUser的时间速率
    switch (TimeUser.timeScale)
    {
    case <= 0:
    IsReallyManual = true;
    break;
    case > 0:
    IsReallyManual = IsManual;
    WakeUp();
    break;
    }
    }
    }