【Unity教程】Time Machine: Rewinding Time – 时光机: 时光倒流

发表于2017-03-16
评论1 2.4k浏览
可以通过时光倒流看到发生过的事情,在有的游戏项目中已经有使用过有光倒流,那么在开发过程这个功能是如何实现的呢,下面就给大家介绍下在unity中的时光倒流效果实现。

简介

时光倒流,这个再熟悉不过的词往往都会出现在各种电影、游戏、动漫的背景设定中。
透过回顾已发生过的事件及事物,可以更加清楚的了解事情所发生的经过。
这次就来尝试实作时光倒流,这个好玩又有趣的效果。

机制核心

时光倒流机制的主要核心浅显易懂,不依赖游戏引擎所提供的时间轴,而依赖于自订时间轴,使物体的移动、旋转等透过自订时间轴而产生变化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public float Time
{
    get { return _time; }
    set
    {
        if (value != _time)
        {
            float deltaTime = value - _time;
 
            UpdateTime(deltaTime);
 
            _time = value;
        }
    }
}
private float _time;
ITimeMachine

在最初,先透过一个简单的介面来定义时间轴的变化。
任何需要与自订时间轴互动的行为,都需要继承并实作这个介面,使物体能够随时间而变化。
1
2
3
4
public interface ITimeMachine
{
    void UpdateTime(float deltaTime);
}
RewindAction

接著定义时光倒流时,所需要的回朔事件结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
 
public class RewindAction
{
    public float m_time;
    public Action m_action;
 
    public RewindAction(float time, Action action)
    {
        m_time = time;
        m_action = action;
    }
}
TimeMachineManager

透过这个唯一的时间轴管理器来注册、反注册所有物件、回朔事件以及时间轴更新。
只需要调用 TimeMachineManager.Instance.Time 就可以相当简单的改变所有已注册物件的时间轴。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
using System;
using System.Collections.Generic;
 
public class TimeMachineManager
{
    public static TimeMachineManager Instance
    {
        get
        {
            if (null == _instance)
            {
                _instance = new TimeMachineManager();
            }
 
            return _instance;
        }
    }
    private static TimeMachineManager _instance;
 
    public float Time
    {
        get { return _time; }
        set
        {
            if (value != _time)
            {
                float deltaTime = value - _time;
 
                if (deltaTime < 0)
                {
                    while(_rewindActions.Count > 0 && _rewindActions.Peek().m_time >= _time + deltaTime)
                    {
                        float curDeltaTime = _rewindActions.Peek().m_time - _time;
                        UpdateTime(curDeltaTime);
 
                        _rewindActions.Pop().m_action();
                        deltaTime -= curDeltaTime;
                    }
                }
 
                UpdateTime(deltaTime);
 
                _time = value;
            }
        }
    }
    private float _time;
 
    private List _timeMachineList;
    private Stack _rewindActions;
 
 
    public TimeMachineManager()
    {
        _timeMachineList = new List();
        _rewindActions = new Stack();
    }
 
 
    public void RegistertimeMachine(ITimeMachine timeMachine)
    {
        _timeMachineList.Add(timeMachine);
    }
 
 
    public void UnregisterTimeMachine(ITimeMachine timeMachine)
    {
        _timeMachineList.Remove(timeMachine);
    }
 
 
    public void AddRewindAction(Action action)
    {
        _rewindActions.Push(new RewindAction(Time, action));
    }
 
 
    private void UpdateTime(float deltaTime)
    {
        foreach (ITimeMachine timeMachine in _timeMachineList)
        {
            timeMachine.UpdateTime(deltaTime);
        }
    }
}
BaseTimeMachine

在这个范例中,所有被监控的行为都继承了 BaseTimeMachine,透过继承 BaseTimeMachine 来处理注册及反注册物件。
接著就可以处理任何想要透过自订时间轴而产生变化的行为了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;
 
public abstract class BaseTimeMachine : MonoBehaviour, ITimeMachine
{
    private void Awake()
    {
        TimeMachineManager.Instance.RegistertimeMachine(this);
 
        Initialize();
    }
 
 
    private void OnDestroy()
    {
        TimeMachineManager.Instance.UnregisterTimeMachine(this);
    }
 
 
    public virtual void Initialize(){}
    public abstract void UpdateTime(float deltaTime);
}

TimeMachine – Line Movement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using UnityEngine;
 
public class LineMovementTimeMachine : BaseTimeMachine
{
    public Vector3 Direction
    {
        get { return _direction; }
        set { _direction = value; }
    }
 
    public float Speed
    {
        get { return _speed; }
        set { _speed = value; }
    }
 
    [SerializeField] private bool _local;
    [SerializeField] private Vector3 _direction;
    [SerializeField] private float _speed;
 
    public override void UpdateTime(float deltaTime)
    {
        if (_local)
        {
            transform.localPosition += _direction.normalized * _speed * deltaTime;
        }
        else
        {
            transform.position += _direction.normalized * _speed * deltaTime;
        }
    }
}

TimeMachine – Rotate

1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine;
 
public class RotateTimeMachine : BaseTimeMachine
{
    [SerializeField] private Vector3 _direction;
    [SerializeField] private float _speed;
 
    public override void UpdateTime(float deltaTime)
    {
        transform.Rotate(_direction.normalized * _speed * deltaTime);
    }
}

TimeMachine – Particle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class ParticleSystemTimeMachine : BaseTimeMachine
{
    private ParticleSystem _particleSystem;
    private float _time;
 
    public override void Initialize()
    {
        _particleSystem = GetComponent();
        _particleSystem.randomSeed = (uint)(new System.Random().Next());
    }
 
 
    public override void UpdateTime(float deltaTime)
    {
        _time += deltaTime;
 
        _particleSystem.Simulate(_time, true, true);
    }
}
BaseAction

在上面的行为中,已经完成了很有趣的效果。但是在游戏中,我们往往会有一些例外状况需要处理,例如:颜色修改、实例化物件、隐藏物件…等。
为了注册这些例外状况,并产生相对应的处理,我们需要複写并继承抽象化类别。
1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
 
public abstract class BaseAction : MonoBehaviour
{
    private void Awake()
    {
        Initialize();
    }
 
    protected abstract void Initialize();
    public abstract void Action();
    public abstract void RewindAction();
}

Action – Color

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using UnityEngine;
 
public class ColorAction : BaseAction
{
    [SerializeField] private Color _color;
    private Color _preColor;
    private Material _material;
 
    protected override void Initialize()
    {
        _material = GetComponent().sharedMaterial;
    }
 
    public override void Action()
    {
        if (_material.color == _color)
            return;
 
        _preColor = _material.color;
        _material.color = _color;
 
        TimeMachineManager.Instance.AddRewindAction(RewindAction);
    }
 
    public override void RewindAction()
    {
        _material.color = _preColor;
    }
}

Action – Invisible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using UnityEngine;
 
public class InvisibleAction : BaseAction
{
    private Renderer _renderer;
 
    protected override void Initialize()
    {
        _renderer = GetComponent();
    }
 
    public override void Action()
    {
        if (null == _renderer)
            return;
 
        if (!_renderer.enabled)
            return;
 
        _renderer.enabled = false;
 
        TimeMachineManager.Instance.AddRewindAction(RewindAction);
    }
 
    public override void RewindAction()
    {
        _renderer.enabled = true;
    }
}

Action – Instantiate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using UnityEngine;
using System.Collections.Generic;
 
public class InstantiateAction : BaseAction
{
    [SerializeField] private GameObject _prefab;
    [SerializeField] private int _instantiateNumber = 1;
 
    private List _instances;
 
    protected override void Initialize()
    {
        _instances = new List();
    }
 
 
    public override void Action()
    {
        GameObject cacheObj = null;
        LineMovementTimeMachine cacheTimeline = null;
        LineMovementTimeMachine thisTimeline = null;
        thisTimeline = GetComponent();
 
        for (int count = 0; count < _instantiateNumber; count++)
        {
            cacheObj = Instantiate(_prefab);
            cacheObj.transform.SetParent(transform.parent);
            cacheObj.transform.position = GetRandomPosition(transform.position);
 
            cacheTimeline = cacheObj.GetComponent();
 
            if (null != cacheTimeline && null != thisTimeline)
            {
                cacheTimeline.Speed = thisTimeline.Speed;
                cacheTimeline.Direction = thisTimeline.Direction;
            }
 
            _instances.Add(cacheObj);
        }
 
        TimeMachineManager.Instance.AddRewindAction(RewindAction);
    }
 
    public override void RewindAction()
    {
        foreach (GameObject go in _instances)
        {
            Destroy(go);
        }
    }
 
    private Vector3 GetRandomPosition(Vector3 position)
    {
        return position + new Vector3(Random.Range(-1.0f, 1.0f), Random.Range(-1.0f, 1.0f), 0);
    }
}

Image Effect – Gray Scale

最后还可以再时光倒流时加入画面滤镜效果,让倒流的效果更加明确。
这边实作了基本的灰阶滤镜效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Shader "Hidden/GrayScaleImageEffectShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Cull Off ZWrite Off ZTest Always
 
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
             
            #include "UnityCG.cginc"
 
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
 
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };
 
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = v.uv;
                return o;
            }
             
            sampler2D _MainTex;
            float _saturation;
 
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
 
                float3 intensity = dot(col.rgb, float3(0.39, 0.59, 0.11));
                col.rgb = lerp(intensity, col.rgb, _saturation);
                return col;
            }
            ENDCG
        }
    }
}

最終效果

 结语


透过这个简单的实作,可以了解到自订时间轴所带来的可控性,然而可控性的提升却会造成便利性大幅下降的情况。

任何需要与时间轴互动的事件、物件,都需要额外实作功能及行为的脚本,没办法很方便及快速的进行功能扩充。

取而代之,若是将所有需要纪录的物件,透过纪录快照功能,将每一个时间点的物件行为纪录并存取下来,或许就能够相当真正的达到时间控制。


如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

0个评论