You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity场景Save/Load:NPC及敌人复杂数据处理技术问询

嘿,我太懂你的困扰了——市面上大部分Unity存档示例要么太浅(只存个血量、位置),要么直接甩付费资产链接,根本没讲透复杂场景下的正确实现思路。针对你有NPC、敌人这类Actor的场景,我给你一套可扩展的纯手写存档方案,一步步拆解:

核心思路:面向数据的存档设计

千万别直接序列化MonoBehaviour组件!Unity的组件序列化依赖场景引用、对象实例,很容易出现加载后引用丢失、状态错乱的问题。正确的做法是把游戏状态和逻辑分离:用纯数据类(POCO)存储需要存档的信息,让Actor组件负责把自身状态转换成数据,再从数据恢复状态。

第一步:定义可存档的数据结构

先创建通用的Actor存档数据类,以及针对不同Actor类型的扩展数据:

using UnityEngine;

// 标记为可序列化,让Unity能把它转成Json/Binary
[System.Serializable]
public class ActorData
{
    public string uniqueId; // 全局唯一ID,用来匹配存档数据和场景中的Actor
    public ActorType actorType; // 区分是玩家、NPC还是敌人
    public Vector3 position;
    public Quaternion rotation;
    public int currentHealth;
    public bool isAlive;
    public string extraData; // 存储该Actor类型的专属数据(比如NPC的对话进度)
}

[System.Serializable]
public enum ActorType
{
    Player,
    NPC,
    Enemy
}

// NPC专属存档数据示例
[System.Serializable]
public class NPCExtraData
{
    public int dialogueProgress;
    public bool hasGivenQuest;
}

// 敌人专属存档数据示例
[System.Serializable]
public class EnemyExtraData
{
    public bool hasDroppedLoot;
    public float aggroRange;
}

第二步:创建可存档Actor基类

让所有需要存档的Actor(玩家、NPC、敌人)都继承这个基类,统一存档/读档接口:

public abstract class SaveableActor : MonoBehaviour
{
    [Header("存档配置")]
    public string uniqueId; // 场景内手动赋值,动态生成的Actor用Guid自动生成
    public ActorType actorType;

    // 抽象方法:由子类实现,返回自身的存档数据
    public abstract ActorData GetSaveData();

    // 抽象方法:由子类实现,从存档数据恢复自身状态
    public abstract void LoadFromData(ActorData data);
}

然后是NPC子类的实现示例:

public class NPC : SaveableActor
{
    public int dialogueProgress;
    public bool hasGivenQuest;

    public override ActorData GetSaveData()
    {
        var data = new ActorData();
        // 填充通用数据
        data.uniqueId = uniqueId;
        data.actorType = actorType;
        data.position = transform.position;
        data.rotation = transform.rotation;
        data.isAlive = true; // NPC默认存活,可根据需求调整

        // 把专属序列化成Json存到extraData
        var npcExtra = new NPCExtraData
        {
            dialogueProgress = this.dialogueProgress,
            hasGivenQuest = this.hasGivenQuest
        };
        data.extraData = JsonUtility.ToJson(npcExtra);

        return data;
    }

    public override void LoadFromData(ActorData data)
    {
        // 恢复通用状态
        transform.position = data.position;
        transform.rotation = data.rotation;

        // 解析专属数据
        var npcExtra = JsonUtility.FromJson<NPCExtraData>(data.extraData);
        dialogueProgress = npcExtra.dialogueProgress;
        hasGivenQuest = npcExtra.hasGivenQuest;
    }
}

敌人子类的实现逻辑类似,只需要替换EnemyExtraData即可。

第三步:全局存档管理器(单例模式)

创建一个SaveManager来统一处理存档的读写、数据收集和实例匹配:

using UnityEngine;
using System.Collections.Generic;
using System.IO;
using System.Linq;

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

    private void Awake()
    {
        // 单例实现,确保全局唯一
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
        DontDestroyOnLoad(gameObject);

        // 存档路径用Unity推荐的持久化目录,不同平台自动适配
        saveFilePath = Path.Combine(Application.persistentDataPath, "game_save.json");
    }

    // 保存游戏
    public void SaveGame()
    {
        var saveData = new SaveGameData();
        saveData.saveVersion = 1; // 存档版本,后续修改数据结构时用来兼容旧存档

        // 收集场景中所有可存档Actor的数据
        var allSaveableActors = FindObjectsOfType<SaveableActor>();
        foreach (var actor in allSaveableActors)
        {
            saveData.actorDatas.Add(actor.GetSaveData());
        }

        // 序列化成Json并写入文件
        string json = JsonUtility.ToJson(saveData, prettyPrint: true);
        File.WriteAllText(saveFilePath, json);
        Debug.Log($"存档成功!路径:{saveFilePath}");
    }

    // 加载游戏
    public void LoadGame()
    {
        if (!File.Exists(saveFilePath))
        {
            Debug.LogWarning("未找到存档文件,跳过加载");
            return;
        }

        // 读取并解析Json
        string json = File.ReadAllText(saveFilePath);
        var saveData = JsonUtility.FromJson<SaveGameData>(json);

        // 先恢复场景中已存在的Actor
        var allSaveableActors = FindObjectsOfType<SaveableActor>();
        foreach (var actor in allSaveableActors)
        {
            var matchingData = saveData.actorDatas.FirstOrDefault(d => d.uniqueId == actor.uniqueId);
            if (matchingData != null)
            {
                actor.LoadFromData(matchingData);
            }
        }

        // 处理动态生成的Actor(比如之前刷出的敌人)
        foreach (var actorData in saveData.actorDatas)
        {
            bool actorExists = allSaveableActors.Any(a => a.uniqueId == actorData.uniqueId);
            if (!actorExists)
            {
                // 根据ActorType实例化对应的预制体,这里可以改成用ScriptableObject管理预制体
                GameObject prefab = GetActorPrefab(actorData.actorType);
                if (prefab != null)
                {
                    GameObject newActor = Instantiate(prefab, actorData.position, actorData.rotation);
                    var saveableComp = newActor.GetComponent<SaveableActor>();
                    saveableComp.uniqueId = actorData.uniqueId;
                    saveableComp.LoadFromData(actorData);
                }
            }
        }

        Debug.Log("存档加载完成");
    }

    // 示例:根据ActorType获取预制体,实际项目可以用资源目录或配置表管理
    private GameObject GetActorPrefab(ActorType type)
    {
        switch (type)
        {
            case ActorType.Enemy:
                return Resources.Load<GameObject>("Prefabs/Enemy_Goblin");
            case ActorType.NPC:
                return Resources.Load<GameObject>("Prefabs/NPC_Blacksmith");
            default:
                return null;
        }
    }
}

// 全局存档数据容器
[System.Serializable]
public class SaveGameData
{
    public int saveVersion;
    public List<ActorData> actorDatas = new List<ActorData>();
    // 可以扩展全局数据:比如玩家背包、任务进度、游戏时间等
}

第四步:关键细节与优化

  • 唯一ID管理:场景中手动放置的Actor(比如固定NPC)可以手动赋值唯一ID(如npc_blacksmith_01);动态生成的Actor(比如刷出的敌人)在实例化时用Guid.NewGuid().ToString()生成唯一ID。
  • 存档版本控制:如果后续修改了ActorData结构(比如加了新字段),可以通过saveVersion判断,给旧存档的缺失字段设置默认值,避免加载报错。
  • 异步存档:大存档时用File.WriteAllTextAsync替代同步写入,避免主线程卡顿。
  • 数据加密:如果怕玩家篡改存档,可以用简单的XOR加密或者AES加密(学习阶段可以先跳过,核心逻辑走通后再添加)。

测试流程

  1. 在场景中给需要存档的Actor添加SaveableActor子类组件,设置唯一ID和ActorType。
  2. 通过UI按钮或快捷键调用SaveManager.Instance.SaveGame()存档。
  3. 退出游戏后重新启动,调用SaveManager.Instance.LoadGame()验证状态是否正确恢复。

内容的提问来源于stack exchange,提问作者Ben Rubin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 08:06:20