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下

