1. 功能设计背景
因为近期项目有新的需求,需要对整个操作过程进行记录保存,并形成记录文件,后续可对此操作过程进行回放观看,效果类似于王者荣耀的回放机制,于是参考了多个资料,开发了此项功能。
2. 功能设计思路
回放本质就是保存数据、加载数据和数据重新利用的过程,因为我的项目是后期要加的回放功能,所以就把这块单独独立出来了。主要分为三个部分,创建数据结构、监测数据变化并保存、数据加载与数据重利用。
3. 功能实现
3.1 创建数据结构
既然是数据的保存和加载的过程,那就要建立一个合适的数据结构来承载这块数据,数据我采用结构体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;
}
}
回放用到的基础的数据类准备完成,下面进行数据的保存工作。
3.2 监测数据变化并保存
先创建一个总的管理类来储存和管理数据,创建回放保存系统控制类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的制作。
4. 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文章来源:https://www.toymoban.com/news/detail-847601.html
有什么问题给我留言吧。文章来源地址https://www.toymoban.com/news/detail-847601.html
到了这里,关于【Unity】Unity中实现回放功能的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!