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
相关产品推荐
相关产品推荐

