存档与恢复¶
Modular Building System 不强制项目使用特定的存档框架。
插件通过 UBuildingWorldSubsystem 提供建筑数据收集和世界重建接口,项目负责将这些数据写入自己的 USaveGame、数据库或服务器存档系统。
存档流程主要使用以下两个 API:
| API | 说明 |
|---|---|
CollectSaveRecords() |
收集当前世界中需要保存的建筑记录。 |
RebuildAll() |
根据存档记录重新建立建筑世界。 |
基础流程¶
保存建筑世界:
获取 UBuildingWorldSubsystem
→ 调用 CollectSaveRecords
→ 将记录写入项目存档
→ 保存到本地或服务器
恢复建筑世界:
进入目标世界
→ 等待 Building World Subsystem 可用
→ 读取项目存档
→ 获取保存的建筑记录
→ 调用 RebuildAll
→ 插件重新建立建筑表现和运行时数据
UBuildingWorldSubsystem¶
UBuildingWorldSubsystem 是建筑世界数据和存档 API 的主要访问入口。
可以通过当前 UWorld 获取:
UBuildingWorldSubsystem* BuildingWorldSubsystem =
World->GetSubsystem<UBuildingWorldSubsystem>();
使用前应检查返回值是否有效:
if (!IsValid(BuildingWorldSubsystem))
{
return;
}
CollectSaveRecords¶
函数声明:
TArray<FUpdateBuildingEntityInfo>
UBuildingWorldSubsystem::CollectSaveRecords() const;
CollectSaveRecords() 用于收集当前建筑世界中需要持久化的数据。
示例:
const TArray<FUpdateBuildingEntityInfo> BuildingRecords =
BuildingWorldSubsystem->CollectSaveRecords();
项目应直接保存返回的 FUpdateBuildingEntityInfo 数组,不需要自行访问 HISM Component、Chunk Actor 或建筑 Actor。
Note
FUpdateBuildingEntityInfo 是插件提供的建筑记录结构。
具体保存内容以当前插件版本中的结构定义为准。项目通常不需要拆分或重新组织其中的数据。
RebuildAll¶
函数声明:
void UBuildingWorldSubsystem::RebuildAll(
const TArray<FUpdateBuildingEntityInfo>& SavedRecords
);
RebuildAll() 用于根据保存的建筑记录重新建立建筑世界。
示例:
BuildingWorldSubsystem->RebuildAll(
SaveGame->BuildingRecords
);
重建过程会根据存档记录恢复建筑系统所需的运行时表现和映射。
项目不需要自行创建:
- HISM Component
- HISM Instance
- Render Chunk Actor
- Actor 模式建筑
- Entity 与渲染对象之间的运行时映射
这些运行时内容应由插件重新建立。
创建 SaveGame 类¶
可以创建一个继承自 USaveGame 的类,用于保存插件返回的建筑记录。
下面是一个最小示例。
BuildingWorldSaveGame.h¶
// Copyright 2026 Zhiying Li. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "Types/UpdateBuildingEntityInfo.h"
#include "BuildingWorldSaveGame.generated.h"
/**
* Stores building records collected from Modular Building System.
*/
UCLASS()
class UBuildingWorldSaveGame : public USaveGame
{
GENERATED_BODY()
public:
/** Records used to rebuild the building world. */
UPROPERTY(SaveGame)
TArray<FUpdateBuildingEntityInfo> BuildingRecords;
};
Note
Types/UpdateBuildingEntityInfo.h 仅作为示例。
请根据插件中 FUpdateBuildingEntityInfo 的实际头文件路径调整 #include。
保存建筑世界¶
下面的示例创建一个 UBuildingWorldSaveGame,收集当前建筑记录,并将其保存到指定 Slot。
// Copyright 2026 Zhiying Li. All Rights Reserved.
#include "BuildingWorldSaveGame.h"
#include "Kismet/GameplayStatics.h"
#include "System/BuildingWorldSubsystem.h"
bool SaveBuildingWorld(
UObject* WorldContextObject,
const FString& SlotName,
int32 UserIndex
)
{
if (!IsValid(WorldContextObject))
{
return false;
}
UWorld* World = WorldContextObject->GetWorld();
if (!IsValid(World))
{
return false;
}
UBuildingWorldSubsystem* BuildingWorldSubsystem =
World->GetSubsystem<UBuildingWorldSubsystem>();
if (!IsValid(BuildingWorldSubsystem))
{
return false;
}
UBuildingWorldSaveGame* SaveGame =
Cast<UBuildingWorldSaveGame>(
UGameplayStatics::CreateSaveGameObject(
UBuildingWorldSaveGame::StaticClass()
)
);
if (!IsValid(SaveGame))
{
return false;
}
SaveGame->BuildingRecords =
BuildingWorldSubsystem->CollectSaveRecords();
return UGameplayStatics::SaveGameToSlot(
SaveGame,
SlotName,
UserIndex
);
}
调用示例:
const bool bSaved = SaveBuildingWorld(
this,
TEXT("BuildingWorld"),
0
);
恢复建筑世界¶
下面的示例从 Slot 中读取建筑记录,并调用 RebuildAll()。
// Copyright 2026 Zhiying Li. All Rights Reserved.
#include "BuildingWorldSaveGame.h"
#include "Kismet/GameplayStatics.h"
#include "System/BuildingWorldSubsystem.h"
bool LoadBuildingWorld(
UObject* WorldContextObject,
const FString& SlotName,
int32 UserIndex
)
{
if (!IsValid(WorldContextObject))
{
return false;
}
UWorld* World = WorldContextObject->GetWorld();
if (!IsValid(World))
{
return false;
}
UBuildingWorldSubsystem* BuildingWorldSubsystem =
World->GetSubsystem<UBuildingWorldSubsystem>();
if (!IsValid(BuildingWorldSubsystem))
{
return false;
}
USaveGame* LoadedObject =
UGameplayStatics::LoadGameFromSlot(
SlotName,
UserIndex
);
UBuildingWorldSaveGame* SaveGame =
Cast<UBuildingWorldSaveGame>(LoadedObject);
if (!IsValid(SaveGame))
{
return false;
}
BuildingWorldSubsystem->RebuildAll(
SaveGame->BuildingRecords
);
return true;
}
调用示例: const bool bLoaded = LoadBuildingWorld( this, TEXT("BuildingWorld"), 0 );
调用时机¶
不要在目标世界尚未初始化时立即调用 RebuildAll()。
恢复建筑前应确保:
- 已经进入需要恢复建筑的目标世界
UBuildingWorldSubsystem已经创建- 插件运行时系统已经完成初始化
- 存档中引用的
Building Data Asset可以被 Asset Manager 找到 - 不会有其他逻辑同时创建同一批建筑
推荐流程: 打开目标关卡 → 等待世界初始化完成 → 读取存档 → 获取 UBuildingWorldSubsystem → 调用 RebuildAll
如果项目使用异步加载,还应等待相关建筑数据资产完成加载后再执行重建。
Building Data Asset¶
建筑存档通常通过资产标识引用对应的 Building Data Asset。
恢复存档时,存档中使用的建筑资产必须仍然存在,并且能够被 Unreal Engine 的 Asset Manager 找到。
修改建筑资产时应注意:
- 不要随意删除已经写入存档的建筑资产
- 不要在没有迁移方案的情况下修改 Primary Asset ID
- 资产重命名后应检查旧存档是否仍能解析
- 打包时应确保建筑数据资产被包含在构建中
建筑资产配置请参阅 建筑数据资产。
不应由项目保存的数据¶
项目不应自行保存插件生成的临时运行时对象。
不要直接保存:
UHierarchicalInstancedStaticMeshComponent*- HISM Instance Index
ARenderChunkActor*ABuildingEntityActor*- Actor 指针
- Component 指针
- Chunk Actor 引用
- Entity 到 Instance 的运行时映射
- 客户端本地缓存
- 当前世界中的 UObject 引用
这些数据只在当前运行时世界中有效,应由 RebuildAll() 重新生成。
Warning
HISM Instance Index 可能在删除、重建或重新排序后发生变化。 不要将 Instance Index 作为永久建筑标识写入项目存档。
Chunk 数据¶
Chunk 用于组织建筑渲染和网络数据,但项目通常不需要直接保存 Chunk Actor 或 Chunk 内部映射。
恢复建筑时,应由插件根据保存记录重新建立对应的 Chunk 和渲染数据。
项目代码不应依赖:
- 某个固定的 Render Chunk Actor
- 某个 HISM Component 地址
- 某个永久不变的 Instance Index
如果项目修改了 Chunk Size,应测试旧存档能否正常重建。
Chunk 系统的详细说明请参阅 Chunk 系统。
多人游戏¶
多人游戏中,建筑存档和恢复应由服务器负责。
推荐流程: 服务器读取建筑存档 → 服务器获取 UBuildingWorldSubsystem → 服务器调用 RebuildAll → 插件同步建筑实体 → 客户端建立对应的 HISM 或 Actor 表现
客户端不应各自读取相同的建筑世界存档并调用 RebuildAll()。
否则可能造成:
- 重复创建建筑
- Entity ID 冲突
- 客户端与服务器数据不一致
- 重复生成 Actor
- HISM Instance 数量异常
保存建筑世界时,也建议由服务器调用:
CollectSaveRecords()
服务器可以将返回的数据保存到:
USaveGame- Dedicated Server 存档
- 数据库
- 云存档
- 项目自己的世界存档结构
空存档处理¶
首次进入世界时,可能不存在建筑存档。
加载前可以先检查:
const bool bHasSave =
UGameplayStatics::DoesSaveGameExist(
TEXT("BuildingWorld"),
0
);
如果存档不存在,可以:
- 保持世界为空
- 加载关卡预设建筑
- 创建新的建筑存档
- 跳过
RebuildAll()
空数组是否需要传入 RebuildAll(),应根据项目希望保留还是清空当前建筑世界来决定。
重复调用¶
不要在没有明确需求的情况下,使用相同数据连续调用多次 RebuildAll()。
例如,不要同时在以下位置重复恢复:
- Game Mode
- Game State
- Level Blueprint
- Game Instance
- Player Controller
项目应指定一个统一的世界恢复入口。
推荐由以下对象之一负责:
- Game Mode
- 专用世界存档管理器
- Game Instance Subsystem
- 项目自己的服务器存档系统
存档版本¶
插件或项目升级后,建筑记录结构可能发生变化。
建议在项目存档中增加版本号: UPROPERTY(SaveGame) int32 SaveVersion = 1;
加载时可以根据版本执行迁移: if (SaveGame->SaveVersion < CurrentSaveVersion) { // Upgrade older building save records here. }
需要特别关注以下变化:
- 建筑资产被删除或重命名
- Entity 数据结构发生变化
- Gameplay Tags 被替换
- 建筑属性名称发生变化
- Chunk Size 发生变化
- 成本或特效资产发生变化
- Actor Class 被替换
常见问题¶
| 问题 | 建议检查 |
|---|---|
CollectSaveRecords() 返回空数组 |
检查是否在正确的世界和服务器端调用。 |
无法获取 UBuildingWorldSubsystem |
检查 World 是否有效,以及调用时机是否过早。 |
| 加载后没有生成建筑 | 检查存档记录、建筑数据资产和 Asset Manager 配置。 |
| 加载后建筑重复 | 检查是否在多个位置重复调用了 RebuildAll()。 |
| HISM 建筑没有恢复 | 检查对应 Building Data Asset 和 Module Definition。 |
| Actor 建筑没有恢复 | 检查对应的 Actor Class 是否有效。 |
| 旧存档无法加载 | 检查存档版本和建筑资产标识是否发生变化。 |
| 加载后建筑位置错误 | 检查保存记录中的世界变换和项目世界原点规则。 |
| 打包后无法恢复建筑 | 检查建筑数据资产是否被包含在打包内容中。 |