unity运行中场景指定模型回放功能(模型是地形并且可以加载预制体进行回放)

回放和加载脚本

using System.Collections.Generic;
using UnityEngine;public class TerrainRecorder : MonoBehaviour
{[Header("基本设置")]public Terrain targetTerrain;public bool isRecording = false;public bool isPlayingBack = false;[Range(0.02f, 1f)] public float recordInterval = 0.1f;public float playbackSpeed = 1.0f;public int maxRecordFrames = 1000;[Header("物体录制")]public string objectTag = "DynamicObject";public bool recordActiveState = true;public bool recordTransforms = true;// 地形数据存储public  List<float[,]> terrainSnapshots = new List<float[,]>();private float timer = 0f;private int currentPlaybackIndex = 0;// 物体数据存储private List<ObjectSnapshot> objectSnapshots = new List<ObjectSnapshot>();private Dictionary<GameObject, int> objectIdMap = new Dictionary<GameObject, int>();private int nextObjectId = 1;private Dictionary<int, GameObject> playbackObjects = new Dictionary<int, GameObject>();void Update(){if (isRecording){timer += Time.deltaTime;if (timer >= recordInterval){RecordFrame();timer = 0f;}}if (isPlayingBack){PlaybackFrame();}}#region 录制功能void RecordFrame(){if (targetTerrain == null) return;// 检查并限制最大记录帧数if (terrainSnapshots.Count >= maxRecordFrames){terrainSnapshots.RemoveAt(0);objectSnapshots.RemoveAt(0);}RecordTerrainState();RecordObjectsState();}void RecordTerrainState(){TerrainData terrainData = targetTerrain.terrainData;int resolution = terrainData.heightmapResolution;float[,] heights = terrainData.GetHeights(0, 0, resolution, resolution);// 深度复制高度图数据float[,] snapshot = new float[resolution, resolution];System.Array.Copy(heights, snapshot, heights.Length);terrainSnapshots.Add(snapshot);}void RecordObjectsState(){GameObject[] dynamicObjects = GameObject.FindGameObjectsWithTag(objectTag);var snapshot = new ObjectSnapshot{frameCount = terrainSnapshots.Count - 1,objectStates = new List<ObjectState>()};foreach (var obj in dynamicObjects){if (!objectIdMap.ContainsKey(obj)){objectIdMap[obj] = nextObjectId++;}var state = new ObjectState{objectId = objectIdMap[obj],prefabName = GetPrefabName(obj),position = obj.transform.position,rotation = obj.transform.rotation,scale = obj.transform.localScale,isActive = recordActiveState ? obj.activeSelf : true};snapshot.objectStates.Add(state);}objectSnapshots.Add(snapshot);}// 从指定位置恢复回放(兼容旧代码)public void ResumePlaybackFromPosition(float progress){JumpToNormalizedTime(progress);ResumePlayback();}string GetPrefabName(GameObject obj){string name = obj.name.Replace("(Clone)", "");// 如果使用Addressables或其他资源系统,可以在这里添加特殊处理return name;}#endregion#region 回放功能void PlaybackFrame(){if (terrainSnapshots.Count == 0 || targetTerrain == null) return;timer += Time.deltaTime * playbackSpeed;if (timer >= recordInterval){if (currentPlaybackIndex < terrainSnapshots.Count){// 回放地形TerrainData terrainData = targetTerrain.terrainData;terrainData.SetHeights(0, 0, terrainSnapshots[currentPlaybackIndex]);// 回放物体PlaybackObjectsState(currentPlaybackIndex);currentPlaybackIndex++;}else{StopPlayback();}timer = 0f;}}void PlaybackObjectsState(int frameIndex){ClearPlaybackObjects();if (frameIndex >= objectSnapshots.Count) return;var snapshot = objectSnapshots[frameIndex];foreach (var state in snapshot.objectStates){GameObject prefab = Resources.Load<GameObject>(state.prefabName);if (prefab == null){Debug.LogWarning($"预制体加载失败: {state.prefabName}");continue;}GameObject obj = Instantiate(prefab, state.position, state.rotation);obj.transform.localScale = state.scale;obj.SetActive(state.isActive);obj.tag = objectTag; // 保持标签一致playbackObjects[state.objectId] = obj;}}void ClearPlaybackObjects(){foreach (var obj in playbackObjects.Values){if (obj != null) Destroy(obj);}playbackObjects.Clear();}#endregion#region 公共控制方法public void StartRecording(){ClearRecordings();isRecording = true;isPlayingBack = false;Debug.Log("开始录制地形和物体变化");}public void StopRecording(){isRecording = false;ClearAllDynamicObjects();Debug.Log($"停止录制,共录制 {terrainSnapshots.Count} 帧");}// 清除所有动态物体(包括回放生成的和场景中现有的)private void ClearAllDynamicObjects(){// 1. 清除回放时生成的物体ClearPlaybackObjects();// 2. 清除场景中现有的动态物体GameObject[] existingObjects = GameObject.FindGameObjectsWithTag(objectTag);foreach (var obj in existingObjects){// 确保只销毁场景实例,不销毁预制体资源if (obj.scene.IsValid() && !IsOriginalPrefab(obj)){Destroy(obj);}}}// 检查是否是原始预制体(不是场景实例)private bool IsOriginalPrefab(GameObject obj){// 通过名称判断或添加特殊组件/标签来识别return obj.name.EndsWith("Prefab") ||obj.GetComponent<OriginalPrefabMarker>() != null;}public void StartPlayback(){if (terrainSnapshots.Count == 0){Debug.LogWarning("没有录制数据可供回放");return;}isPlayingBack = true;isRecording = false;currentPlaybackIndex = 0;Debug.Log("开始回放录制内容");}public void StopPlayback(){isPlayingBack = false;Debug.Log("停止回放");}public void PausePlayback(){isPlayingBack = false;}public void ResumePlayback(){if (terrainSnapshots.Count > 0){isPlayingBack = true;}}public void ClearRecordings(){terrainSnapshots.Clear();objectSnapshots.Clear();objectIdMap.Clear();nextObjectId = 1;ClearPlaybackObjects();Debug.Log("已清除所有录制数据");}public void JumpToFrame(int frameIndex){frameIndex = Mathf.Clamp(frameIndex, 0, terrainSnapshots.Count - 1);currentPlaybackIndex = frameIndex;// 应用地形状态targetTerrain.terrainData.SetHeights(0, 0, terrainSnapshots[frameIndex]);// 应用物体状态PlaybackObjectsState(frameIndex);}public void JumpToNormalizedTime(float time){time = Mathf.Clamp01(time);int frameIndex = Mathf.FloorToInt(time * (terrainSnapshots.Count - 1));JumpToFrame(frameIndex);}public float GetCurrentProgress(){if (terrainSnapshots.Count == 0) return 0;return (float)currentPlaybackIndex / (terrainSnapshots.Count - 1);}public float GetTotalDuration(){return recordInterval * (terrainSnapshots.Count - 1);}#endregion#region 数据类[System.Serializable]class ObjectSnapshot{public int frameCount;public List<ObjectState> objectStates;}[System.Serializable]class ObjectState{public int objectId;public string prefabName;public Vector3 position;public Quaternion rotation;public Vector3 scale;public bool isActive;}#endregion
}
// 用于标记原始预制体的组件
public class OriginalPrefabMarker : MonoBehaviour { }

UI脚本

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;[RequireComponent(typeof(Slider))]
public class TerrainRecorderUI : MonoBehaviour, IDragHandler, IEndDragHandler
{public TerrainRecorder terrainRecorder;public Button recordButton;public Button stopButton;public Button playbackButton;public Slider playbackSlider;public Text timeText;private bool wasPlayingBeforeDrag = false;void Start(){// 初始化按钮事件recordButton.onClick.AddListener(() => {terrainRecorder.StartRecording();playbackSlider.value = 0;});stopButton.onClick.AddListener(() => {terrainRecorder.StopRecording();});playbackButton.onClick.AddListener(() => {terrainRecorder.StartPlayback();});// 滑块值变化事件(用于实时跳转)playbackSlider.onValueChanged.AddListener(OnSliderValueChanged);}void Update(){// 自动更新进度条(当没有拖动时)if (terrainRecorder.isPlayingBack && !IsDragging()){float progress = terrainRecorder.GetCurrentProgress();playbackSlider.SetValueWithoutNotify(progress); // 不触发onValueChangedUpdateTimeDisplay(progress);}}// 滑块值变化时的处理void OnSliderValueChanged(float value){if (terrainRecorder == null || terrainRecorder.terrainSnapshots.Count == 0)return;UpdateTimeDisplay(value);// 只有在拖动时才实时跳转if (IsDragging()){int frameIndex = Mathf.RoundToInt(value * (terrainRecorder.terrainSnapshots.Count - 1));terrainRecorder.JumpToFrame(frameIndex);}}// 开始拖动时处理public void OnDrag(PointerEventData eventData){if (terrainRecorder.isPlayingBack){wasPlayingBeforeDrag = true;terrainRecorder.PausePlayback();}}// 结束拖动时处理public void OnEndDrag(PointerEventData eventData){if (wasPlayingBeforeDrag){terrainRecorder.ResumePlaybackFromPosition(playbackSlider.value);wasPlayingBeforeDrag = false;}}// 检查是否正在拖动private bool IsDragging(){return Input.GetMouseButton(0); // 检查鼠标左键是否按住}// 更新时间显示void UpdateTimeDisplay(float progress){if (timeText != null){float totalTime = terrainRecorder.GetTotalDuration();float currentTime = progress * totalTime;timeText.text = $"{currentTime:F1}s / {totalTime:F1}s";}}
}

物体生成器脚本

using UnityEngine;public class DynamicObjectSpawner : MonoBehaviour
{public GameObject[] prefabs;public TerrainRecorder recorder;public float spawnInterval = 2f;private float timer;private Terrain terrain; // 添加地形引用void Start(){// 获取地形引用terrain = Terrain.activeTerrain;if (terrain == null){Debug.LogError("No active terrain found in the scene!");enabled = false;}}void Update(){if (!recorder.isRecording || terrain == null) return;timer += Time.deltaTime;if (timer >= spawnInterval){//  SpawnRandomObject();timer = 0f;}}void SpawnRandomObject(){if (prefabs == null || prefabs.Length == 0) return;// 生成随机位置(考虑地形边界)Vector3 position = new Vector3(Random.Range(0, terrain.terrainData.size.x),0,Random.Range(0, terrain.terrainData.size.z));// 调整Y坐标到地形表面position.y = terrain.SampleHeight(position) + terrain.transform.position.y;GameObject prefab = prefabs[Random.Range(0, prefabs.Length)];GameObject obj = Instantiate(prefab, position, Quaternion.identity);obj.tag = "DynamicObject";// 随机旋转和缩放obj.transform.rotation = Quaternion.Euler(Random.Range(0, 360f),Random.Range(0, 360f),Random.Range(0, 360f));float scale = Random.Range(0.5f, 2f);obj.transform.localScale = new Vector3(scale, scale, scale);}
}

设置步骤:

将TerrainRecorder添加到场景中的空对象

连接目标Terrain

创建UI并连接TerrainRecorderUI脚本

将动态物体预制体放入Resources文件夹

物体要求:

必须标记为指定的标签(默认"DynamicObject")

预制体名称不能包含"(Clone)"

建议使用简单的物体以保持性能

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/75298.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot的河道水情大数据可视化分析平台设计与实现(源码+论文+部署讲解等)

需要资料&#xff0c;请文末联系 一、平台介绍 水情监测数据大屏 - 平台首页 日均水位 日均水速 二、论文内容 摘要&#xff08;中文&#xff09; 本文针对河道水情监测领域的数据管理和可视化分析需求&#xff0c;设计并实现了一套河道水情大数据可视化分析平台。该平台基…

Knife4j文档请求异常 空指针

打开swagger文档报空指针异常 java.lang.NullPointerException: nullat springfox.documentation.oas.mappers.SchemaMapper.model(SchemaMapper.java:97)at springfox.documentation.oas.mappers.SchemaMapper.mapModel(SchemaMapper.java:85)at springfox.documentation.oas…

车辆选择解决方案

车辆选择解决方案 /* * Purpose: 添加车辆选择的功能 -> 用户在选择不同的车辆时&#xff0c;重新初始化系统状态&#xff0c;清除之前的定时器&#xff0c;并根据新选择的车辆设置新的定时器&#xff0c;以实现对新车辆状态的实时加载。 * File Name: 车辆选择解决方案 * …

魔塔社区使用llamafactory微调AI阅卷试题系统

启动 LLaMA-Factory 1. 安装 LLaMA-Factory 执行安装指令 git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Factory pip install -e ".[torch,metrics]"解决依赖冲突 如果遇到依赖冲突&#xff0c;可使用以下命令安装&#xff0c;不…

程序化广告行业(51/89):Cookie映射与移动设备ID映射解析

程序化广告行业&#xff08;51/89&#xff09;&#xff1a;Cookie映射与移动设备ID映射解析 在当今数字化营销的浪潮中&#xff0c;程序化广告已经成为企业精准触达目标客户的重要手段。作为一名对程序化广告充满兴趣的学习者&#xff0c;我希望通过这篇博客和大家一起深入探索…

内网服务器centos7安装jdk17

1. 下载 JDK 17 安装包&#xff08;在外网环境操作&#xff09; 在可联网的机器上下载 JDK 17 的压缩包&#xff08;推荐使用 OpenJDK&#xff09;&#xff1a; OpenJDK 官方源&#xff1a; Adoptium Eclipse Temurin Azul Zulu 直接下载命令示例&#xff08;在外网机器上执行…

【学Rust写CAD】21 2D 点(point.rs)

源码 //matrix/point.rs use std::ops::Mul; use super::algebraic_units::{Zero, One}; use super::generic::Matrix;/// 点坐标结构体 #[derive(Debug, Clone, Copy, PartialEq)] pub struct Point<X, Y>(Matrix<X, Y, One, Zero, Zero, One>);impl<X, Y>…

《AI大模型应知应会100篇》第7篇:Prompt Engineering基础:如何与大模型有效沟通

第7篇&#xff1a;Prompt Engineering基础&#xff1a;如何与大模型有效沟通 摘要 Prompt Engineering&#xff08;提示工程&#xff09;是与大模型高效沟通的关键技能。通过精心设计的Prompt&#xff0c;可以让模型生成更准确、更有用的结果。本文将从基础知识到高级策略&…

Java高频面试题1:Java SE

一、Java概述 1. Java语言的特点&#xff1f; 面向对象&#xff1a;封装、继承、多态。跨平台&#xff1a;通过JVM实现“一次编写&#xff0c;到处运行”。内存管理&#xff1a;自动垃圾回收&#xff08;GC&#xff09;&#xff0c;避免手动内存管理。多线程&#xff1a;内置…

基于RapidIO接口的DSP+GPU工业AI实时计算解决方案

基于RapidIO接口的DSPGPU工业AI实时计算解决方案是一种面向高性能、低延迟工业应用的异构计算架构&#xff0c;适用于工业自动化、机器视觉、预测性维护、机器人控制等场景。以下是该方案的核心设计思路和技术要点&#xff1a; 1. 方案背景与目标 工业需求&#xff1a; 工业…

SQL DB 数据类型

SQL DB 数据类型 引言 在数据库管理系统中,数据类型是定义和存储数据的方式。SQL(结构化查询语言)数据库中的数据类型决定了数据的存储格式、大小、取值范围以及如何处理数据。合理选择和使用数据类型对于确保数据库性能、数据完整性和应用程序的准确性至关重要。 SQL 数…

常见电源模块设计

目录 1. 5V电源模块 2. 3.3V电源模块 3. 1.9V电源模块 4. 220V转12V电源模块 1. 5V电源模块 参考电路 电路说明&#xff1a; 这个电路采用的是稳压芯片78L05&#xff0c;我是用的12V的电源模块转成为5V,为后续的供电。 2. 3.3V电源模块 参考电路&#xff1a; 电路说明…

python操作es

1、常用操作 ### 创建索引 bash curl -u elastic:123 -X PUT -H "Content-Type: application/json" -d mapping.json "http://0.0.0.0:9200/ai_kg_extraction_new_lower_tag_index" ### 删除索引 bash curl -u elastic:123 -X DELETE "http://0.0…

记一个.NET AOT交叉编译时的坑

记一个.NET AOT交叉编译时的坑 背景&#xff1a; 使用.NET9开发的Avalonia项目需要部署到Linux-arm64 踩坑&#xff1a; 根据官方AOT交叉编译文档配置后执行打包 dotnet publish -r linux-arm64提示error : The PrivateSdkAssemblies ItemGroup is required for _ComputeA…

【Linux篇】探索进程地址空间:计算机背后的虚拟世界

进程地址空间的奥秘&#xff1a;让你理解程序如何在计算机中生存 一. 程序地址空间1.1 基本概念1.2 虚拟内存管理1.3 为什么存在虚拟地址空间1.3.1 意义 2. 最后 本文将介绍进程地址空间的基本概念与结构&#xff0c;帮助读者理解操作系统如何管理和分配内存。进程地址空间指的…

17查询文档的方式

目录 1.鼠标放在你要查询的地方或者选中&#xff0c;按FnF1 2Assistant文档 3帮助菜单界面 1.鼠标放在你要查询的地方或者选中&#xff0c;按FnF1 2Assistant文档 3帮助菜单界面 大家一定要有 查询文档 的意识!! 未来实际开发中,一定会用到很多的第三方库和框架的. 很可能用到的…

壹起航:引领中国工厂迈向全球市场的先锋

在全球化的浪潮中&#xff0c;中国工厂正积极寻求拓展海外市场的新机遇。面对激烈的国际竞争&#xff0c;如何脱颖而出&#xff0c;成为行业翘楚&#xff1f;壹起航凭借其深厚的行业积淀和创新的营销理念&#xff0c;为中国工厂提供了全方位的出海解决方案。 一、构建国际化外…

“数据导航仪”:企业迁移知识库如何赋能精准决策

在全球化与区域经济一体化的浪潮下&#xff0c;企业迁移已成为经济发展的重要现象。 无论是为了拓展市场、降低成本&#xff0c;还是为了寻找更好的政策环境&#xff0c;企业迁移都牵动着无数从业者的心。 然而&#xff0c;面对海量且分散的企业迁移信息&#xff0c;金融机构…

理解激活函数,多个网络层之间如何连接

1. 激活函数如何在两个层之间作用 如果不在两个层之间添加激活函数&#xff0c;模型将无法学习非线性关系&#xff0c;表现出像线性模型一样的局限性。 LeakyReLU(0.2) 是一个激活函数&#xff0c;它的作用是对每一层的输出进行非线性转换。激活函数通常在神经网络中用于增加网…

红帽Linux怎么重置密码

完整流程 ●重启操作系统&#xff0c;进入启动界面 ●然后按进入选择项界面 ●找到linux单词开头的那一行&#xff0c;然后移动到该行末尾&#xff08;方向键移动或者使用键盘上的end&#xff09;&#xff0c;在末尾加入rd.break ●按ctrl x进入rd.break模式 ●在该模式下依次…