在制作游戏中,我们有时候会播放过场动画或者剧情动画,有时候会需要有动画重新看,或者拖动进度条看每一帧信息的需求,那么怎么办呢,我们需要实现一个动画重放系统,实现逻辑主要是依靠Unity自带的动画曲线类(AnimationCurve),储存游戏物体从动画开始始末的运动轨迹。然后我们用一个重播管理器去管理各项数据,像播放视频一样控制每帧的位置信息,实现重放。以下是核心的两个脚本:
ReplayEntity.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine.AI;
namespace Replay
{
[Serializable]
public class TimelinedVector3
{
public AnimationCurve x;
public AnimationCurve y;
public AnimationCurve z;
public void Add (Vector3 v)
{
float time = ReplayManager.Singleton.GetCurrentTime ();
x.AddKey (time, v.x);
y.AddKey (time, v.y);
z.AddKey (time, v.z);
}
public Vector3 Get (float _time)
{
return new Vector3 (x.Evaluate (_time), y.Evaluate (_time), z.Evaluate (_time));
}
}
[Serializable]
public class TimelinedQuaternion
{
public AnimationCurve x;
public AnimationCurve y;
public AnimationCurve z;
public AnimationCurve w;
public void Add (Quaternion v)
{
float time = ReplayManager.Singleton.GetCurrentTime ();
x.AddKey (time, v.x);
y.AddKey (time, v.y);
z.AddKey (time, v.z);
w.AddKey (time, v.w);
}
public Quaternion Get (float _time)
{
return new Quaternion (x.Evaluate (_time), y.Evaluate (_time), z.Evaluate (_time), w.Evaluate (_time));
}
}
[Serializable]
public class RecordData
{
public TimelinedVector3 position;
public TimelinedQuaternion rotation;
public TimelinedVector3 scale;
public void Add (Transform t)
{
position.Add (t.position);
rotation.Add (t.rotation);
scale.Add (t.localScale);
}
public void Set (float _time, Transform _transform)
{
_transform.position = position.Get (_time);
_transform.rotation = rotation.Get (_time);
_transform.localScale = scale.Get (_time);
}
}
public class ReplayEntity : MonoBehaviour
{
public RecordData data = new RecordData ();
private Rigidbody rigidbody;
private NavMeshAgent agent;
private Animator animator;
protected virtual void Start ()
{
StartCoroutine (Recording ());
ReplayManager.Singleton.OnReplayTimeChange += Replay;
ReplayManager.Singleton.OnReplayStart += OnReplayStart;
rigidbody = GetComponent<Rigidbody> ();
agent = GetComponent<NavMeshAgent> ();
animator = GetComponent<Animator> ();
}
IEnumerator Recording ()
{
while (true) {
yield return new WaitForSeconds (1 / ReplayManager.Singleton.recordRate);
if (ReplayManager.Singleton.isRecording) {
data.Add (transform);
}
}
}
public void OnReplayStart ()
{
if (rigidbody != null)
rigidbody.isKinematic = true;
if (agent)
agent.enabled = false;
if (animator)
animator.enabled = false;
}
public void Replay (float t)
{
data.Set (t, transform);
}
}
}
ReplayManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System;
namespace Replay
{
public class ReplayManager : MonoBehaviour
{
public int recordRate = 120;
public bool isRecording = false;
public bool isPlaying = false;
public static ReplayManager Singleton;
public Action<float> OnReplayTimeChange;
public Action OnReplayStart;
private bool wasPlaying = true;
private bool replayReplayAvailable = false;
#region UI
public Slider _slide;
public Image _play;
public Image _replay;
public Image _pause;
public Text _timestamp;
public GameObject _replayCanvas;
#endregion
#region Time
private float _startTime;
private float _endTime;
#endregion
void Awake ()
{
if (ReplayManager.Singleton == null) {
ReplayManager.Singleton = this;
} else {
Destroy (gameObject);
}
}
public float GetCurrentTime ()
{
return Time.time - _startTime;
}
void StartReplay ()
{
_endTime = Time.time;
_replayCanvas.SetActive (true);
isPlaying = false;
_replayCanvas.GetComponent<CanvasGroup> ().alpha = 1;
_slide.maxValue = _endTime - _startTime;
OnReplayTimeChange (0);
RefreshTimer ();
if (OnReplayStart != null) {
// You can remove this log if you don't care
#if UNITY_EDITOR
Debug.Log ("There's " + OnReplayStart.GetInvocationList ().Length + " objects affected by the replay.");
#endif
OnReplayStart ();
}
}
// Use this for initialization
void Start ()
{
// This line call the replay to start after 3 seconds. You can remove this line and call StartReplay when you want.
Invoke ("StartReplay", 3f);
isRecording = true;
_startTime = Time.time;
_slide = _replayCanvas.GetComponentInChildren<Slider> ();
_play.GetComponent<Button> ().onClick.AddListener (() => Play ());
_pause.GetComponent<Button> ().onClick.AddListener (() => Pause ());
_replay.GetComponent<Button> ().onClick.AddListener (() => ReplayReplay ());
_slide.GetComponent<Slider> ().onValueChanged.AddListener ((Single v) => SetCursor (v));
EventTrigger trigger = _slide.GetComponent<EventTrigger> ();
{
EventTrigger.Entry entry = new EventTrigger.Entry ();
entry.eventID = EventTriggerType.PointerDown;
entry.callback.AddListener ((eventData) => {
wasPlaying = isPlaying;
Pause ();
});
trigger.triggers.Add (entry);
}
{
EventTrigger.Entry entry = new EventTrigger.Entry ();
entry.eventID = EventTriggerType.PointerUp;
entry.callback.AddListener ((eventData) => {
if (wasPlaying)
Play ();
});
trigger.triggers.Add (entry);
}
trigger = _slide.transform.parent.GetComponent<EventTrigger> ();
{
EventTrigger.Entry entry = new EventTrigger.Entry ();
entry.eventID = EventTriggerType.PointerExit;
entry.callback.AddListener ((eventData) => {
_slide.handleRect.transform.localScale = Vector3.zero;
});
trigger.triggers.Add (entry);
}
{
EventTrigger.Entry entry = new EventTrigger.Entry ();
entry.eventID = EventTriggerType.PointerEnter;
entry.callback.AddListener ((eventData) => {
_slide.handleRect.transform.localScale = Vector3.one;
});
trigger.triggers.Add (entry);
}
}
// Update is called once per frame
void Update ()
{
if (isPlaying) {
_slide.value += Time.deltaTime * Time.timeScale;
OnReplayTimeChange (_slide.value);
}
// You can remove/modify this if you use Space for something else
if (Input.GetKeyDown (KeyCode.Space)) {
if (isPlaying) {
Pause ();
} else {
Play ();
}
}
// ------
}
public void Play ()
{
_slide.Select ();
if (!isPlaying && _slide.value != _endTime - _startTime) {
isPlaying = true;
Swap (_play.gameObject, _pause.gameObject);
if (_play.transform.GetSiblingIndex () > _pause.transform.GetSiblingIndex ()) {
_play.transform.SetSiblingIndex (_pause.transform.GetSiblingIndex ());
}
}
}
void Swap (GameObject _out, GameObject _in = null, float delay = 0f)
{
if (_in != null) {
_in.SetActive (true);
}
_out.SetActive (false);
}
public void Pause ()
{
_slide.Select ();
if (isPlaying) {
isPlaying = false;
Swap (_pause.gameObject, _play.gameObject);
if (_pause.transform.GetSiblingIndex () > _play.transform.GetSiblingIndex ()) {
_pause.transform.SetSiblingIndex (_play.transform.GetSiblingIndex ());
}
}
}
public void ReplayReplay ()
{
_slide.value = 0;
replayReplayAvailable = false;
Swap (_replay.gameObject);
Play ();
}
public void SetCursor (Single value)
{
RefreshTimer ();
if (replayReplayAvailable) {
replayReplayAvailable = false;
Swap (_replay.gameObject, _play.gameObject);
}
if (_slide.value == _endTime - _startTime) {
Pause ();
replayReplayAvailable = true;
Swap (_play.gameObject, _replay.gameObject, .2f);
}
if (OnReplayTimeChange != null) {
OnReplayTimeChange (value + _startTime);
}
}
void RefreshTimer ()
{
float current = _slide.value;
float total = (_endTime - _startTime);
string currentMinutes = Mathf.Floor (current / 60).ToString ("00");
string currentSeconds = (current % 60).ToString ("00");
string totalMinutes = Mathf.Floor (total / 60).ToString ("00");
string totalSeconds = (total % 60).ToString ("00");
_timestamp.text = currentMinutes + ":" + currentSeconds + " / " + totalMinutes + ":" + totalSeconds;
}
#if UNITY_EDITOR
void OnDestroy ()
{
Debug.LogWarning (gameObject.name + " destroyed.");
}
#endif
}
}
接着,我们打开Unity,开始制作动画播放器预设体,预设体层级如下图:
我们给预设体加上ReplayManager.cs脚本,然后依次把Slider,Play等游戏物体拖上去,Inspector面板设置如下图:
接着,我们新建几个游戏物体在空中,给他们挂上刚体和ReplayEntity.cs脚本,如下图:
可以看到,运行之前,运动曲线的值是空的,接着我们点击运行,Cube开始下落动画,动画曲线开始赋值,运行后的动画曲线如下图:
然后我们可以看到,动画播放器的进度条出来了,我们可以拖动Slider观看每帧画面,也可以点击播放按钮,重放动画。