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