Unity中实现动画回放功能

时间:2024-04-12 11:31:52

在制作游戏中,我们有时候会播放过场动画或者剧情动画,有时候会需要有动画重新看,或者拖动进度条看每一帧信息的需求,那么怎么办呢,我们需要实现一个动画重放系统,实现逻辑主要是依靠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,开始制作动画播放器预设体,预设体层级如下图:

Unity中实现动画回放功能

我们给预设体加上ReplayManager.cs脚本,然后依次把Slider,Play等游戏物体拖上去,Inspector面板设置如下图:

Unity中实现动画回放功能

接着,我们新建几个游戏物体在空中,给他们挂上刚体和ReplayEntity.cs脚本,如下图:

Unity中实现动画回放功能

可以看到,运行之前,运动曲线的值是空的,接着我们点击运行,Cube开始下落动画,动画曲线开始赋值,运行后的动画曲线如下图:

Unity中实现动画回放功能

然后我们可以看到,动画播放器的进度条出来了,我们可以拖动Slider观看每帧画面,也可以点击播放按钮,重放动画。

Unity中实现动画回放功能

 

资源来源:https://github.com/FeNo/InGameReplay