【Unity】Unity中实现回放功能

这篇具有很好参考价值的文章主要介绍了【Unity】Unity中实现回放功能。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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组件并把动画配置好。
unity记录与回放,Unity,unity
unity记录与回放,Unity,unity
在Player上挂载动画监测组件ReplayAnimator.cs和Transform监测组件ReplayTransform.cs,ReplayAnimator上的Rt变量赋值本身的ReplayTransform即可。
unity记录与回放,Unity,unity
再创建一个简单的人物控制类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);
    }
}

unity记录与回放,Unity,unity
Player控制完成,下面两个按钮,一个保存一个播放,并写一个简单的界面控制GameManager.cs进行测试。

unity记录与回放,Unity,unity
unity记录与回放,Unity,unity
将这个类挂载到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(); });
    }
}

unity记录与回放,Unity,unity
先运行程序,点击W按键控制Player行走一段距离,然后点击保存。

unity记录与回放,Unity,unity

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

unity记录与回放,Unity,unity

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

有什么问题给我留言吧。文章来源地址https://www.toymoban.com/news/detail-847601.html

到了这里,关于【Unity】Unity中实现回放功能的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Unity3D小功能】Unity3D中实现点击‘文字’出现‘UI面板’

    推荐阅读 CSDN主页 GitHub开源地址 Unity3D插件分享 简书地址 QQ群:398291828 大家好,我是佛系工程师 ☆恬静的小魔龙☆ ,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 宠粉博主又来了,今天有粉丝问我如何实现点击一段文字然后出现的面板在那段文字附近显示: 深入了

    2024年04月13日
    浏览(77)
  • 【Unity3D小功能】Unity3D中实现仿真时钟、表盘、仿原神时钟

    推荐阅读 CSDN主页 GitHub开源地址 Unity3D插件分享 简书地址 我的个人博客 大家好,我是佛系工程师 ☆恬静的小魔龙☆ ,不定时更新Unity开发技巧,觉得有用记得一键三连哦。 今天实现一个时钟工具,其实在之前已经完成了一个简单的时钟工具:【Unity3D应用案例系列】时钟、

    2024年02月05日
    浏览(72)
  • 在unity中实现视频的暂停播放和拖拽进度条的功能

    #Unity中实现视频的暂停播放和拖拽进度条的功能 在UI上,视频包含一个播放、暂停和停止按钮,以及一个拖动条,可以使用这些按钮来控制视频的播放,使用拖动进度条来调整视频的播放进度。 1.建立一个UI,导入视频素材,然后将视频拖放到场景中。 2.建立一个Canvas对象作

    2024年02月07日
    浏览(46)
  • 【Unity学习】完全基于Ultimate Replay 2.0的UI回放系统

    前面两节已经介绍了本人在项目中使用的结合JSON和Ultimate Replay 2.0的UI回放系统,那是在项目结构特殊,代码不好更改的情况下,本人所做的些许调整。但在这几天的开发过程中,我发现通常情况下只使用Ultimate Replay 2.0即可实现大部分情况下的UI回放。 这在基于JSON的UI回放系

    2024年02月12日
    浏览(56)
  • 【Unity】在Unity中实时显示北京时间

    感觉在网上搜到的大部分Unity或者C#获取北京时间的方法都已经不提供服务了,搜到一个可用的稍微拓展下做成了实时显示北京时间的脚本。 但因为只在程序启动的时候有获取北京时间,接下来显示的时间都是每秒钟在那个时间的基础上+1s,所以在编辑器里面只要拖动下整个

    2024年02月13日
    浏览(65)
  • Unity中实现2D遮罩

    一:前言 可以使用SpriteMask用作控制图形显示区域,SpriteRenderer用作显示图形,在SpriteRenderer中选择MaskInteraction遮罩类型 二:基础使用 创建一个空物体,添加SpriteMask组件,设置遮罩图片。创建一个空物体,添加SpriteRenderer组件用作显示图,设置SpriteRenderer的MaskInteraction遮罩类型

    2024年02月16日
    浏览(65)
  • 在Unity中实现优先队列

    前言 在.Net 6,7,8 中C#提供了优先队列PriorityQueueTElement,TPriority 类,详情参见官方文档PriorityQueueTElement,TPriority 类 (System.Collections.Generic) ,在Unity中想直接使用这个类时,发现不支持,没办法只好自己写一个了,这里讲一下我的实现思路和源码: 优先队列是什么? 百度百科定义:

    2024年01月16日
    浏览(29)
  • 【Unity大气渲染】Unity Shader中实现大气散射(半成品)

    写在前面 这是之前在做天空盒的时候同步写的分析博客,结果后面写到一半就忘了继续了,这里先贴出当时写的半成品,有小伙伴问我怎么做的,这里只能尽力把之前的半成品先放出来了(写得很乱,勿怪orz),,后面有机会会完善好的!希望能帮到大家~ 前置知识学习 【

    2024年02月08日
    浏览(46)
  • 在Unity中实现有限状态机

    本文将介绍Unity开发中的有限状态机,给出对应的实现代码。 有限状态机借鉴了图灵机的思想,可以看作是最简单的图灵机。 它包含4要素: 现态 条件 动作 次态 基础的有限状态机不复杂,无非是几个状态定义成类,提供OnEnter/OnExit/OnUpdate方法,这里直接根据需求给出对应的

    2024年02月05日
    浏览(49)
  • Unity中实现动画数据导出导入

    目录 数据导出: 数据导入 解析数据播放动画 根据曲线插值每帧计算数据,模拟Unity中动画播放系统,实现不通过动画控制器播放动画的功能,解决帧同步中动画结果无法预测问题,其实有可能涉及到对动画插值算法的模拟。 数据导出: 首先我们要大概梳理一下Unity中动画控

    2023年04月13日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包