回放和加载脚本
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)"
建议使用简单的物体以保持性能