unity 保存场景功能 可以保存运行时候地形变化和动态生成得物体,点击加载进来后可以继续上次退出时得场景

发布于:2025-04-14 ⋅ 阅读:(24) ⋅ 点赞:(0)

直接上代码了

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();
        }
    }
}
```![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/1052b5a6087d40c8ad2bc2eb8a957754.png)

还有地形编辑脚本

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