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); // 不触发onValueChanged
            UpdateTimeDisplay(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)"

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值