当前位置:   article > 正文

【Unity】Unity中实现回放功能_unity3d场景回放机制

unity3d场景回放机制

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[] { };
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

数据各种各样,位置数据、动画数据、操作数据等,只有分类保存才可以在使用时有序不乱,所以创建数据指令至关重要,本次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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

为了方便同步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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
/// <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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

回放用到的基础的数据类准备完成,下面进行数据的保存工作。

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++;
    }   
}
  • 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

创建动画信息监测类.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;
        }
    }
}
  • 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
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133

创建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);
        }
    }
}
  • 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

保存所用到的逻辑搭建完成,下面写回放时加载数据的逻辑,创建一个数据加载和播放控制类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);
    }
}
  • 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
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118

回放逻辑相对简单,下面进行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);
    }
}

  • 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

在这里插入图片描述
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(); });
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在这里插入图片描述
先运行程序,点击W按键控制Player行走一段距离,然后点击保存。

在这里插入图片描述

关闭程序,重新启动程序,点击播放按钮,会看到Player按照之前的轨迹和状态运动,这个Demo中回放的时候不要按W和S键,会和Player本身的控制类冲突。

在这里插入图片描述

Demo链接:百度网盘 提取码:02ls

有什么问题给我留言吧。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/633309
推荐阅读
相关标签
  

闽ICP备14008679号