在 Unity 中实现“数据与逻辑分离”的存档架构,核心思想是:游戏对象负责逻辑,纯 C# 类负责携带数据,管理器负责磁盘 IO。

在 Unity 中实现“数据与逻辑分离”的存档架构,核心思想是:游戏对象负责逻辑,纯 C# 类负责携带数据,管理器负责磁盘 IO。

以下是一个基于 JSON + 接口 (Interface) + 单例管理器 的完整代码示例:


1. 定义数据传输对象 (DTO)

这是一个纯粹的 C# 类,只包含变量,用于被序列化。

C#

using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class GameSaveData
{
    // 玩家数据
    public Vector3 playerPosition;
    public float playerHealth;
    
    // 任务/物品数据(示例:使用字典需特殊处理,此处用列表简化)
    public List<string> inventoryItems = new List<string>();
    
    // 游戏状态
    public int currentLevel;
    public string lastSaveTime;

    public GameSaveData()
    {
        // 默认构造函数,初始化默认值
        lastSaveTime = DateTime.Now.ToString();
    }
}


2. 定义存档接口 (ISaveable)

任何需要存档的组件(如玩家、敌人、任务系统)都必须实现这个接口。

C#

public interface ISaveable
{
    // 将当前游戏状态存入 Data 对象
    void CaptureState(GameSaveData data);

    // 从 Data 对象恢复游戏状态
    void RestoreState(GameSaveData data);
}


3. 实现具体逻辑组件

例如,你的玩家控制脚本:

C#

using UnityEngine;

public class PlayerController : MonoBehaviour, ISaveable
{
    public float health = 100f;

    public void CaptureState(GameSaveData data)
    {
        data.playerPosition = transform.position;
        data.playerHealth = health;
        Debug.Log("玩家数据已捕获");
    }

    public void RestoreState(GameSaveData data)
    {
        transform.position = data.playerPosition;
        health = data.playerHealth;
        Debug.Log("玩家数据已恢复");
    }
    
    // 这里的逻辑代码(如移动、受伤)与存档代码互不干扰
    void Update() { /* 逻辑代码 */ }
}


4. 存档管理器 (SaveManager)

这是核心大脑,负责遍历所有实现接口的对象,并将结果写入磁盘。

C#

using UnityEngine;
using System.IO;
using System.Linq;

public class SaveManager : MonoBehaviour
{
    public static SaveManager Instance { get; private set; }

    private string savePath;
    private const string SAVE_FILENAME = "gamesave.json";

    private void Awake()
    {
        Instance = this;
        savePath = Path.Combine(Application.persistentDataPath, SAVE_FILENAME);
    }

    // 核心存档方法
    public void SaveGame()
    {
        GameSaveData data = new GameSaveData();

        // 1. 找到场景中所有实现了 ISaveable 接口的脚本
        var saveables = FindObjectsOfType<MonoBehaviour>().OfType<ISaveable>();

        // 2. 让每个脚本把自己的数据填进 data 对象
        foreach (var saveable in saveables)
        {
            saveable.CaptureState(data);
        }

        // 3. 序列化为 JSON 字符串
        string json = JsonUtility.ToJson(data, true);

        // 4. 写入文件
        File.WriteAllText(savePath, json);
        
        Debug.Log($"游戏已存档至: {savePath}");
    }

    // 核心读档方法
    public void LoadGame()
    {
        if (!File.Exists(savePath))
        {
            Debug.LogWarning("未找到存档文件");
            return;
        }

        // 1. 读取文件并反序列化
        string json = File.ReadAllText(savePath);
        GameSaveData data = JsonUtility.FromJson<GameSaveData>(json);

        // 2. 找到所有实现了接口的对象并分发数据
        var saveables = FindObjectsOfType<MonoBehaviour>().OfType<ISaveable>();
        foreach (var saveable in saveables)
        {
            saveable.RestoreState(data);
        }

        Debug.Log("游戏存档已加载");
    }
}


5. 如何使用?

在你的 UI 按钮或者触发器中直接调用:

C#

// 点击存档按钮
public void OnClickSave()
{
    SaveManager.Instance.SaveGame();
}

// 点击读档按钮
public void OnClickLoad()
{
    SaveManager.Instance.LoadGame();
}


为什么这是最佳实践?

  1. 低耦合SaveManager 不需要知道 PlayerController 的存在,它只认 ISaveable 接口。如果你以后增加了一个“任务系统”,只需让它实现接口,无需修改 SaveManager 的代码。
  2. 易于扩展:如果你想从 JSON 切换到二进制存储,你只需要修改 SaveManager 里的两行代码(序列化和反序列化部分),而不需要动所有的游戏逻辑脚本。
  3. 单一权责PlayerController 只负责管理自己的血量和坐标,而不必关心文件是怎么写入磁盘的。
  4. 调试友好:由于使用了 JSON,你可以直接打开 persistentDataPath 下的文件,手动修改血量或坐标来测试游戏。

进阶提示:在实际项目中,建议将 FindObjectsOfType 替换为在 OnEnable/OnDisable 时主动向 SaveManager 注册/注销,以提高性能。

Last Updated: