赞
踩
因为近期项目有新的需求,需要对整个操作过程进行记录保存,并形成记录文件,后续可对此操作过程进行回放观看,效果类似于王者荣耀的回放机制,于是参考了多个资料,开发了此项功能。
回放本质就是保存数据、加载数据和数据重新利用的过程,因为我的项目是后期要加的回放功能,所以就把这块单独独立出来了。主要分为三个部分,创建数据结构、监测数据变化并保存、数据加载与数据重利用。
既然是数据的保存和加载的过程,那就要建立一个合适的数据结构来承载这块数据,数据我采用结构体struct,方便快速存储和加载。
/// <summary> /// 回放数据类 /// </summary> public struct ReplayFrameData { public int index;//物体id public byte cmd;//回放指令 public byte cmd1;//备用指令1 public byte cmd2;//备用指令2 public string[] parames;//回放参数 public int frame;//帧索引 public int index1, index2;//备用索引1,备用索引2 public float index3;//备用索引3 public ReplayFrameData(byte cmd, int frame) : this() { this.cmd = cmd; this.frame = frame; parames = new string[] { }; } }
数据各种各样,位置数据、动画数据、操作数据等,只有分类保存才可以在使用时有序不乱,所以创建数据指令至关重要,本次Demo用到位置、动画这两种指令,有其他要保存的数据也按照此方式添加即可。
/// <summary> /// 回放指令 /// </summary> public class ReplayCommond { /// <summary> /// Transform同步指令 /// </summary> public const byte Transform = 100; /// <summary> /// 动画同步命令 /// </summary> public const byte Animator = 101; /// <summary> /// 动画参数同步命令 /// </summary> public const byte AnimatorParameter = 102; }
为了方便同步Transform信息,创建了两个数据类SaveVector3Data.cs和SaveQuaternionData.cs,直接保存Vector3和Quaternion包含用不到的东西太多,自己定义一个还方便一些。
/// <summary> /// float3数据 /// </summary> public class SaveVector3Data { public float x; public float y; public float z; public SaveVector3Data(float _x, float _y, float _z) { x = _x; y = _y; z = _z; } }
/// <summary> /// float4数据 /// </summary> public class SaveQuaternionData { public float x; public float y; public float z; public float w; public SaveQuaternionData(float _x, float _y, float _z, float _w) { x = _x; y = _y; z = _z; w = _w; } }
回放用到的基础的数据类准备完成,下面进行数据的保存工作。
先创建一个总的管理类来储存和管理数据,创建回放保存系统控制类ReplaySystem .cs
using UnityEngine; using System.Collections; using System.Collections.Generic; using System.IO; using Net.Component; using Net.Share; using Newtonsoft.Json; using System; /// <summary> /// 回放保存系统 /// </summary> public class ReplaySystem : MonoBehaviour { public static ReplaySystem Instance; private int relayObjectNumber = 10000;//用来标记用户标识ID(从10000开始标记) public List<ReplayFrameData> replayFrameDatas = new List<ReplayFrameData>();//回放信息储存器 private void Awake() { Instance = this; Application.targetFrameRate = 60;//保存时的帧率和回放的帧率要一致,不然镜头效果会出现变快或者变慢的情况 } /// <summary> /// 添加数据 /// </summary> /// <param name="data"></param> public void AddReplayFrameData(ReplayFrameData data) { replayFrameDatas.Add(data); } /// <summary> /// 保存回放文件 /// </summary> /// <param name="_name"></param> public void SaveReplayFile(string _planName, string leaderName, string time) { string json = JsonConvert.SerializeObject(replayFrameDatas); //路径可以自己选择,本次先保存在StreamingAssets中 string dirPath = Path.Combine(Application.streamingAssetsPath, "Replay"); if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath); string filePath = Path.Combine(dirPath, "replaytemp.json"); File.WriteAllText(filePath, json); } /// <summary> /// 获取一个新的ID /// </summary> public int GetNewID() { return relayObjectNumber++; } }
创建动画信息监测类.cs
using UnityEngine; using System.Collections; /// <summary> /// 回放信息动画记录组件 /// </summary> public class ReplayAnimator : MonoBehaviour { private Animator animator; private AnimatorParameter[] parameters; private int id; public ReplayTransform rt; private int nameHash = -1; private class AnimatorParameter { internal string name; internal AnimatorControllerParameterType type; internal float defaultFloat; internal int defaultInt; internal bool defaultBool; } private void Awake() { animator = GetComponent<Animator>(); parameters = new AnimatorParameter[animator.parameters.Length]; for (int i = 0; i < parameters.Length; i++) { parameters[i] = new AnimatorParameter() { type = animator.parameters[i].type, name = animator.parameters[i].name, defaultBool = animator.parameters[i].defaultBool, defaultFloat = animator.parameters[i].defaultFloat, defaultInt = animator.parameters[i].defaultInt }; } rt.animators.Add(this); id = rt.animators.Count - 1; } private void Update() { var nameHash1 = animator.GetCurrentAnimatorStateInfo(0).shortNameHash; for (int i = 0; i < parameters.Length; i++) { switch (parameters[i].type) { case AnimatorControllerParameterType.Bool: var bvalue = animator.GetBool(parameters[i].name); if (parameters[i].defaultBool != bvalue) { parameters[i].defaultBool = bvalue; ReplaySystem.Instance?.AddReplayFrameData(new ReplayFrameData(ReplayCommond.AnimatorParameter, Time.frameCount) { index = rt.ID, cmd1 = (byte)id, cmd2 = 1, index1 = i, index2 = bvalue ? 1 : 0 }); } break; case AnimatorControllerParameterType.Float: var fvalue = animator.GetFloat(parameters[i].name); if (parameters[i].defaultFloat != fvalue) { parameters[i].defaultFloat = fvalue; ReplaySystem.Instance?.AddReplayFrameData(new ReplayFrameData(ReplayCommond.AnimatorParameter, Time.frameCount) { index = rt.ID, cmd1 = (byte)id, cmd2 = 2, index1 = i, index3 = fvalue }); } break; case AnimatorControllerParameterType.Int: var ivalue = animator.GetInteger(parameters[i].name); if (parameters[i].defaultInt != ivalue) { parameters[i].defaultInt = ivalue; ReplaySystem.Instance?.AddReplayFrameData(new ReplayFrameData(ReplayCommond.AnimatorParameter, Time.frameCount) { index = rt.ID, cmd1 = (byte)id, cmd2 = 3, index1 = i, index2 = ivalue }); } break; } } if (nameHash != nameHash1) { nameHash = nameHash1; ReplaySystem.Instance?.AddReplayFrameData(new ReplayFrameData(ReplayCommond.Animator, Time.frameCount) { index = rt.ID, index1 = id, index2 = nameHash1 }); } } public void Play(int hashName) { animator.Play(hashName, 0); } public void SyncAnimatorParameter(ReplayFrameData opt) { switch (opt.cmd2) { case 1: parameters[opt.index1].defaultBool = opt.index2 == 1; animator.SetBool(parameters[opt.index1].name, parameters[opt.index1].defaultBool); break; case 2: parameters[opt.index1].defaultFloat = opt.index3; animator.SetFloat(parameters[opt.index1].name, parameters[opt.index1].defaultFloat); break; case 3: parameters[opt.index1].defaultInt = opt.index2; animator.SetInteger(parameters[opt.index1].name, parameters[opt.index1].defaultInt); break; } } }
创建Transform信息监测类ReplayTransform .cs
using UnityEngine; using System.Collections; using System.Collections.Generic; /// <summary> /// 回放信息Transform /// </summary> public class ReplayTransform : MonoBehaviour { public int ID; private SaveVector3Data rePos; private SaveQuaternionData reRot; private SaveVector3Data reScale; public List<ReplayAnimator> animators = new List<ReplayAnimator>(); private void Start() { rePos = new SaveVector3Data(transform.position.x, transform.position.y, transform.position.z); reRot = new SaveQuaternionData(transform.rotation.x, transform.rotation.y, transform.rotation.z, transform.rotation.w); reScale = new SaveVector3Data(transform.localScale.x, transform.localScale.y, transform.localScale.z); } private void Update() { if (transform.position.x != rePos.x || transform.position.y != rePos.y || transform.position.z != rePos.z || transform.rotation.x != reRot.x || transform.rotation.y != reRot.y || transform.rotation.z != reRot.z || transform.rotation.w != reRot.w || transform.localScale.x != reScale.x || transform.localScale.y != reScale.y || transform.localScale.z != reScale.z) { ReplaySystem.Instance?.AddReplayFrameData(new ReplayFrameData(ReplayCommond.Transform, Time.frameCount) { parames = new string[] { transform.position.x.ToString(), transform.position.y.ToString(), transform.position.z.ToString(), transform.rotation.x.ToString(), transform.rotation.y.ToString(), transform.rotation.z.ToString(), transform.rotation.w.ToString(), transform.localScale.x.ToString(), transform.localScale.y.ToString(), transform.localScale.z.ToString() }, index = ID }); rePos = new SaveVector3Data(transform.position.x, transform.position.y, transform.position.z); reRot = new SaveQuaternionData(transform.rotation.x, transform.rotation.y, transform.rotation.z, transform.rotation.w); reScale = new SaveVector3Data(transform.localScale.x, transform.localScale.y, transform.localScale.z); } } }
保存所用到的逻辑搭建完成,下面写回放时加载数据的逻辑,创建一个数据加载和播放控制类PlayBackSystem.cs。
using UnityEngine; using System.Collections; using System.Collections.Generic; using Newtonsoft.Json; using System; using System.IO; using System.Text; /// <summary> /// 回放加载播放控制类 /// </summary> public class PlayBackSystem : MonoBehaviour { public static PlayBackSystem Instance; private Dictionary<int, List<ReplayFrameData>> replayDataDic = new Dictionary<int, List<ReplayFrameData>>(); private Dictionary<int, ReplayTransform> replayTransDic = new Dictionary<int, ReplayTransform>(); public int curFrameIndex;//当前播放的帧数 public int rate = 60;//回放时播放的帧率,要和保存时的一致 private int maxFrameIndex;//加载的回放文件中最大的帧数 private int minFrameIndex = int.MaxValue;//加载的回放文件中最小帧数 private void Awake() { Instance = this; } public void Play() { //加载回放文件 string json = File.ReadAllText(Path.Combine(Application.streamingAssetsPath, "Replay", "replaytemp.json")); List<ReplayFrameData> reDatas = JsonConvert.DeserializeObject<List<ReplayFrameData>>(json); for (int i = 0; i < reDatas.Count; i++) { if (!replayDataDic.ContainsKey(reDatas[i].frame)) replayDataDic.Add(reDatas[i].frame, new List<ReplayFrameData>()); replayDataDic[reDatas[i].frame].Add(reDatas[i]); //确定加载的回访文件中最小和最大的帧数 if (reDatas[i].frame > maxFrameIndex) maxFrameIndex = reDatas[i].frame; if (reDatas[i].frame < minFrameIndex) minFrameIndex = reDatas[i].frame; } StartCoroutine(PlayBackAtor()); } public void AddTransDic(ReplayTransform t) { replayTransDic.Add(t.ID, t); } private IEnumerator PlayBackAtor() { while (true) { Debug.Log("当前回放帧数::" + curFrameIndex); if (curFrameIndex > maxFrameIndex) { Debug.Log("结束回放"); break; } if (replayDataDic.ContainsKey(curFrameIndex)) { List<ReplayFrameData> reDatas = replayDataDic[curFrameIndex]; for (int i = 0; i < reDatas.Count; i++) { switch (reDatas[i].cmd) { case ReplayCommond.Transform: ReplayTransform(reDatas[i]); break; case ReplayCommond.AnimatorParameter: ReplayAnimatorParameter(reDatas[i]); break; case ReplayCommond.Animator: ReplayAnimator(reDatas[i]); break; default: break; } } replayDataDic.Remove(curFrameIndex); } yield return new WaitForSeconds(1f / rate); curFrameIndex += 1; } } private void ReplayTransform(ReplayFrameData rdata) { if (replayTransDic.ContainsKey(rdata.index)) { ReplayTransform replayTransform = replayTransDic[rdata.index]; replayTransform.transform.position = new Vector3(float.Parse(rdata.parames[0]), float.Parse(rdata.parames[1]), float.Parse(rdata.parames[2])); replayTransform.transform.rotation = new Quaternion(float.Parse(rdata.parames[3]), float.Parse(rdata.parames[4]), float.Parse(rdata.parames[5]), float.Parse(rdata.parames[6])); replayTransform.transform.localScale = new Vector3(float.Parse(rdata.parames[7]), float.Parse(rdata.parames[8]), float.Parse(rdata.parames[9])); } } protected void ReplayAnimator(ReplayFrameData rdata) { if (replayTransDic.TryGetValue(rdata.index, out ReplayTransform t)) t.animators[rdata.index1].Play(rdata.index2); } private void ReplayAnimatorParameter(ReplayFrameData rdata) { if (replayTransDic.TryGetValue(rdata.index, out ReplayTransform t)) t.animators[rdata.cmd1].SyncAnimatorParameter(rdata); } }
回放逻辑相对简单,下面进行Demo的制作。
创建一个场景,导入一个至少带两个动画的模型,方便描述则命名为Player,将Player上挂载Animator组件并把动画配置好。
在Player上挂载动画监测组件ReplayAnimator.cs和Transform监测组件ReplayTransform.cs,ReplayAnimator上的Rt变量赋值本身的ReplayTransform即可。
再创建一个简单的人物控制类MoveCtr.cs控制Player行走和动画的播放,将该类挂载到Player上。
using UnityEngine; /// <summary> /// 行走控制类 /// </summary> public class MoveCtr : MonoBehaviour { public float speed; public Animator ani; void Start() { PlayBackSystem.Instance.AddTransDic(GetComponent<ReplayTransform>()); } void Update() { float v = Input.GetAxis("Vertical"); if (v == 0) return; Vector3 moveDir = new Vector3(0, 0, v); transform.position += transform.forward * moveDir.z * Time.deltaTime * speed; ani.SetFloat("move", v); } }
Player控制完成,下面两个按钮,一个保存一个播放,并写一个简单的界面控制GameManager.cs进行测试。
将这个类挂载到Canvas上,并将两个button拖上。
using UnityEngine; using UnityEngine.UI; /// <summary> /// UI控制 /// </summary> public class GameManager : MonoBehaviour { public Button saveBtn; public Button playBtn; void Start() { saveBtn.onClick.AddListener(() => { ReplaySystem.Instance.SaveReplayFile(); }); playBtn.onClick.AddListener(() => { PlayBackSystem.Instance.Play(); }); } }
先运行程序,点击W按键控制Player行走一段距离,然后点击保存。
关闭程序,重新启动程序,点击播放按钮,会看到Player按照之前的轨迹和状态运动,这个Demo中回放的时候不要按W和S键,会和Player本身的控制类冲突。
Demo链接:百度网盘 提取码:02ls
有什么问题给我留言吧。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。