直接上代码了
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
[System.Serializable]
public class TerrainSaveData
{
public int heightmapResolution;
public float terrainWidth;
public float terrainLength;
public float terrainHeight;
// 改为使用一维数组和尺寸信息来存储高度图
public float[] heightmapDataFlat;
public int heightmapSizeX;
public int heightmapSizeY;
}
[System.Serializable]
public class DynamicObjectData
{
public string prefabName;
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
}
[System.Serializable]
public class SceneSaveData
{
public TerrainSaveData terrainData;
public List<DynamicObjectData> dynamicObjects = new List<DynamicObjectData>();
}
public class SceneSaveLoadSystem : MonoBehaviour
{
public Terrain terrain;
public TerrainEditor terrainEditor; // 引用您的地形编辑器
public string saveFileName = "terrainSave";
[Header("UI References")]
public InputField saveNameInput;
public Button saveButton;
public Button loadButton;
// public Text statusText;
private void Start()
{
if (saveButton != null) saveButton.onClick.AddListener(SaveTerrain);
if (loadButton != null) loadButton.onClick.AddListener(LoadTerrain);
}
public void SaveTerrain()
{
try
{
if (terrain == null)
{
throw new NullReferenceException("terrain引用未设置");
}
SceneSaveData saveData = new SceneSaveData();
// 保存地形数据
saveData.terrainData = SaveTerrainData();
// 添加这行:保存动态物体
SaveDynamicObjects(saveData); // <-- 这是关键修复
// 检查数据有效性
if (saveData.terrainData.heightmapDataFlat == null)
{
throw new Exception("高度图数据为空");
}
if (saveData.dynamicObjects == null)
{
throw new Exception("动态物体列表为空");
}
string jsonData = JsonUtility.ToJson(saveData, true);
string savePath = Path.Combine(Application.persistentDataPath, GetSaveFileName() + ".json");
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
File.WriteAllText(savePath, jsonData);
Debug.Log($"保存成功,动态物体数量: {saveData.dynamicObjects.Count}");
UpdateStatus($"场景已保存,包含 {saveData.dynamicObjects.Count} 个动态物体");
}
catch (Exception e)
{
Debug.LogError($"保存失败: {e.Message}\n{e.StackTrace}");
UpdateStatus($"保存失败: {e.Message}");
}
}
public void LoadTerrain()
{
string fileName = GetSaveFileName();
string savePath = Path.Combine(Application.persistentDataPath, fileName + ".json");
Debug.Log($"正在从路径加载: {savePath}");
// 检查文件是否存在且非空
if (!File.Exists(savePath))
{
Debug.LogError($"文件不存在: {savePath}");
UpdateStatus("错误: 存档文件不存在");
return;
}
if (new FileInfo(savePath).Length == 0)
{
Debug.LogError($"文件为空: {savePath}");
UpdateStatus("错误: 存档文件为空");
return;
}
try
{
string jsonData = File.ReadAllText(savePath);
Debug.Log($"读取的JSON数据(前100字符): {jsonData.Substring(0, Math.Min(100, jsonData.Length))}");
SceneSaveData saveData = JsonUtility.FromJson<SceneSaveData>(jsonData);
// 详细检查反序列化结果
if (saveData == null)
{
throw new Exception("反序列化失败,saveData为null");
}
if (saveData.terrainData == null)
{
throw new Exception("地形数据为null");
}
if (saveData.terrainData.heightmapDataFlat == null)
{
throw new Exception("高度图数据为null");
}
Debug.Log($"即将加载地形数据,分辨率: {saveData.terrainData.heightmapResolution}");
// 确保地形引用有效
if (terrain == null || terrain.terrainData == null)
{
throw new Exception("地形引用无效");
}
LoadTerrainData(saveData.terrainData);
LoadDynamicObjects(saveData);
UpdateStatus($"场景已从 {fileName} 加载成功");
}
catch (Exception e)
{
Debug.LogError($"加载失败: {e.GetType()} - {e.Message}\n堆栈跟踪:\n{e.StackTrace}");
UpdateStatus($"加载失败: {e.Message}");
}
}
private TerrainSaveData SaveTerrainData()
{
TerrainData tData = terrain.terrainData;
int resolution = tData.heightmapResolution;
float[,] heights = tData.GetHeights(0, 0, resolution, resolution);
// 将二维数组转换为一维数组
float[] flatHeights = new float[resolution * resolution];
for (int y = 0; y < resolution; y++)
{
for (int x = 0; x < resolution; x++)
{
flatHeights[y * resolution + x] = heights[x, y];
}
}
return new TerrainSaveData
{
heightmapResolution = resolution,
heightmapDataFlat = flatHeights,
heightmapSizeX = resolution,
heightmapSizeY = resolution,
terrainWidth = tData.size.x,
terrainLength = tData.size.z,
terrainHeight = tData.size.y
};
}
private void LoadTerrainData(TerrainSaveData terrainData)
{
TerrainData tData = terrain.terrainData;
// 验证数据
if (terrainData.heightmapDataFlat == null || terrainData.heightmapDataFlat.Length == 0)
{
throw new Exception("高度图数据为空");
}
int targetResolution = tData.heightmapResolution;
float[,] heights;
// 如果分辨率匹配
if (targetResolution == terrainData.heightmapResolution)
{
heights = new float[targetResolution, targetResolution];
for (int y = 0; y < targetResolution; y++)
{
for (int x = 0; x < targetResolution; x++)
{
heights[x, y] = terrainData.heightmapDataFlat[y * targetResolution + x];
}
}
}
else
{
// 分辨率不匹配时需要重新采样
heights = ResampleHeightmap(terrainData);
}
tData.SetHeights(0, 0, heights);
}
private float[,] ResampleHeightmap(TerrainSaveData terrainData)
{
int sourceRes = terrainData.heightmapResolution;
int targetRes = terrain.terrainData.heightmapResolution;
float[,] result = new float[targetRes, targetRes];
float ratio = (float)sourceRes / targetRes;
for (int y = 0; y < targetRes; y++)
{
for (int x = 0; x < targetRes; x++)
{
float srcX = x * ratio;
float srcY = y * ratio;
int x1 = Mathf.FloorToInt(srcX);
int y1 = Mathf.FloorToInt(srcY);
int x2 = Mathf.Min(x1 + 1, sourceRes - 1);
int y2 = Mathf.Min(y1 + 1, sourceRes - 1);
// 双线性插值
float fx = srcX - x1;
float fy = srcY - y1;
float h11 = terrainData.heightmapDataFlat[y1 * sourceRes + x1];
float h21 = terrainData.heightmapDataFlat[y1 * sourceRes + x2];
float h12 = terrainData.heightmapDataFlat[y2 * sourceRes + x1];
float h22 = terrainData.heightmapDataFlat[y2 * sourceRes + x2];
result[x, y] = Mathf.Lerp(
Mathf.Lerp(h11, h21, fx),
Mathf.Lerp(h12, h22, fx),
fy);
}
}
return result;
}
private float[,] ResampleHeightmap(float[,] source, int sourceSize, int targetSize)
{
float[,] result = new float[targetSize, targetSize];
float ratio = (float)sourceSize / targetSize;
for (int y = 0; y < targetSize; y++)
{
for (int x = 0; x < targetSize; x++)
{
float srcX = x * ratio;
float srcY = y * ratio;
int x1 = Mathf.FloorToInt(srcX);
int y1 = Mathf.FloorToInt(srcY);
int x2 = Mathf.Min(x1 + 1, sourceSize - 1);
int y2 = Mathf.Min(y1 + 1, sourceSize - 1);
// 双线性插值
float fx = srcX - x1;
float fy = srcY - y1;
result[x, y] = Mathf.Lerp(
Mathf.Lerp(source[x1, y1], source[x2, y1], fx),
Mathf.Lerp(source[x1, y2], source[x2, y2], fx),
fy);
}
}
return result;
}
private void SaveDynamicObjects(SceneSaveData saveData)
{
GameObject[] dynamicObjects = GameObject.FindGameObjectsWithTag("DynamicObject");
foreach (var obj in dynamicObjects)
{
saveData.dynamicObjects.Add(new DynamicObjectData
{
prefabName = GetPrefabName(obj),
position = obj.transform.position,
rotation = obj.transform.rotation,
scale = obj.transform.localScale
});
}
}
private void LoadDynamicObjects(SceneSaveData saveData)
{
// 先清除现有动态物体
GameObject[] existingObjects = GameObject.FindGameObjectsWithTag("DynamicObject");
foreach (var obj in existingObjects)
{
Destroy(obj);
}
// 重新生成保存的动态物体
foreach (var objData in saveData.dynamicObjects)
{
// 实际项目中应该从资源管理系统加载预制体
var prefab = Resources.Load<GameObject>("ModePrefab/" + objData.prefabName);
if (prefab != null)
{
var newObj = Instantiate(prefab, objData.position, objData.rotation);
newObj.transform.localScale = objData.scale;
newObj.tag = "DynamicObject";
}
}
}
private string GetPrefabName(GameObject obj)
{
// 简化版本 - 实际项目中可能需要更复杂的逻辑
return obj.name.Replace("(Clone)", "").Trim();
}
private string GetSaveFileName()
{
return string.IsNullOrEmpty(saveNameInput?.text) ? saveFileName : saveNameInput.text;
}
private void UpdateStatus(string message)
{
// if (statusText != null) statusText.text = message;
Debug.Log(message);
}
// 编辑器快捷方式
#if UNITY_EDITOR
[ContextMenu("Quick Save")]
private void QuickSave()
{
SaveTerrain();
}
[ContextMenu("Quick Load")]
private void QuickLoad()
{
LoadTerrain();
}
#endif
// 修改后的动态物体生成方法
public GameObject SpawnDynamicObject(GameObject prefab, Vector3 position)
{
if (prefab == null) return null;
GameObject newObj = Instantiate(prefab, position, Quaternion.identity);
newObj.tag = "DynamicObject"; // 确保设置标签
newObj.name = prefab.name; // 保持名称一致
// 添加唯一标识组件(可选)
if (!newObj.GetComponent<DynamicObjectID>())
{
newObj.AddComponent<DynamicObjectID>();
}
return newObj;
}
// 唯一标识组件
public class DynamicObjectID : MonoBehaviour
{
public string originalPrefabName;
void Awake()
{
originalPrefabName = gameObject.name.Replace("(Clone)", "").Trim();
}
}
}
```
还有地形编辑脚本
```csharp
```csharp
using UnityEngine;
using UnityEngine.EventSystems; // 添加这个命名空间
public class TerrainEditor : MonoBehaviour
{
public Terrain terrain; // 地形对象
public float strength = 0.01f; // 拉伸强度
public float brushSize = 10f; // 笔刷大小
public bool useCircleBrush = true; // 是否使用圆形笔刷
void Update()
{
// 检查鼠标是否在UI上
if (EventSystem.current.IsPointerOverGameObject())
{
return; // 如果在UI上,则不执行地形编辑
}
// 鼠标左键提升地形,右键降低地形
if (Input.GetMouseButton(0))
{
EditTerrain(true); // 提升地形
}
else if (Input.GetMouseButton(1))
{
EditTerrain(false); // 降低地形
}
// 使用鼠标滚轮调整笔刷大小
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (scroll != 0)
{
brushSize += scroll * 5f;
brushSize = Mathf.Clamp(brushSize, 1f, 50f);
Debug.Log("Brush Size: " + brushSize);
}
// 按 B 键切换笔刷形状
if (Input.GetKeyDown(KeyCode.B))
{
useCircleBrush = !useCircleBrush;
Debug.Log("Brush Shape: " + (useCircleBrush ? "Circle" : "Square"));
}
}
void EditTerrain(bool raise)
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
if (hit.collider.GetComponent<Terrain>())
{
// 将鼠标点击的世界坐标转换为地形局部坐标
Vector3 terrainPos = hit.point - terrain.transform.position;
int x = (int)(terrainPos.x / terrain.terrainData.size.x * terrain.terrainData.heightmapResolution);
int y = (int)(terrainPos.z / terrain.terrainData.size.z * terrain.terrainData.heightmapResolution);
// 计算笔刷范围
int size = (int)brushSize;
int offsetX = x - size / 2;
int offsetY = y - size / 2;
// 检查边界,确保不会超出地形范围
offsetX = Mathf.Clamp(offsetX, 0, terrain.terrainData.heightmapResolution - size);
offsetY = Mathf.Clamp(offsetY, 0, terrain.terrainData.heightmapResolution - size);
// 获取当前高度图数据
float[,] heights = terrain.terrainData.GetHeights(offsetX, offsetY, size, size);
// 修改高度图数据
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size; j++)
{
// 计算当前点到笔刷中心的距离
float distance = Mathf.Sqrt((i - size / 2) * (i - size / 2) + (j - size / 2) * (j - size / 2));
// 如果是圆形笔刷且距离大于半径,则跳过
if (useCircleBrush && distance > size / 2)
{
continue;
}
// 根据 raise 参数提升或降低地形
if (raise)
{
heights[i, j] += strength * Time.deltaTime;
}
else
{
heights[i, j] -= strength * Time.deltaTime;
}
heights[i, j] = Mathf.Clamp01(heights[i, j]);
}
}
// 应用修改后的高度图数据
terrain.terrainData.SetHeights(offsetX, offsetY, heights);
}
}
}
}