跳转至

存档与恢复

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 AssetModule Definition
Actor 建筑没有恢复 检查对应的 Actor Class 是否有效。
旧存档无法加载 检查存档版本和建筑资产标识是否发生变化。
加载后建筑位置错误 检查保存记录中的世界变换和项目世界原点规则。
打包后无法恢复建筑 检查建筑数据资产是否被包含在打包内容中。