【Unity教程】Time Machine: Rewinding Time – 时光机: 时光倒流
发表于2017-03-16

可以通过时光倒流看到发生过的事情,在有的游戏项目中已经有使用过有光倒流,那么在开发过程这个功能是如何实现的呢,下面就给大家介绍下在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 } } } |
最終效果
结语
透过这个简单的实作,可以了解到自订时间轴所带来的可控性,然而可控性的提升却会造成便利性大幅下降的情况。
任何需要与时间轴互动的事件、物件,都需要额外实作功能及行为的脚本,没办法很方便及快速的进行功能扩充。
取而代之,若是将所有需要纪录的物件,透过纪录快照功能,将每一个时间点的物件行为纪录并存取下来,或许就能够相当真正的达到时间控制。