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

Unity3D百万人口城市建造游戏自动存档系统优化方案问询

Unity百万级人口城市建造游戏自动存档优化方案

针对你当前用JSON+PlayerPrefs存储百万级人口数据导致的性能瓶颈,以下是可落地的优化方案:

1. 替换PlayerPrefs,改用文件系统存储

PlayerPrefs设计用于存储小量配置数据,不适合百万级体量的存档。建议直接使用文件流写入本地文件:

  • 将建筑和人口数据拆分到独立文件(如buildings_v2.dat、population_v2.dat),减少单文件体积,也方便后续单独加载某类数据。
  • 使用FileStream结合异步写入逻辑,避免阻塞主线程。

2. 放弃逐帧全量存档,采用「增量存档+定时全量备份」模式

逐帧全量序列化完全是资源浪费,只记录变更数据即可:

  • 给每个建筑、人物添加isDirty标记,当状态发生变更(如人物换职业、进出建筑、建筑功能状态变化)时,将标记设为true。
  • 设定固定间隔(如每30秒)仅序列化并存储标记为dirty的对象,完成后清除标记。
  • 每日/每小时执行一次全量存档作为备份,防止增量数据损坏或丢失。

示例代码(脏标记实现):

public class Citizen : MonoBehaviour
{
    public int CitizenID;
    public CitizenState CurrentState; // InBuilding / Walking
    public int TargetBuildingID;
    public string Profession;
    public bool isDirty;

    // 状态变更时调用
    public void UpdateState(CitizenState newState, int targetBuildingID = 0)
    {
        CurrentState = newState;
        TargetBuildingID = targetBuildingID;
        isDirty = true;
        ArchiveManager.Instance.RegisterDirtyCitizen(this);
    }
}

3. 替换JSON,改用高效二进制序列化

JSON的序列化/反序列化开销极高,尤其面对百万级数据时,二进制序列化能大幅提升性能:

  • 使用Unity自带的BinaryFormatter(注意跨版本兼容性,或选择MessagePack for Unity等第三方高效序列化库)。
  • 手动编写序列化逻辑,只存储必要字段:比如人物无需存档「闲聊音效文字特效」这类实时生成的状态,仅保留ID、当前状态、目标建筑ID、职业核心数据。

4. 异步处理存档逻辑,避免主线程阻塞

将序列化和文件写入操作放到后台线程执行,Unity中可通过Task.Run实现:

  • 先在主线程中拷贝脏数据的副本(转为线程安全的DTO类,避免后台线程访问Unity对象)。
  • 将序列化和写入操作丢给后台线程,主线程不等待,仅在存档完成后执行回调(如显示存档完成提示)。

示例代码(异步增量存档):

public class ArchiveManager : MonoBehaviour
{
    private List<Citizen> _dirtyCitizens = new List<Citizen>();
    private List<Building> _dirtyBuildings = new List<Building>();
    private readonly string _savePath = Application.persistentDataPath + "/incremental_save.dat";

    public void RegisterDirtyCitizen(Citizen citizen)
    {
        if (!_dirtyCitizens.Contains(citizen))
            _dirtyCitizens.Add(citizen);
    }

    public async Task ExecuteIncrementalSaveAsync()
    {
        // 主线程拷贝脏数据为DTO
        var citizenSaveData = _dirtyCitizens.Select(c => new CitizenSaveDTO
        {
            CitizenID = c.CitizenID,
            CurrentState = c.CurrentState,
            TargetBuildingID = c.TargetBuildingID,
            Profession = c.Profession
        }).ToList();
        var buildingSaveData = _dirtyBuildings.Select(b => new BuildingSaveDTO
        {
            BuildingID = b.BuildingID,
            CurrentOccupancy = b.CurrentOccupancy
        }).ToList();

        // 后台线程执行序列化与写入
        await Task.Run(() =>
        {
            using (var stream = new FileStream(_savePath, FileMode.Create))
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(stream, new IncrementalSavePackage
                {
                    SaveTimestamp = DateTime.UtcNow,
                    Citizens = citizenSaveData,
                    Buildings = buildingSaveData
                });
            }
        });

        // 主线程清除脏标记
        _dirtyCitizens.ForEach(c => c.isDirty = false);
        _dirtyCitizens.Clear();
        _dirtyBuildings.ForEach(b => b.isDirty = false);
        _dirtyBuildings.Clear();
    }
}

// 线程安全的存档DTO类
[Serializable]
public class CitizenSaveDTO
{
    public int CitizenID;
    public CitizenState CurrentState;
    public int TargetBuildingID;
    public string Profession;
}

5. 人口数据合并压缩,减少存储体积

百万级人口中大量状态是重复的,可通过分组合并减少数据量:

  • 按「建筑ID+职业」分组,记录每组的人数,而非单个人物数据(比如某餐厅内有50个厨师,只需存储餐厅ID、厨师职业、数量50)。
  • 行走状态的人物按「路径段/区域」分组,记录区域内的人口密度、职业分布,无需存储每个人物的精确位置(拥堵逻辑基于区域密度的话,精确位置无存档必要)。
  • 对序列化后的二进制数据使用GZipStream压缩,进一步减少文件体积和写入时间。

6. 调整存档时机,避开高负载时段

不要在游戏帧内强制执行存档,选择低负载时机触发:

  • 玩家暂停游戏时、场景加载过渡时触发存档。
  • 检测当前帧耗时低于阈值(如16ms)时,再执行存档逻辑。
  • 限制存档最小间隔(如最短10秒),避免频繁触发导致卡顿。

内容的提问来源于stack exchange,提问作者Dang Hoang Ha

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 09:35:35