unity运行时进行录制并保存(可进行二次加载包含场景中生成动态物体)

TerrainRecorder 脚本:是核心脚本,承担地形和物体状态的录制、回放功能,同时处理拖动滑动条时的逻辑

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

/// <summary>
/// 地形录制与回放系统
/// </summary>
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;   // 是否记录变换信息

    [Header("内存管理")]
    public bool enableMemoryLimit = true;  // 是否启用内存限制
    [Tooltip("MB")]
    public int maxMemoryUsage = 4096;      // 最大内存使用量(MB)
    private bool _isCriticalMemory = false; // 内存临界状态标志

    // 运行时数据存储
    private List<float[,]> _terrainSnapshots = new List<float[,]>(); // 地形高度图快照
    private List<ObjectSnapshot> _objectSnapshots = new List<ObjectSnapshot>(); // 物体快照
    private Dictionary<GameObject, int> _objectIdMap = new Dictionary<GameObject, int>(); // 物体ID映射
    private Dictionary<int, GameObject> _playbackObjects = new Dictionary<int, GameObject>(); // 回放物体实例
    private int _nextObjectId = 1;         // 下一个物体ID
    private float _timer = 0f;             // 录制/回放计时器
    public int _currentPlaybackIndex = 0; // 当前回放帧索引

    #region 数据类定义
    /// <summary>
    /// 物体快照数据(单帧)
    /// </summary>
    [Serializable]
    private class ObjectSnapshot
    {
        public int frameIndex;             // 对应的帧索引
        public List<ObjectState> objectStates = new List<ObjectState>(); // 物体状态列表
    }

    /// <summary>
    /// 单个物体状态
    /// </summary>
    [Serializable]
    private class ObjectState
    {
        public int objectId;               // 物体唯一ID
        public string prefabName;          // 预制体名称
        public Vector3 position;           // 位置
        public Quaternion rotation;        // 旋转
        public Vector3 scale;              // 缩放
        public bool isActive;              // 是否激活
    }
    #endregion

    void Update()
    {
        if (isRecording) HandleRecording(); // 处理录制逻辑
        if (isPlayingBack) HandlePlayback(); // 处理回放逻辑
    }

    #region 核心功能
    /// <summary>
    /// 处理录制流程
    /// </summary>
    private void HandleRecording()
    {
        _timer += Time.deltaTime;
        if (_timer >= recordInterval)
        {
            if (CheckMemoryOverflow()) // 内存检查
            {
                Debug.LogError("内存超出限制!已停止录制");
                StopRecording();
                return;
            }

            RecordFrame(); // 录制当前帧
            _timer = 0f;
        }
    }

    /// <summary>
    /// 处理回放流程
    /// </summary>
    private void HandlePlayback()
    {
        // 如果正在拖动,不自动推进播放
        if (isDragging) return;
        _timer += Time.deltaTime * playbackSpeed;
        if (_timer >= recordInterval)
        {
            if (_currentPlaybackIndex < _terrainSnapshots.Count)
            {
                PlaybackFrame(_currentPlaybackIndex); // 回放当前帧
                _currentPlaybackIndex++;
            }
            else
            {
                StopPlayback(); // 到达末尾停止回放
            }
            _timer = 0f;
        }
    }
    /// <summary>
    /// 暂停回放(保持当前帧)
    /// </summary>
    public void PausePlayback()
    {
        if (isPlayingBack)
        {
            isPlayingBack = false;
            // 保持当前所有回放物体的状态
            Debug.Log("回放已暂停");
        }
    }

    /// <summary>
    /// 继续回放(从当前帧恢复)
    /// </summary>
    public void ResumePlayback()
    {
        if (!isPlayingBack && _terrainSnapshots.Count > 0)
        {
            isPlayingBack = true;
            _timer = 0f; // 重置计时器
            // 不需要重新加载帧,保持现有状态继续
            Debug.Log("回放已继续");
        }
    }

    /// <summary>
    /// 停止回放(清除回放状态)
    /// </summary>
    public void StopPlayback()
    {
        isPlayingBack = false;
        ClearPlaybackObjects();
        Debug.Log("回放已停止");
    }
    /// <summary>
    /// 录制单帧数据
    /// </summary>
    private void RecordFrame()
    {
        // 帧数限制检查
        if (_terrainSnapshots.Count >= maxRecordFrames)
        {
            RemoveOldestFrame(); // 移除最旧的帧
        }

        RecordTerrainState(); // 录制地形状态
        RecordObjectsState(); // 录制物体状态
    }

    /// <summary>
    /// 录制地形高度图
    /// </summary>
    private 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);
    }

    /// <summary>
    /// 录制场景物体状态
    /// </summary>
    private void RecordObjectsState()
    {
        GameObject[] dynamicObjects = GameObject.FindGameObjectsWithTag(objectTag);
        var snapshot = new ObjectSnapshot
        {
            frameIndex = _terrainSnapshots.Count - 1,
            objectStates = new List<ObjectState>()
        };

        // 为每个物体创建状态记录
        foreach (var obj in dynamicObjects)
        {
            if (!_objectIdMap.ContainsKey(obj))
            {
                _objectIdMap[obj] = _nextObjectId++; // 分配新ID
            }

            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);
    }

    #region 持久化存储
    /// <summary>
    /// 保存录制数据到文件
    /// </summary>
    public void SaveRecording(string fileName)
    {
        try
        {
            // 分块处理大数据
            int chunkSize = 100; // 每100帧一个块
            int totalChunks = Mathf.CeilToInt((float)_terrainSnapshots.Count / chunkSize);

            for (int chunk = 0; chunk < totalChunks; chunk++)
            {
                int startFrame = chunk * chunkSize;
                int endFrame = Mathf.Min((chunk + 1) * chunkSize, _terrainSnapshots.Count);

                SerializedRecording data = new SerializedRecording
                {
                    recordInterval = this.recordInterval,
                    terrainFrames = new List<SerializedTerrainFrame>(),
                    objectFrames = new List<SerializedObjectFrame>()
                };

                // 填充当前块的数据
                for (int i = startFrame; i < endFrame; i++)
                {
                    // 添加地形帧
                    if (i < _terrainSnapshots.Count)
                    {
                        var terrainFrame = new SerializedTerrainFrame
                        {
                            resolution = _terrainSnapshots[i].GetLength(0),
                            heightmapData = ConvertTo1DHeightmap(_terrainSnapshots[i])
                        };
                        data.terrainFrames.Add(terrainFrame);
                    }

                    // 添加物体帧
                    if (i < _objectSnapshots.Count)
                    {
                        var objectFrame = new SerializedObjectFrame();
                        foreach (var state in _objectSnapshots[i].objectStates)
                        {
                            objectFrame.objectStates.Add(new SerializedObjectState
                            {
                                objectId = state.objectId,
                                prefabName = state.prefabName,
                                position = state.position,
                                rotation = state.rotation,
                                scale = state.scale,
                                isActive = state.isActive
                            });
                        }
                        data.objectFrames.Add(objectFrame);
                    }
                }

                // 序列化当前块
                string json = JsonUtility.ToJson(data, true);
                if (string.IsNullOrEmpty(json))
                {
                    Debug.LogError($"序列化失败: 第 {chunk} 块数据");
                    continue;
                }

                // 保存分块文件
                string path = Path.Combine(Application.persistentDataPath, $"{fileName}_part{chunk}.rec");
                File.WriteAllText(path, json);
                Debug.Log($"成功保存分块 {chunk} 到: {path}");
            }
        }
        catch (System.Exception e)
        {
            Debug.LogError($"保存录制时出错: {e.Message}\n{e.StackTrace}");
        }
    }

    /// <summary>
    /// 从文件加载录制数据
    /// </summary>
    public void LoadRecording(string fileName)
    {
        try
        {
            ClearRecordings();
            int chunk = 0;

            while (true)
            {
                string path = Path.Combine(Application.persistentDataPath, $"{fileName}_part{chunk}.rec");
                if (!File.Exists(path)) break;

                string json = File.ReadAllText(path);
                SerializedRecording data = JsonUtility.FromJson<SerializedRecording>(json);

                if (data == null)
                {
                    Debug.LogError($"反序列化失败: {path}");
                    chunk++;
                    continue;
                }

                // 处理地形数据
                foreach (var frame in data.terrainFrames)
                {
                    _terrainSnapshots.Add(ConvertTo2DHeightmap(frame.heightmapData, frame.resolution));
                }

                // 处理物体数据
                for (int i = 0; i < data.objectFrames.Count; i++)
                {
                    var snapshot = new ObjectSnapshot
                    {
                        frameIndex = _terrainSnapshots.Count - data.objectFrames.Count + i,
                        objectStates = new List<ObjectState>()
                    };

                    foreach (var state in data.objectFrames[i].objectStates)
                    {
                        snapshot.objectStates.Add(new ObjectState
                        {
                            objectId = state.objectId,
                            prefabName = state.prefabName,
                            position = state.position,
                            rotation = state.rotation,
                            scale = state.scale,
                            isActive = state.isActive
                        });

                        if (state.objectId >= _nextObjectId)
                        {
                            _nextObjectId = state.objectId + 1;
                        }
                    }
                    _objectSnapshots.Add(snapshot);
                }

                this.recordInterval = data.recordInterval;
                chunk++;
            }

            _currentPlaybackIndex = 0;
            Debug.Log($"成功加载 {chunk} 个数据块,共 {_terrainSnapshots.Count} 帧");
        }
        catch (System.Exception e)
        {
            Debug.LogError($"加载录制时出错: {e.Message}\n{e.StackTrace}");
        }
    }

    /// <summary>
    /// 将2D高度图转换为1D数组
    /// </summary>
    private float[] ConvertTo1DHeightmap(float[,] heights)
    {
        int resolution = heights.GetLength(0);
        float[] flatData = new float[resolution * resolution];

        for (int y = 0; y < resolution; y++)
        {
            for (int x = 0; x < resolution; x++)
            {
                flatData[y * resolution + x] = heights[x, y];
            }
        }
        return flatData;
    }

    /// <summary>
    /// 将1D数组转换为2D高度图
    /// </summary>
    private float[,] ConvertTo2DHeightmap(float[] flatData, int resolution)
    {
        float[,] heights = new float[resolution, resolution];
        for (int y = 0; y < resolution; y++)
        {
            for (int x = 0; x < resolution; x++)
            {
                heights[x, y] = flatData[y * resolution + x];
            }
        }
        return heights;
    }
    #endregion

    #region 新增数据类(放在原有数据类区域)
    [Serializable]
    public class SerializedRecording
    {
        public float recordInterval;
        public List<SerializedTerrainFrame> terrainFrames = new List<SerializedTerrainFrame>();
        public List<SerializedObjectFrame> objectFrames = new List<SerializedObjectFrame>();
    }

    [Serializable]
    public class SerializedTerrainFrame
    {
        public float[] heightmapData;
        public int resolution;
    }

    [Serializable]
    public class SerializedObjectFrame
    {
        public List<SerializedObjectState> objectStates = new List<SerializedObjectState>();
    }

    [Serializable]
    public class SerializedObjectState
    {
        public int objectId;
        public string prefabName;
        public Vector3 position;
        public Quaternion rotation;
        public Vector3 scale;
        public bool isActive;
    }
    #endregion
    /// <summary>
    /// 回放指定帧
    /// </summary>
    private void PlaybackFrame(int index)
    {
        if (index >= _terrainSnapshots.Count) return;

        // 恢复地形状态
        targetTerrain.terrainData.SetHeights(0, 0, _terrainSnapshots[index]);

        // 恢复物体状态
        PlaybackObjectsState(index);
    }

    /// <summary>
    /// 回放物体状态
    /// </summary>
    private 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; // 记录回放实例
        }
    }
    #endregion

    #region 公共接口
    /// <summary>
    /// 开始录制
    /// </summary>
    public void StartRecording()
    {
        ClearRecordings();
        isRecording = true;
        isPlayingBack = false;
        _isCriticalMemory = false;
        Debug.Log("开始录制地形和物体变化");
    }

    /// <summary>
    /// 停止录制
    /// </summary>
    public void StopRecording()
    {
        isRecording = false;
        Debug.Log($"停止录制,共录制 {_terrainSnapshots.Count} 帧");
        // 清除所有动态生成的预制体
        ClearAllDynamicObjects();

        // 清除回放物体(如果有)
        ClearPlaybackObjects();
    }
    /// <summary>
    /// 清除所有动态生成的物体
    /// </summary>
    private void ClearAllDynamicObjects()
    {
        // 查找场景中所有带有指定标签的物体
        GameObject[] dynamicObjects = GameObject.FindGameObjectsWithTag(objectTag);

        // 销毁这些物体
        foreach (GameObject obj in dynamicObjects)
        {
            if (obj != null)
            {
                // 检查是否是预制体实例或者是录制期间生成的物体
                if (obj.name.EndsWith("(Clone)") || _objectIdMap.ContainsKey(obj))
                {
                    DestroyImmediate(obj);
                }
            }
        }

        // 清除物体ID映射
        _objectIdMap.Clear();

        // 强制垃圾回收
        Resources.UnloadUnusedAssets();
        System.GC.Collect();

        Debug.Log($"已清除 {dynamicObjects.Length} 个动态生成的物体");
    }
    /// <summary>
    /// 开始回放
    /// </summary>
    public void StartPlayback()
    {
        if (_terrainSnapshots.Count == 0)
        {
            Debug.LogWarning("没有录制数据可供回放");
            return;
        }

        isPlayingBack = true;
        isRecording = false;
        _currentPlaybackIndex = 0;
        Debug.Log("开始回放录制内容");
    }


    /// <summary>
    /// 跳转到指定帧
    /// </summary>
    public void JumpToFrame(int frameIndex)
    {
        frameIndex = Mathf.Clamp(frameIndex, 0, _terrainSnapshots.Count - 1);
        // 如果正在播放,先暂停
        bool wasPlaying = isPlayingBack;
        if (wasPlaying)
        {
            PausePlayback();
        }

        _currentPlaybackIndex = frameIndex;
        PlaybackFrame(frameIndex);

        // 如果不是拖动操作,恢复播放状态
        if (wasPlaying && !isDragging)
        {
            ResumePlayback();
        }
    }

    // ========== 状态查询接口 ==========
    public int GetCurrentFrameIndex() => _currentPlaybackIndex;
    public int GetTotalFrames() => _terrainSnapshots.Count;
    public float GetCurrentProgress() => _terrainSnapshots.Count == 0 ? 0 : (float)_currentPlaybackIndex / (_terrainSnapshots.Count - 1);
    public float GetTotalDuration() => recordInterval * (_terrainSnapshots.Count - 1);
    #endregion

    #region 辅助方法
    /// <summary>
    /// 获取物体预制体名称
    /// </summary>
    private string GetPrefabName(GameObject obj) => obj.name.Replace("(Clone)", "");

    /// <summary>
    /// 检查内存是否超限
    /// </summary>
    private bool CheckMemoryOverflow()
    {
        if (!enableMemoryLimit) return false;
        long usedMB = System.GC.GetTotalMemory(false) / (1024 * 1024);
        _isCriticalMemory = usedMB > maxMemoryUsage * 0.9f;
        return _isCriticalMemory;
    }

    /// <summary>
    /// 移除最旧的帧
    /// </summary>
    private void RemoveOldestFrame()
    {
        if (_terrainSnapshots.Count > 0) _terrainSnapshots.RemoveAt(0);
        if (_objectSnapshots.Count > 0) _objectSnapshots.RemoveAt(0);
    }

    /// <summary>
    /// 清除所有回放物体实例
    /// </summary>
    private void ClearPlaybackObjects()
    {
        foreach (var obj in _playbackObjects.Values)
        {
            if (obj != null) Destroy(obj);
        }
        _playbackObjects.Clear();
    }

    /// <summary>
    /// 清除所有录制数据
    /// </summary>
    private void ClearRecordings()
    {
        _terrainSnapshots.Clear();
        _objectSnapshots.Clear();
        _objectIdMap.Clear();
        _nextObjectId = 1;
        ClearPlaybackObjects();
    }
    #endregion
    [Header("性能监控")]
    public bool showPerformanceStats = true;
    private GUIStyle _statsStyle;

    void OnGUI()
    {
        if (!showPerformanceStats) return;

        if (_statsStyle == null)
        {
            _statsStyle = new GUIStyle(GUI.skin.box);
            _statsStyle.fontSize = 14;
        }

        string stats = $"录制状态: {(isRecording ? "进行中" : "停止")}\n" +
                      $"当前帧: {_currentPlaybackIndex}/{_terrainSnapshots.Count}\n" +
                      $"内存使用: {System.GC.GetTotalMemory(false) / (1024 * 1024)}MB/{maxMemoryUsage}MB\n" +
                      $"物体实例: {_playbackObjects.Count}";

        GUI.Box(new Rect(10, 10, 250, 100), stats, _statsStyle);
    }

    [Header("回放控制")]
    public bool isDragging = false; // 是否正在拖动进度条
    private float _dragStartTime; // 拖动开始时间
    private bool wasPlayingBeforeDrag; // 新增变量,用于记录拖拽前的播放状态

    /// <summary>
    /// 设置回放进度(0-1)
    /// </summary>
    public void SetPlaybackProgress(float progress)
    {
        progress = Mathf.Clamp01(progress);
        int targetFrame = Mathf.RoundToInt(progress * (_terrainSnapshots.Count - 1));
        JumpToFrame(targetFrame);
    }

    /// <summary>
    /// 开始拖动进度条
    /// </summary>
    public void OnBeginDrag()
    {
        isDragging = true;
        _dragStartTime = Time.time;
        wasPlayingBeforeDrag = isPlayingBack;

        if (isPlayingBack)
        {
            PausePlayback();
        }
    }

    /// <summary>
    /// 结束拖动进度条
    /// </summary>
    public void OnEndDrag()
    {
        isDragging = false;

        if (wasPlayingBeforeDrag && Time.time - _dragStartTime < 0.5f)
        {
            ResumePlayback();
        }
        else
        {
            wasPlayingBeforeDrag = false;
        }
    }
}

TerrainRecorderUI 脚本:此脚本用于管理 UI 按钮的交互,像开始录制、停止录制、播放 / 暂停、保存和加载录制数据等操作。

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class TerrainRecorderUI : MonoBehaviour
{
    [Header("控制器引用")]
    public TerrainRecorder terrainRecorder;
    public InputField fileNameInput;
    public PlaybackController playbackController; // 新增引用PlaybackController

    [Header("控制按钮")]
    public Button recordButton;
    public Button stopButton;
    public Button playPauseButton; // 合并播放/暂停按钮
    public Button saveButton;
    public Button loadButton;

    void Start()
    {
        recordButton.onClick.AddListener(StartRecording);
        stopButton.onClick.AddListener(StopRecording);
        playPauseButton.onClick.AddListener(TogglePlayPause);
        saveButton.onClick.AddListener(SaveRecording);
        loadButton.onClick.AddListener(LoadRecording);
    }

    private void StartRecording()
    {
        terrainRecorder.StartRecording();
    }

    private void StopRecording()
    {
        terrainRecorder.StopRecording();
    }

    private void TogglePlayPause()
    {
        if (terrainRecorder.isPlayingBack)
        {
            terrainRecorder.PausePlayback();
            playPauseButton.GetComponentInChildren<Text>().text = "播放";
        }
        else
        {
            // 如果是从暂停状态恢复,直接继续播放
            if (terrainRecorder.GetCurrentFrameIndex() < terrainRecorder.GetTotalFrames() - 1)
            {
                terrainRecorder.ResumePlayback();
            }
            else // 如果是重新开始播放
            {
                terrainRecorder.StartPlayback();
            }
            playPauseButton.GetComponentInChildren<Text>().text = "暂停";
        }
    }

    private void SaveRecording()
    {
        if (string.IsNullOrEmpty(fileNameInput.text))
        {
            Debug.LogWarning("请输入文件名");
            return;
        }
        terrainRecorder.SaveRecording(fileNameInput.text);
    }

    private void LoadRecording()
    {
        if (string.IsNullOrEmpty(fileNameInput.text))
        {
            Debug.LogWarning("请输入文件名");
            return;
        }
        terrainRecorder.LoadRecording(fileNameInput.text);
        // 加载完成后更新Slider的最大值
        if (playbackController != null)
        {
            int totalFrames = terrainRecorder.GetTotalFrames();
            playbackController.playbackSlider.maxValue = Mathf.Max(totalFrames - 1, 0);
            playbackController.playbackSlider.value = 0;
            playbackController.playbackSlider.interactable = (totalFrames > 0);
        }
    }
}

PlaybackController 脚本:负责同步滑动条的值和回放进度,处理滑动条的拖动事件,并且更新时间显示。

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class PlaybackController : MonoBehaviour, IBeginDragHandler, IEndDragHandler
{
    public TerrainRecorder terrainRecorder; // 引用TerrainRecorder组件
    public Slider playbackSlider; // 引用UI Slider组件
    public Text currentTimeText;   // 当前时间显示
    public Text totalTimeText;    // 总时间显示

    void Start()
    {
        if (terrainRecorder == null || playbackSlider == null) return;

        int totalFrames = terrainRecorder.GetTotalFrames();
        playbackSlider.maxValue = Mathf.Max(totalFrames - 1, 0); // 防止负数
        playbackSlider.value = 0;
        playbackSlider.interactable = (totalFrames > 0);

        // 绑定 On Value Changed 事件
        playbackSlider.onValueChanged.AddListener(OnSliderValueChanged);
    }

    void Update()
    {
        if (terrainRecorder == null || playbackSlider == null) return;

        int totalFrames = terrainRecorder.GetTotalFrames();
        if (totalFrames == 0)
        {
            playbackSlider.interactable = false;
            currentTimeText.text = "00:00:000";
            totalTimeText.text = "00:00:000";
            return;
        }

        // 非拖动状态下同步Slider值
        if (!terrainRecorder.isDragging && terrainRecorder.isPlayingBack)
        {
            // 确保_currentPlaybackIndex不会越界
            int currentFrame = Mathf.Clamp(terrainRecorder._currentPlaybackIndex, 0, totalFrames - 1);
            if (playbackSlider.value != currentFrame)
            {
                playbackSlider.value = currentFrame;
            }
        }

        // 更新时间显示
        float currentTime = terrainRecorder._currentPlaybackIndex * terrainRecorder.recordInterval;
        float totalTime = terrainRecorder.GetTotalDuration();
        currentTimeText.text = FormatTime(currentTime);
        totalTimeText.text = FormatTime(totalTime);
    }

    /// <summary>
    /// Slider值变化时更新回放进度
    /// </summary>
    public void OnSliderValueChanged(float value)
    {
        if (terrainRecorder == null) return;
        int targetFrame = Mathf.RoundToInt(value);
        terrainRecorder.JumpToFrame(targetFrame);
    }

    /// <summary>
    /// 拖动开始时调用
    /// </summary>
    public void OnBeginDrag(PointerEventData eventData)
    {
        if (terrainRecorder != null)
        {
            terrainRecorder.OnBeginDrag();
        }
    }

    /// <summary>
    /// 拖动结束时调用
    /// </summary>
    public void OnEndDrag(PointerEventData eventData)
    {
        if (terrainRecorder != null)
        {
            terrainRecorder.OnEndDrag();
        }
    }

    /// <summary>
    /// 时间戳格式化(mm:ss:fff)
    /// </summary>
    private string FormatTime(float seconds)
    {
        int minutes = Mathf.FloorToInt(seconds / 60);
        float sec = seconds % 60;
        int secondsInt = Mathf.FloorToInt(sec);
        float ms = (sec - secondsInt) * 1000;
        return $"{minutes:D2}:{secondsInt:D2}:{ms.ToString("F3").PadRight(3, '0')}";
    }
}

动态物体上编号

```csharp
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);
    }
}

动态物体得tag写成DynamicObject 放在Resources下 在这里插入图片描述

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/dd744a29d95d4230992bd6c8ffb9d842.png)
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/e4b02024959d442daaaba3e73bf39cb3.png)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值