UE5源码分析——AddContentDialog模块全面分析

AddContentDialog 模块全面系统分析


目录

  1. 模块概述
  2. 模块整体架构解析
  3. 类级代码注释详解
  4. 功能使用示例编写
  5. 总结与最佳实践

1 模块概述

1.1 基本信息

属性
模块名称AddContentDialog
类型Editor(编辑器模块)
描述提供"向项目添加内容"对话框,用于浏览和安装 Feature Pack(功能包)内容到当前项目中

打开 UE5 编辑器,在内容浏览器或者项目菜单中点击"Add Content to the Project",弹出来的那个对话框就是由这个模块实现的。它本质上是一个内容分发系统的前端,负责扫描、展示、搜索、安装 Engine 和 Enterprise 目录下的 Feature Pack 资源包。

在这里插入图片描述

整个模块包含约 16 个源文件,涉及一个核心 Slate 窗口、一个 ViewModel 层、一个内容源提供者系统、以及负责处理 .upack 和 manifest.json 格式的 Feature Pack 解析器。代码量不算大,但层次结构清晰,非常适合作为学习 UE5 编辑器 UI 架构和插件化内容分发设计的案例。


2 模块整体架构解析

2.1 架构图

AddContentDialog 模块采用了分层架构设计,从底层的磁盘文件扫描,到中间层的数据模型抽象,再到上层的 Slate UI 展示,每一层都有清晰的职责边界。

┌─────────────────────────────────────────────────────────────────────┐
│                        Slate UI 层 (表现层)                          │
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐ │
│  │ SAddContentDialog│  │ SAddContentWidget│  │SGenericThumbnail  │ │
│  │ (SWindow 子类)   │  │ (SCompoundWidget) │  │Tile (内容卡片)   │ │
│  └────────┬─────────┘  └────────┬─────────┘  └──────────────────┘ │
└───────────┼──────────────────────┼──────────────────────────────────┘
            │                      │
            │                      ▼
┌───────────┴──────────────────────────────────────────────────────────┐
│                       ViewModel 层 (数据绑定层)                        │
│  ┌─────────────────────────────────────────────────────────────────┐ │
│  │  FAddContentWidgetViewModel (核心 ViewModel)                     │ │
│  │  - 管理分类列表 (Categories)                                      │ │
│  │  - 管理内容源列表 (ContentSourceViewModels)                       │ │
│  │  - 文本搜索过滤 (TTextFilter)                                    │ │
│  │  - 分类×内容源的选中状态维护                                      │ │
│  └─────────────────────────────────────────────────────────────────┘ │
│  ┌────────────────────────┐  ┌────────────────────────────────────┐ │
│  │  FCategoryViewModel    │  │  FContentSourceViewModel           │ │
│  │  - 分类枚举→显示名称   │  │  - 名称/描述/图标/截图/分类        │ │
│  │  - 排序ID              │  │  - 多语言文本选择                   │ │
│  │                        │  │  - PNG→SlateBrush 转换              │ │
│  └────────────────────────┘  └────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────────┐
│                    内容源抽象层 (接口层)                                 │
│  ┌─────────────────────────────────────────────────────────────────┐ │
│  │  IContentSource (接口)                                            │ │
│  │  - 名称/描述/分类/资产类型/类类型                                  │ │
│  │  - 图标/截图数据                                                  │ │
│  │  - 排序键/标识                                                    │ │
│  │  - InstallToProject() 安装到项目                                  │ │
│  └─────────────────────────────────────────────────────────────────┘ │
│  ┌─────────────────────────────────────────────────────────────────┐ │
│  │  IContentSourceProvider (接口)                                    │ │
│  │  - GetContentSources() 获取内容源列表                              │ │
│  │  - SetContentSourcesChanged() 注册变更通知委托                    │ │
│  └─────────────────────────────────────────────────────────────────┘ │
│  ┌─────────────────────────────────────────────────────────────────┐ │
│  │  FContentSourceProviderManager (管理器)                           │ │
│  │  - RegisterContentSourceProvider() 注册提供者                     │ │
│  │  - GetContentSourceProviders() 获取全部提供者                      │ │
│  └─────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────────┐
│                     实际实现层                                           │
│  ┌─────────────────────────────────────────────────────────────────┐ │
│  │  FFeaturePackContentSourceProvider (特征包内容源提供者)          │ │
│  │  - 扫描 FeaturePackDir + EnterpriseFeaturePackDir 目录            │ │
│  │  - 注册 DirectoryWatcher 实现热更新                               │ │
│  │  - 按 SortKey 排序内容源                                          │ │
│  └─────────────────────────────────────────────────────────────────┘ │
│  ┌─────────────────────────────────────────────────────────────────┐ │
│  │  FFeaturePackContentSource (特征包内容源)                         │ │
│  │  - 解析 .upack 文件 (Pak 挂载 + manifest.json 解析)               │ │
│  │  - 解析松散 manifest.json 文件 (Templates/FeaturePack/ 目录)     │ │
│  │  - 安装到项目 (ImportAssets + 保存 + ContentBrowser 导航)         │ │
│  │  - 附加文件复制 / 附加资源包插入 / 启动时导入                      │ │
│  └─────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘

2.2 模块间依赖关系

如果上述的架构图看起来还有点抽象,可以参考下面的依赖关系进行理解。

SAddContentDialog (顶层窗口)
    ↓ 持有
SAddContentWidget (主 Widget)
    ↓ 绑定
FAddContentWidgetViewModel (数据核心)
    ↓ 使用
┌───────────────────────┬──────────────────────────┐
│ FCategoryViewModel    │ FContentSourceViewModel  │
└───────────────────────┴──────────────────────────┘
                              │
                    FContentSourceProviderManager
                              │
              ┌───────────────┴────────────────┐
              │ FFeaturePackContentSourceProvider│
              └───────────────┬────────────────┘
                              │
              ┌───────────────┴────────────────┐
              │ FFeaturePackContentSource      │
              └───────────────┬────────────────┘
                              │
              ┌───────────────┼────────────────┐
              │ Json │ PakFile │ ImageWrapper │ AssetTools │
              └──────┴─────────┴──────────────┴────────────┘

可能大家看到这里会跟我一样感到好奇,为什么编辑器的一个添加内容对话框,要分这么多层,搞这么多接口和抽象。

其实,UE5 引擎开发者的目的是为了支持内容源的扩展性。虽然目前引擎只内置了 Feature Pack 这一种内容源(通过 .upack 文件和 manifest.json 提供),但 IContentSourceIContentSourceProvider 的接口设计允许第三方(比如插件市场、企业版)通过 FContentSourceProviderManager::RegisterContentSourceProvider() 注册自己的内容源提供者。这样一来,未来如果要支持从 Marketplace 直接下载内容、从局域网共享服务器获取内容,只需实现这两个接口即可,整个 UI 层和 ViewModel 层完全不需要修改。

同时,ViewModel 层的引入将 UI 状态与 Slate 控件解耦。FAddContentWidgetViewModel 负责管理分类选择、搜索过滤、内容源选中状态这些纯数据逻辑,而 SAddContentWidget 只负责渲染和响应用户交互。这种设计让代码更易于测试和维护。

2.3 模块划分

对于每个模块的功能总结如下:

1. 模块入口层

职责:

  • 实现 IAddContentDialogModule 接口
  • 管理模块生命周期(启动/关闭)
  • 注册内容源提供者
  • 提供对话框展示入口

包含类:

  • FAddContentDialogModule

2. Slate UI 层

职责:

  • 提供"添加内容到项目"对话框窗口
  • 展示分类标签页、内容卡片网格、详情面板
  • 处理用户交互(搜索、选择、安装)

包含类:

  • SAddContentDialog(顶层窗口)
  • SAddContentWidget(主布局 Widget)
  • SGenericThumbnailTile(内容卡片内部类)

3. ViewModel 数据层

职责:

  • 管理分类列表和内容源列表
  • 提供文本搜索过滤功能
  • 维护每个分类的选中状态
  • 将原始数据(IContentSource)转换为 UI 友好的格式(SlateBrush、缓存文本)

包含类:

  • FAddContentWidgetViewModel
  • FCategoryViewModel
  • FContentSourceViewModel

4. 内容源抽象层

职责:

  • 定义内容源和内容源提供者的通用接口
  • 管理内容源提供者的注册和获取
  • 提供拖拽操作支持

包含类:

  • IContentSource(接口)
  • IContentSourceProvider(接口)
  • FContentSourceProviderManager(管理器)
  • FContentSourceDragDropOp(拖拽操作)

5. Feature Pack 实现层

职责:

  • 扫描磁盘上的 .upack 文件和 manifest.json 文件
  • 解析 Feature Pack 的 JSON 清单
  • 挂载 Pak 文件并读取内容
  • 安装内容到项目中(导入资产、复制文件、聚焦资源)

包含类:

  • FFeaturePackContentSourceProvider
  • FFeaturePackContentSource
  • FPackData(辅助结构体)
  • FFeaturePackLevelSet(辅助结构体)
  • FFeatureAdditionalFiles(辅助结构体)

2.4 数据流走向

对话框打开流程:

1. 外部调用 IAddContentDialogModule::ShowDialog(ParentWindow)
   ↓
2. FAddContentDialogModule::ShowDialog() 创建 SAddContentDialog
   ↓
3. SAddContentDialog::Construct() 创建 SAddContentWidget 作为子 Widget
   ↓
4. SAddContentWidget::Construct() 创建 FAddContentWidgetViewModel
   ↓
5. FAddContentWidgetViewModel::Initialize()
   ├─ 创建 TTextFilter 用于文本搜索
   ├─ 从 FContentSourceProviderManager 获取所有 IContentSourceProvider
   ├─ 为每个 Provider 绑定 ContentSourcesChanged 委托
   └─ 调用 BuildContentSourceViewModels()
   ↓
6. BuildContentSourceViewModels()
   ├─ 遍历所有 Provider 的 ContentSources
   ├─ 过滤掉 SharedPack 和 Unknown 分类
   ├─ 为每个内容源创建 FContentSourceViewModel
   ├─ 收集所有分类并创建 FCategoryViewModel
   └─ 按分类排序,初始化每个分类的选中状态
   ↓
7. UI 绑定完成,对话框显示

用户选择内容并安装流程:

1. 用户点击分类标签页 → OnSelectedCategoryChanged()
   ↓
2. ViewModel->SetSelectedCategory() → UpdateFilteredContentSourcesAndSelection()
   ↓
3. 过滤后的内容源列表更新 → STileView 刷新
   ↓
4. 用户搜索 → SearchTextChanged() → ViewModel->SetSearchText()
   ↓
5. TTextFilter 过滤 → UpdateFilteredContentSourcesAndSelection()
   ↓
6. 用户点击内容卡片 → ContentSourceTileViewSelectionChanged()
   ↓
7. ViewModel->SetSelectedContentSource()
   ↓
8. 右侧详情面板更新(截图轮播、名称、描述、资产类型、类类型)
   ↓
9. 用户点击 "Add to Project" → AddButtonClicked()
   ↓
10. 调用 FFeaturePackContentSource::InstallToProject("/Game")
    ├─ 插入附加资源包(InsertAdditionalResources)
    ├─ 复制附加文件(CopyAdditionalFilesToFolder)
    ├─ 通过 AssetTools 导入 .upack 资产
    ├─ 保存导入的资产
    └─ 聚焦到指定资产(如果配置了 FocusAsset)

Feature Pack 目录变更热更新流程:

1. FDirectoryWatcher 检测到 FeaturePackDir 文件变更
   ↓
2. FFeaturePackContentSourceProvider::OnFeaturePackDirectoryChanged()
   ↓
3. RefreshFeaturePacks()
   ├─ 清空 ContentSources 数组
   ├─ 重新扫描 .upack 文件
   ├─ 重新扫描 Templates/FeaturePack/ 下的 manifest.json
   └─ 按 SortKey 排序
   ↓
4. OnContentSourcesChanged.ExecuteIfBound()
   ↓
5. FAddContentWidgetViewModel::ContentSourcesChanged()
   ↓
6. BuildContentSourceViewModels() 重建全部 ViewModel
   ↓
7. UI 自动刷新

2.5 核心技术栈

技术/类用途
SWindow对话框顶层窗口
SCompoundWidget复合 Widget 基类
STileView内容卡片网格视图
SSegmentedControl分类标签页控件
SSearchBox搜索框
SWidgetCarouselWithNavigation截图轮播控件
FSlateDynamicImageBrush动态创建 Slate 画刷(从 PNG 数据)
IImageWrapper解码 PNG 图像数据
FPakPlatformFile挂载和读取 .upack(Pak 格式)文件
FJsonSerializer解析 manifest.json
FDirectoryWatcher监控 Feature Pack 目录变更
TTextFilter文本搜索过滤
FAssetToolsModule导入资产到项目中
FContentBrowserModule同步 ContentBrowser 到指定资产

2.6 架构设计优势

  1. 高扩展性IContentSource/IContentSourceProvider 接口设计允许任意添加新的内容源类型,无需修改 UI 和 ViewModel 层
  2. MVVM 分离:ViewModel 负责数据逻辑,Slate Widget 负责渲染,职责清晰
  3. 热更新支持:通过 FDirectoryWatcher 监控目录,新增/删除 Feature Pack 后 UI 自动刷新,无需重启编辑器
  4. 多语言支持:整个内容源系统从底层 JSON 清单到顶层 ViewModel 都支持多语言本地化
  5. 双格式兼容:同时支持 .upack(打包格式)和松散 manifest.json 两种分发方式
  6. 细节完善:截图轮播、搜索过滤、分类记忆、安装后自动聚焦资产等细节都处理到位

潜在改进点

  1. 内容源类型单一:目前只有 Feature Pack 一种实现,接口的扩展能力尚未被充分利用
  2. 异步安装InstallToProject() 是同步操作,安装大型 Pack 时可能阻塞 UI 线程
  3. 进度反馈:安装过程缺少进度回调,用户无法得知安装进度
  4. 错误恢复:安装失败后缺少回滚机制,部分文件可能已复制但资产导入失败
  5. 缓存优化:每次打开对话框都会重新扫描目录并解析 JSON,可以增加缓存机制

3 类级代码注释详解

3.1 IAddContentDialogModule 接口

概述

IAddContentDialogModule 是 AddContentDialog 模块的对外接口,继承自 IModuleInterface。它定义了模块的两个核心能力:获取内容源提供者管理器、展示对话框。

核心方法:

GetContentSourceProviderManager()

功能分析:

  • 返回 FContentSourceProviderManager 的共享引用
  • 外部模块可以通过这个接口注册自己的内容源提供者
  • 这是模块扩展性的关键入口

ShowDialog(TSharedRef<SWindow> ParentWindow)

功能分析:

  • 创建并显示"添加内容到项目"对话框
  • 接收父窗口引用,作为模态子窗口显示
  • 如果对话框已存在,不会重复创建(通过 TWeakPtr<SWindow> 跟踪)

实现原理:

FAddContentDialogModule::ShowDialog() 中,使用了 FSlateApplication::Get().AddWindowAsNativeChild() 将对话框添加为原生子窗口,这比 AddModalWindow() 更轻量,且不会阻塞编辑器主循环。


3.2 FAddContentDialogModule 模块实现

功能分析(模块入口):

class FAddContentDialogModule : public IAddContentDialogModule

这是模块的核心实现类,通过 IMPLEMENT_MODULE 宏注册到引擎中。

StartupModule()

void StartupModule() override
{
    FModuleManager::LoadModuleChecked<FWidgetCarouselModule>("WidgetCarousel");
    FWidgetCarouselModuleStyle::Initialize();

    ContentSourceProviderManager = TSharedPtr<FContentSourceProviderManager>(
        new FContentSourceProviderManager());
    ContentSourceProviderManager->RegisterContentSourceProvider(
        MakeShareable(new FFeaturePackContentSourceProvider()));
}

功能分析:

  1. 加载 WidgetCarousel 模块:截图轮播功能依赖此模块,需要确保在模块启动时已加载
  2. 初始化样式FWidgetCarouselModuleStyle::Initialize() 注册轮播控件所需的 Slate 样式
  3. 创建管理器:创建 FContentSourceProviderManager 实例
  4. 注册提供者:将 FFeaturePackContentSourceProvider 注册为内容源提供者。这一步是扩展性的体现——如果有更多内容源类型,只需在这里注册额外的 Provider

ShutdownModule()

void ShutdownModule() override
{
    FWidgetCarouselModuleStyle::Shutdown();
}

模块关闭时清理样式资源。


3.3 FContentSourceProviderManager 类

概述

FContentSourceProviderManager 是一个简单的管理器类,负责维护已注册的 IContentSourceProvider 列表。

核心方法:

RegisterContentSourceProvider(TSharedRef<IContentSourceProvider> ContentSourceProvider)

功能分析:

  • 将内容源提供者添加到内部数组
  • 使用 TSharedRef 确保提供者的生命周期被正确管理

GetContentSourceProviders()

功能分析:

  • 返回内部数组的指针
  • 调用者可以遍历所有提供者以获取全部内容源

设计意图:

这个管理器的存在是为了支持插件化的内容源注册。虽然目前只有一个内置的 FFeaturePackContentSourceProvider,但如果未来有插件想要提供自己的内容源(比如从远程服务器下载),只需获取 IAddContentDialogModule 然后调用 GetContentSourceProviderManager()->RegisterContentSourceProvider(...) 即可。


3.4 IContentSource 接口

概述

IContentSource 是内容源的抽象接口,定义了内容源必须提供的信息和行为。这个接口是所有内容源实现的基石。

class IContentSource
{
public:
    virtual const TArray<FLocalizedText>& GetLocalizedNames() const = 0;
    virtual const TArray<FLocalizedText>& GetLocalizedDescriptions() const = 0;
    virtual const TArray<EContentSourceCategory>& GetCategories() const = 0;
    virtual TSharedPtr<FImageData> GetIconData() const = 0;
    virtual const TArray<TSharedPtr<FImageData>>& GetScreenshotData() const = 0;
    virtual const TArray<FLocalizedText>& GetLocalizedAssetTypes() const = 0;
    virtual const FString& GetClassTypesUsed() const = 0;
    virtual const FString& GetSortKey() const = 0;
    virtual bool InstallToProject(FString InstallPath) = 0;
    virtual bool IsDataValid() const = 0;
    virtual const FString& GetIdent() const = 0;
};

关键接口分析:

多语言文本 (LocalizedNames / LocalizedDescriptions / LocalizedAssetTypes)

这三个方法返回的都是 TArray<FLocalizedText>,而不是单个 FText。这是因为 Feature Pack 的 manifest.json 可以为同一条信息提供多种语言版本。FLocalizedText 包含两个字段:TwoLetterLanguage(ISO 639-1 语言代码)和 Text(该语言的文本)。UI 层会根据当前编辑器语言选择合适的版本。

分类 (GetCategories)

一个内容源可以属于多个分类,返回的是 TArray<EContentSourceCategory>EContentSourceCategory 枚举定义如下:

枚举值含义
BlueprintFeature蓝图功能模板
CodeFeatureC++ 代码功能模板
EnterpriseFeature企业版(Unreal Studio)功能
Content内容包
EnterpriseContent企业版内容包
SharedPack共享资源包(UI 中隐藏)
Unknown未知类型(UI 中隐藏)

安装到项目 (InstallToProject)

这是内容源最核心的行为——将内容安装到项目中。返回 bool 表示安装是否成功。

数据有效性 (IsDataValid)

允许内容源在解析失败后标记自己为无效,UI 层会据此过滤掉无效的内容源。

标识 (GetIdent)

返回内容源的唯一标识字符串,用于在 InsertAdditionalResources 中查找已注册的内容源。


3.5 IContentSourceProvider 接口

概述

IContentSourceProvider 是内容源提供者的抽象接口。它负责发现和提供内容源列表,并在内容源变更时通知监听者。

class IContentSourceProvider
{
public:
    DECLARE_DELEGATE(FOnContentSourcesChanged);

    virtual const TArray<TSharedRef<IContentSource>>& GetContentSources() const = 0;
    virtual void SetContentSourcesChanged(FOnContentSourcesChanged OnContentSourcesChangedIn) = 0;
};

FOnContentSourcesChanged 委托机制是 ViewModel 层能够自动刷新 UI 的关键。当 FFeaturePackContentSourceProvider 检测到磁盘上的 Feature Pack 文件发生变化时,会触发这个委托,ViewModel 层收到通知后重建所有内容源的 ViewModel 并更新 UI。


3.6 FFeaturePackContentSourceProvider 类

概述

FFeaturePackContentSourceProviderIContentSourceProvider 的唯一内置实现,负责扫描磁盘上的 Feature Pack 文件并提供相应的 FFeaturePackContentSource 对象。

核心实现分析:

构造函数

FFeaturePackContentSourceProvider::FFeaturePackContentSourceProvider()
{
    FeaturePackPath = FPaths::FeaturePackDir();
    EnterpriseFeaturePackPath = FPaths::EnterpriseFeaturePackDir();
    TemplatePath = FPaths::RootDir() + TEXT("Templates/");
    EnterpriseTemplatePath = FPaths::EnterpriseDir() + TEXT("Templates/");
    StartUpDirectoryWatcher();
    RefreshFeaturePacks();
}

功能分析:

  1. 设置四个扫描路径:Engine 的 FeaturePacks 目录、Enterprise 的 FeaturePacks 目录、Templates 目录、Enterprise Templates 目录
  2. 启动目录监控器(DirectoryWatcher),以便在文件变更时自动刷新
  3. 立即执行一次全量扫描(RefreshFeaturePacks()

RefreshFeaturePacks()

void FFeaturePackContentSourceProvider::RefreshFeaturePacks()
{
    ContentSources.Empty();
    // 1. 扫描 .upack 文件
    IPlatformFile &PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
    FFillArrayDirectoryVisitor DirectoryVisitor;
    PlatformFile.IterateDirectory(*FeaturePackPath, DirectoryVisitor);
    PlatformFile.IterateDirectory(*EnterpriseFeaturePackPath, DirectoryVisitor);
    for (auto FeaturePackFile : DirectoryVisitor.Files)
    {
        if (FeaturePackFile.EndsWith(TEXT(".upack")) == true)
        {
            TUniquePtr<FFeaturePackContentSource> NewContentSource = 
                MakeUnique<FFeaturePackContentSource>(FeaturePackFile);
            if (NewContentSource->IsDataValid())
            {
                ContentSources.Add(MakeShareable(NewContentSource.Release()));
            }
        }
    }
    // 2. 扫描松散 manifest.json 文件
    // ...
    // 3. 按 SortKey 排序
    ContentSources.Sort(FFeaturePackCompareSortKey());
    // 4. 通知 UI 刷新
    OnContentSourcesChanged.ExecuteIfBound();
}

功能分析:

整个扫描分为两个阶段:

  1. .upack 文件扫描:使用 IPlatformFile::IterateDirectory() 遍历 FeaturePacks 目录,找到所有 .upack 文件,为每个文件创建 FFeaturePackContentSource 实例
  2. 松散 manifest.json 扫描:递归遍历 Templates 目录,找到路径以 “FeaturePack” 结尾的目录下的 manifest.json 文件

两种格式的共存设计是为了兼容不同的分发场景——.upack 适合打包分发,而松散格式适合开发期快速迭代和模板系统中的内容。

目录监控

void StartUpDirectoryWatcher()
{
    FDirectoryWatcherModule& DirectoryWatcherModule = 
        FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher"));
    IDirectoryWatcher* DirectoryWatcher = DirectoryWatcherModule.Get();
    // 注册 FeaturePackPath 和 EnterpriseFeaturePackPath 的变更回调
    DirectoryWatcher->RegisterDirectoryChangedCallback_Handle(
        FeaturePackPath, DirectoryChangedDelegate, DirectoryChangedDelegateHandle);
    DirectoryWatcher->RegisterDirectoryChangedCallback_Handle(
        EnterpriseFeaturePackPath, DirectoryChangedDelegate, DirectoryChangedDelegateHandle);
}

目录监控使得用户可以在编辑器运行时,将新的 .upack 文件放入 FeaturePacks 目录,对话框会自动检测到并刷新内容列表,无需重启编辑器。


3.7 FFeaturePackContentSource 类

概述

FFeaturePackContentSourceIContentSource 接口的核心实现,也是整个模块中代码量最大的类(约 920 行)。它负责解析 Feature Pack 的 JSON 清单、挂载 Pak 文件、读取图像数据、以及安装内容到项目中。

构造函数(两种模式)

FFeaturePackContentSource::FFeaturePackContentSource(FString InFeaturePackPath)
{
    FeaturePackPath = InFeaturePackPath;
    if (InFeaturePackPath.EndsWith(TEXT(".upack")) == true)
    {
        // Pak 模式:挂载 .upack 文件,从 Pak 中读取 manifest.json 和图片
        bContentsInPakFile = true;
        MountPoint = FPaths::GameFeatureRootPrefix();
        // 创建/获取 PakPlatformFile,挂载文件
        // 读取 manifest.json → ParseManifestString()
        // 读取图片 → LoadFeaturePackImageDataFromPackFile()
        // 卸载 Pak
    }
    else
    {
        // 松散模式:直接读取 manifest.json 和图片文件
        bContentsInPakFile = false;
        // 设置 MountPoint 为 manifest 文件所在目录
        // 读取 manifest.json → ParseManifestString()
        // 读取图片 → LoadFeaturePackImageData()
    }
}

功能分析:

构造函数承担了所有解析工作,分为两种路径:

  • .upack 路径:将 .upack 文件作为 Pak 文件挂载到 FPaths::GameFeatureRootPrefix() 挂载点,然后通过 FPakPlatformFile 读取内部的 manifest.json 和图片资源。解析完成后立即卸载 Pak,释放文件句柄。
  • 松散路径:直接通过 FFileHelper::LoadFileToString() 读取 manifest.json,通过 FFileHelper::LoadFileToArray() 读取图片。

bContentsInPakFile 成员变量在后续的 InstallToProject() 中发挥了关键作用——它决定了安装时是使用 AssetTools 导入 .upack 文件,还是使用其他方式处理。

ParseManifestString()

这是整个类中最核心的解析函数,负责将 JSON 字符串解析为内容源的各项属性。

JSON 校验逻辑通过 TryValidateManifestObject() 辅助函数完成,它检查了以下必须字段:

  • Name(数组,每个元素包含 Language 和 Text)
  • Description(数组,同上)
  • AssetTypes(数组,同上)
  • ClassTypes(字符串)
  • Category(数组或字符串)
  • Thumbnail(字符串,缩略图文件名)
  • Screenshots(数组,截图文件名列表)

解析过程还包括可选字段:

  • Version:版本号
  • Ident:唯一标识
  • SortKey:排序键
  • FocusAsset:安装后聚焦的资产路径
  • SearchTags:搜索标签
  • AdditionalFiles:附加文件配置
  • AdditionalFeaturePacks:依赖的附加资源包

InstallToProject()

bool FFeaturePackContentSource::InstallToProject(FString InstallPath)
{
    // 1. 插入附加资源包(依赖)
    InsertAdditionalResources(AdditionalFeaturePacks, EFeaturePackDetailLevel::High, 
        FPaths::ProjectDir(), FilesCopied);
    // 2. 复制附加文件
    if (AdditionalFilesForPack.AdditionalFilesList.Num() != 0)
    {
        CopyAdditionalFilesToFolder(FPaths::ProjectDir(), FilesCopied, bHasSourceFiles);
    }
    // 3. 导入 .upack 资产
    if (bContentsInPakFile == true)
    {
        FAssetToolsModule& AssetToolsModule = FModuleManager::Get()
            .LoadModuleChecked<FAssetToolsModule>("AssetTools");
        TArray<FString> AssetPaths;
        AssetPaths.Add(FeaturePackPath);
        TArray<UObject*> ImportedObjects = AssetToolsModule.Get()
            .ImportAssets(AssetPaths, InstallPath);
        // 保存导入的资产
        FEditorFileUtils::PromptForCheckoutAndSave(ToSave, false, false);
    }
    // 4. 聚焦到指定资产
    if (GetFocusAssetName().IsEmpty() == false)
    {
        // 通过 ContentBrowserModule 导航到 FocusAsset
    }
    return bResult;
}

功能分析:

安装流程分为四个步骤,步骤之间有依赖关系:

  1. 附加资源包InsertAdditionalResources):先安装依赖的共享资源包。这个方法会遍历 AdditionalFeaturePacks 列表,尝试从已注册的 ContentSourceProvider 中找到匹配的包(通过 GetIdent() 匹配),如果找不到则去磁盘上查找对应的 .upack 文件。
  2. 附加文件CopyAdditionalFilesToFolder):复制 manifest.json 中 AdditionalFiles 配置的文件到项目目录。支持通配符匹配,会自动检测是否包含源码文件。
  3. 资产导入ImportAssets):通过 FAssetToolsModule::ImportAssets() 将 .upack 文件中的所有资产导入到项目中。导入完成后,调用 FAssetCompilingManager::Get().FinishAllCompilation() 等待异步编译完成,然后通过 FEditorFileUtils::PromptForCheckoutAndSave() 保存。
  4. 聚焦资产FocusAsset):如果清单中配置了 FocusAsset,安装完成后自动在 ContentBrowser 中定位并选中该资产,方便用户快速找到新添加的内容。

ImportPendingPacks() 静态方法

这是一个静态方法,用于在编辑器启动时"自动导入"之前通过命令行或其他方式标记的 Feature Pack。它从 GGameIni[StartupActions] 段读取 bAddPacksInsertPack 配置,执行导入后清除标记。这主要用于项目模板生成等自动化场景。


3.8 FAddContentWidgetViewModel 类

概述

FAddContentWidgetViewModel 是整个 UI 的数据核心,承担了"Model"和"ViewModel"的双重角色。它管理分类列表、内容源列表、搜索过滤、以及每个分类的选中状态。

核心成员变量:

TArray<TSharedPtr<IContentSourceProvider>> ContentSourceProviders;  // 内容源提供者
TArray<FCategoryViewModel> Categories;                               // 分类列表
TArray<TSharedPtr<FContentSourceViewModel>> ContentSourceViewModels; // 全部内容源
TArray<TSharedPtr<FContentSourceViewModel>> FilteredContentSourceViewModels; // 过滤后
TMap<FCategoryViewModel, TSharedPtr<FContentSourceViewModel>> CategoryToSelectedContentSourceMap; // 每个分类的选中状态
FCategoryViewModel SelectedCategory;                                 // 当前选中分类
TSharedPtr<ContentSourceTextFilter> ContentSourceFilter;            // 文本过滤器

关键设计:分类×内容源选中状态

CategoryToSelectedContentSourceMap 是一个 TMap<FCategoryViewModel, TSharedPtr<FContentSourceViewModel>>,它为每个分类独立维护一个选中状态。这意味着用户切换到不同分类时,之前在该分类下的选中内容会被保留。这是一个非常贴心的 UX 细节。

BuildContentSourceViewModels()

void FAddContentWidgetViewModel::BuildContentSourceViewModels()
{
    // 1. 清空所有数据
    Categories.Empty();
    ContentSourceViewModels.Empty();
    // ...
    // 2. 过滤掉 SharedPack 和 Unknown 分类
    TArray<EContentSourceCategory> FilteredCategories;
    FilteredCategories.Add(EContentSourceCategory::SharedPack);
    FilteredCategories.Add(EContentSourceCategory::Unknown);
    // 3. 遍历所有 Provider 和 ContentSource
    for (const TSharedPtr<IContentSourceProvider>& ContentSourceProvider : ContentSourceProviders)
    {
        for (const TSharedRef<IContentSource>& ContentSource : ContentSourceProvider->GetContentSources())
        {
            // 检查是否所有分类都被过滤
            bool bAnyVisible = false;
            for (EContentSourceCategory ContentCategory : ContentSource->GetCategories())
            {
                if (!FilteredCategories.Contains(ContentCategory))
                {
                    FoundCategories.Add(ContentCategory);
                    bAnyVisible = true;
                }
            }
            if (bAnyVisible)
            {
                ContentSourceViewModels.Add(MakeShared<FContentSourceViewModel>(ContentSource));
            }
        }
    }
    // 4. 创建 CategoryViewModel 并排序
    // 5. 反向遍历初始化每个分类的选中状态
    for (int i = Categories.Num() - 1; i >= 0; i--)
    {
        SelectedCategory = Categories[i];
        UpdateFilteredContentSourcesAndSelection(false);
    }
}

功能分析:

SharedPackUnknown 分类被硬编码过滤掉,因为它们是内部使用的分类,不应暴露给用户。

反向遍历初始化选中状态的逻辑值得注意:从最后一个分类开始向前遍历,这样第一个分类最终会成为选中状态。这种写法避免了记录"第一个分类"的额外逻辑。

UpdateFilteredContentSourcesAndSelection()

void FAddContentWidgetViewModel::UpdateFilteredContentSourcesAndSelection(bool bAllowEmptySelection)
{
    FilteredContentSourceViewModels.Empty();
    for (const TSharedPtr<FContentSourceViewModel>& ContentSource : ContentSourceViewModels)
    {
        if (ContentSource->GetCategories().Contains(SelectedCategory) && 
            ContentSourceFilter->PassesFilter(ContentSource))
        {
            FilteredContentSourceViewModels.Add(ContentSource);
        }
    }
    OnContentSourcesChanged.ExecuteIfBound();
    // 如果当前选中项不在过滤结果中,自动选择第一个
    if (FilteredContentSourceViewModels.Contains(GetSelectedContentSource()) == false)
    {
        TSharedPtr<FContentSourceViewModel> NewSelectedContentSource;
        if (bAllowEmptySelection == false && FilteredContentSourceViewModels.Num() > 0)
        {
            NewSelectedContentSource = FilteredContentSourceViewModels[0];
        }
        SetSelectedContentSource(NewSelectedContentSource);
    }
}

功能分析:

过滤逻辑通过两步完成:先按分类过滤(Contains(SelectedCategory)),再按搜索文本过滤(ContentSourceFilter->PassesFilter(ContentSource))。

bAllowEmptySelection 参数控制当过滤结果为空时,是否允许选中项也为空。在初始化阶段(BuildContentSourceViewModels),它被设为 false,确保每个分类至少有一个选中项。在用户交互阶段,它被设为 true,允许搜索无结果时清空选中。


3.9 FCategoryViewModel 类

概述

FCategoryViewModel 是一个轻量级的 ViewModel,将 EContentSourceCategory 枚举映射为 UI 友好的显示名称和排序 ID。

void FCategoryViewModel::Initialize()
{
    switch (Category)
    {
    case EContentSourceCategory::BlueprintFeature:
        Text = LOCTEXT("BlueprintFeature", "Blueprint");
        SortID = 0;
        break;
    case EContentSourceCategory::CodeFeature:
        Text = LOCTEXT("CodeFeature", "C++");
        SortID = 1;
        break;
    // ...
    }
}

排序 ID 决定了分类标签页在 UI 中的显示顺序:Blueprint → C++ → Unreal Studio Feature → Content → Unreal Studio Content → Miscellaneous。


3.10 FContentSourceViewModel 类

概述

FContentSourceViewModelIContentSource 的原始数据转换为 UI 可直接使用的格式。它的核心职责包括:

  1. 多语言文本选择:根据当前编辑器语言,从 IContentSource 的多语言文本数组中选择最合适的版本
  2. PNG 图像转换:将 IContentSource 提供的原始 PNG 数据 (TArray<uint8>) 转换为 FSlateDynamicImageBrush
  3. 文本缓存:使用 FCachedContentText 结构体缓存当前语言下的文本,避免每次 Get 时都执行语言匹配

ChooseLocalizedText()

FText FContentSourceViewModel::ChooseLocalizedText(
    const TArray<FLocalizedText>& Choices, const FString& InCurrentLanguage) const
{
    // 1. 尝试按优先级匹配本地化翻译
    const TArray<FString> PrioritizedCultureNames = 
        FInternationalization::Get().GetPrioritizedCultureNames(InCurrentLanguage);
    for (const FString& CultureName : PrioritizedCultureNames)
    {
        if (const FLocalizedText* Match = FindLocalizedTextForCulture(CultureName))
            return Match->GetText();
    }
    // 2. 回退到英语
    if (InCurrentLanguage != TEXT("en"))
    {
        if (const FLocalizedText* Match = FindLocalizedTextForCulture(TEXT("en")))
            return Match->GetText();
    }
    // 3. 最后回退到第一个可用翻译
    if (Choices.Num() > 0)
        return Choices[0].GetText();
    return FText();
}

功能分析:

这个多语言选择逻辑设计得相当完善,有三层回退机制:

  1. 优先使用当前语言的最佳匹配(通过 GetPrioritizedCultureNames() 获取优先级列表,例如 “zh-Hans” 会优先匹配 “zh-Hans”,然后尝试 “zh”)
  2. 如果当前语言没有匹配,回退到英语
  3. 如果连英语都没有,使用第一个可用的翻译

CreateBrushFromRawData()

TSharedPtr<FSlateDynamicImageBrush> FContentSourceViewModel::CreateBrushFromRawData(
    const FString& ResourceNamePrefix, const TArray<uint8>& RawData) const
{
    // 1. 使用 ImageWrapper 解码 PNG 数据
    IImageWrapperModule& ImageWrapperModule = 
        FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
    TSharedPtr<IImageWrapper> ImageWrapper = 
        ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);
    ImageWrapper->SetCompressed(RawData.GetData(), RawData.Num());
    // 2. 获取原始 BGRA 像素数据
    ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, DecodedImage);
    // 3. 创建 FSlateDynamicImageBrush
    FString UniqueResourceName = ResourceNamePrefix + "_" + FString::FromInt(ImageID++);
    Brush = FSlateDynamicImageBrush::CreateWithImageData(
        FName(*UniqueResourceName), 
        FVector2D(Width, Height), 
        DecodedImage);
    return Brush;
}

功能分析:

这里有一个值得注意的细节——ImageID 是一个静态自增计数器。每个 Brush 的名称都附加了唯一的 ID,这是为了避免两个不同内容源使用相同名称时,FSlateDynamicImageBrush 的纹理数据被意外释放的问题。因为 FSlateDynamicImageBrush 通过名称引用纹理资源,如果两个 Brush 同名,任意一个销毁时都会导致另一个的纹理数据被清空。


3.11 SAddContentDialog 类

概述

SAddContentDialog 继承自 SWindow,是整个对话框的顶层窗口。

void SAddContentDialog::Construct(const FArguments& InArgs)
{
    SWindow::Construct(SWindow::FArguments()
        .Title(LOCTEXT("AddContentDialogTitle", "Add Content to the Project"))
        .SizingRule(ESizingRule::UserSized)
        .ClientSize(FVector2D(900, 500))
        .SupportsMinimize(false)
        .SupportsMaximize(false)
        [
            SNew(SBorder)
            .BorderImage(FAppStyle::GetBrush("Brushes.Panel"))
            .Padding(FMargin(10,0))
            [
                SNew(SAddContentWidget)
            ]
        ]);
}

功能分析:

  • 窗口大小为 900×500,用户可调整大小
  • 禁用了最小化和最大化按钮
  • 内部直接嵌入 SAddContentWidget 作为唯一子 Widget
  • 使用 FAppStyle 的面板画刷作为背景

3.12 SAddContentWidget 类

概述

SAddContentWidget 是整个 UI 的核心,负责布局和管理所有子控件。它继承自 SCompoundWidget,内部绑定了 FAddContentWidgetViewModel

UI 布局结构:

SAddContentWidget
├── SVerticalBox
│   ├── SHorizontalBox (主内容区)
│   │   ├── SVerticalBox (左侧:内容选择区)
│   │   │   ├── SBox (分类标签页容器)
│   │   │   │   └── SSegmentedControl<FCategoryViewModel> (分类标签页)
│   │   │   ├── SSearchBox (搜索框)
│   │   │   └── STileView<FContentSourceViewModel> (内容卡片网格)
│   │   ├── SSeparator (垂直分隔线)
│   │   └── SBox (右侧:详情面板容器)
│   │       └── SScrollBox
│   │           ├── SWidgetCarouselWithNavigation (截图轮播)
│   │           ├── STextBlock (名称)
│   │           ├── STextBlock (描述)
│   │           ├── STextBlock (资产类型标签)
│   │           ├── STextBlock (资产类型列表)
│   │           ├── STextBlock (类类型标签)
│   │           └── STextBlock (类类型列表)
│   ├── SSeparator (水平分隔线)
│   └── SHorizontalBox (底部按钮区)
│       ├── SPrimaryButton ("Add to Project")
│       └── SButton ("Cancel")

委托绑定机制:

Construct() 中,SAddContentWidget 将自己的三个回调方法绑定到 ViewModel 的委托上:

ViewModel->SetOnCategoriesChanged(...CreateSP(this, &SAddContentWidget::CategoriesChanged));
ViewModel->SetOnContentSourcesChanged(...CreateSP(this, &SAddContentWidget::ContentSourcesChanged));
ViewModel->SetOnSelectedContentSourceChanged(...CreateSP(this, &SAddContentWidget::SelectedContentSourceChanged));

当 ViewModel 的数据发生变化时,这三个回调会自动更新 UI:

  • CategoriesChanged():重建分类标签页
  • ContentSourcesChanged():刷新 STileView 列表
  • SelectedContentSourceChanged():更新 STileView 选中状态 + 更新详情面板

SGenericThumbnailTile 内部类

这是一个定义在 SAddContentWidget.cpp 文件中的内部类,用于渲染内容卡片。它展示了一个带缩略图和名称的卡片,并支持选中和高亮状态:

class SGenericThumbnailTile : public SCompoundWidget
{
    // 布局:缩略图区域 + 名称区域
    // 交互:选中/悬停时显示不同边框
};

卡片使用 Lambda 动态绑定 IsSelected 状态,通过比较 ViewModel->GetSelectedContentSource() 与当前卡片的内容源来判断是否选中。这种写法避免了在 ViewModel 中维护额外的选中状态数组。


3.13 FContentSourceDragDropOp 类

概述

FContentSourceDragDropOp 继承自 FDecoratedDragDropOp,支持从 SAddContentDialog 中拖拽内容源到其他位置。虽然目前编辑器中没有明显的使用场景,但这个类为未来的拖拽导入功能(例如拖拽 Feature Pack 到 ContentBrowser 中直接安装)提供了基础。

TSharedPtr<SWidget> FContentSourceDragDropOp::GetDefaultDecorator() const
{
    return SNew(SImage)
        .Image(ContentSource->GetIconBrush().Get());
}

拖拽时的装饰器显示内容源的图标,光标变为 EMouseCursor::GrabHandClosed


4 功能使用示例编写

示例1:通过代码打开"添加内容"对话框

// 在编辑器的任意模块中
#include "IAddContentDialogModule.h"
#include "Modules/ModuleManager.h"

void OpenAddContentDialog()
{
    IAddContentDialogModule& AddContentDialog = 
        FModuleManager::LoadModuleChecked<IAddContentDialogModule>("AddContentDialog");
    
    TSharedRef<SWindow> ParentWindow = 
        FSlateApplication::Get().GetActiveTopLevelWindow().ToSharedRef();
    
    AddContentDialog.ShowDialog(ParentWindow);
}

示例2:注册自定义内容源提供者

如果想要添加自己的内容源(例如从远程服务器获取内容列表),可以通过实现 IContentSourceProviderIContentSource 接口来扩展:

// MyRemoteContentSource.h
#pragma once

#include "IContentSource.h"
#include "IContentSourceProvider.h"

// 自定义内容源
class FMyRemoteContentSource : public IContentSource
{
public:
    FMyRemoteContentSource(const FString& InName, const FString& InDescription)
    {
        LocalizedNames.Add(FLocalizedText(TEXT("en"), FText::FromString(InName)));
        LocalizedDescriptions.Add(FLocalizedText(TEXT("en"), FText::FromString(InDescription)));
        Categories = { EContentSourceCategory::Content };
    }

    virtual const TArray<FLocalizedText>& GetLocalizedNames() const override 
    { 
        return LocalizedNames; 
    }
    
    virtual const TArray<FLocalizedText>& GetLocalizedDescriptions() const override 
    { 
        return LocalizedDescriptions; 
    }
    
    virtual const TArray<EContentSourceCategory>& GetCategories() const override 
    { 
        return Categories; 
    }
    
    virtual TSharedPtr<FImageData> GetIconData() const override 
    { 
        return nullptr; 
    }
    
    virtual const TArray<TSharedPtr<FImageData>>& GetScreenshotData() const override 
    { 
        static TArray<TSharedPtr<FImageData>> Empty;
        return Empty; 
    }
    
    virtual const TArray<FLocalizedText>& GetLocalizedAssetTypes() const override 
    { 
        static TArray<FLocalizedText> Empty;
        return Empty; 
    }
    
    virtual const FString& GetClassTypesUsed() const override 
    { 
        static FString Empty;
        return Empty; 
    }
    
    virtual const FString& GetSortKey() const override 
    { 
        return LocalizedNames[0].GetText().ToString(); 
    }
    
    virtual const FString& GetIdent() const override 
    { 
        return LocalizedNames[0].GetText().ToString(); 
    }
    
    virtual bool InstallToProject(FString InstallPath) override
    {
        // 执行下载和安装逻辑
        UE_LOG(LogTemp, Log, TEXT("Installing remote content to %s"), *InstallPath);
        return true;
    }
    
    virtual bool IsDataValid() const override 
    { 
        return true; 
    }

private:
    TArray<FLocalizedText> LocalizedNames;
    TArray<FLocalizedText> LocalizedDescriptions;
    TArray<EContentSourceCategory> Categories;
};

// 自定义内容源提供者
class FMyRemoteContentSourceProvider : public IContentSourceProvider
{
public:
    virtual const TArray<TSharedRef<IContentSource>>& GetContentSources() const override
    {
        return ContentSources;
    }
    
    virtual void SetContentSourcesChanged(FOnContentSourcesChanged OnContentSourcesChangedIn) override
    {
        OnContentSourcesChanged = OnContentSourcesChangedIn;
    }
    
    void RefreshFromServer()
    {
        ContentSources.Empty();
        // 从服务器获取内容列表...
        ContentSources.Add(MakeShareable(
            new FMyRemoteContentSource(TEXT("Remote Pack 1"), TEXT("A remote content pack"))));
        OnContentSourcesChanged.ExecuteIfBound();
    }

private:
    TArray<TSharedRef<IContentSource>> ContentSources;
    FOnContentSourcesChanged OnContentSourcesChanged;
};

在模块启动时注册:

// 在某个编辑器模块的 StartupModule() 中
void FMyEditorModule::StartupModule()
{
    IAddContentDialogModule& AddContentDialog = 
        FModuleManager::LoadModuleChecked<IAddContentDialogModule>("AddContentDialog");
    
    TSharedRef<FMyRemoteContentSourceProvider> RemoteProvider = 
        MakeShareable(new FMyRemoteContentSourceProvider());
    RemoteProvider->RefreshFromServer();
    
    AddContentDialog.GetContentSourceProviderManager()
        ->RegisterContentSourceProvider(RemoteProvider);
}

示例3:手动创建并安装 Feature Pack

#include "FeaturePackContentSource.h"

void InstallFeaturePack(const FString& PackFilePath)
{
    TUniquePtr<FFeaturePackContentSource> ContentSource = 
        MakeUnique<FFeaturePackContentSource>(PackFilePath);
    
    if (ContentSource->IsDataValid())
    {
        // 查看 Pack 的基本信息
        for (const FLocalizedText& Name : ContentSource->GetLocalizedNames())
        {
            UE_LOG(LogTemp, Log, TEXT("Pack Name [%s]: %s"), 
                *Name.GetTwoLetterLanguage(), *Name.GetText().ToString());
        }
        
        // 安装到项目
        bool bSuccess = ContentSource->InstallToProject(TEXT("/Game/MyContent"));
        if (bSuccess)
        {
            UE_LOG(LogTemp, Log, TEXT("Feature Pack installed successfully!"));
        }
    }
    else
    {
        UE_LOG(LogTemp, Error, TEXT("Invalid Feature Pack: %s"), *PackFilePath);
        for (const FString& Error : ContentSource->ParseErrors)
        {
            UE_LOG(LogTemp, Error, TEXT("  Parse Error: %s"), *Error);
        }
    }
}

示例4:读取 Feature Pack 中的图片数据

#include "FeaturePackContentSource.h"
#include "IImageWrapper.h"
#include "IImageWrapperModule.h"

UTexture2D* ExtractIconFromFeaturePack(const FString& PackFilePath)
{
    TUniquePtr<FFeaturePackContentSource> ContentSource = 
        MakeUnique<FFeaturePackContentSource>(PackFilePath);
    
    if (!ContentSource->IsDataValid())
    {
        return nullptr;
    }
    
    TSharedPtr<FImageData> IconData = ContentSource->GetIconData();
    if (!IconData.IsValid())
    {
        return nullptr;
    }
    
    // 解码 PNG 数据
    IImageWrapperModule& ImageWrapperModule = 
        FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
    TSharedPtr<IImageWrapper> ImageWrapper = 
        ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);
    
    if (!ImageWrapper->SetCompressed(IconData->GetData()->GetData(), 
        IconData->GetData()->Num()))
    {
        return nullptr;
    }
    
    TArray<uint8> RawData;
    if (!ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, RawData))
    {
        return nullptr;
    }
    
    // 创建 UTexture2D
    UTexture2D* Texture = UTexture2D::CreateTransient(
        ImageWrapper->GetWidth(), ImageWrapper->GetHeight(), PF_B8G8R8A8);
    
    void* TextureData = Texture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE);
    FMemory::Memcpy(TextureData, RawData.GetData(), RawData.Num());
    Texture->GetPlatformData()->Mips[0].BulkData.Unlock();
    Texture->UpdateResource();
    
    return Texture;
}

5 总结与最佳实践

核心要点

  1. 接口驱动的内容源扩展

    • IContentSourceIContentSourceProvider 是模块扩展性的核心接口
    • 实现这两个接口即可添加新的内容源类型,无需修改 UI 和 ViewModel 代码
    • FContentSourceProviderManager 是注册入口
  2. MVVM 风格的 UI 架构

    • FAddContentWidgetViewModel 作为数据核心,管理所有 UI 状态
    • Slate Widget 通过委托绑定自动响应数据变化
    • 这种分离使得 UI 逻辑更容易测试和维护
  3. 双格式 Feature Pack 支持

    • .upack 格式:打包分发,通过 FPakPlatformFile 挂载读取
    • 松散 manifest.json 格式:开发期友好,直接读取文件系统
    • 两种格式共存的架构设计兼顾了不同使用场景
  4. 目录监控热更新

    • FDirectoryWatcher 监控 FeaturePack 目录变更
    • 新增/删除 .upack 文件后 UI 自动刷新
    • 无需重启编辑器即可看到新添加的内容
  5. 多语言本地化支持

    • 从 JSON 清单到 ViewModel 层,全链路支持多语言
    • 三层回退机制:当前语言 → 英语 → 第一个可用翻译
    • 使用 GetPrioritizedCultureNames() 实现智能语言匹配

最佳实践

  1. 扩展内容源时遵循接口设计

    • 为新的内容源类型实现 IContentSourceIContentSourceProvider
    • 在模块启动时通过 FContentSourceProviderManager::RegisterContentSourceProvider() 注册
    • 内容源变更时调用 OnContentSourcesChanged.ExecuteIfBound() 通知 UI 刷新
  2. Feature Pack 清单编写规范

    • manifest.json 必须包含所有必需字段:Name、Description、AssetTypes、ClassTypes、Category、Thumbnail、Screenshots
    • SortKey 用于控制内容源在列表中的排序,建议使用有意义的键值
    • FocusAsset 是可选的,但强烈建议设置,以便用户安装后快速找到新内容
    • AdditionalFeaturePacks 用于声明依赖的共享资源包
  3. 安装路径的选择

    • 默认安装路径为 /Game,但可以根据需要指定子路径
    • 如果 Pack 包含共享资源,确保 AdditionalFeaturePacks 中的 MountName 与资源包的实际路径一致
  4. 错误处理

    • FFeaturePackContentSourceParseErrors 数组记录了所有解析错误
    • 在安装前检查 IsDataValid() 的返回值
    • 如果安装失败,检查 ParseErrors 获取详细错误信息
  5. 调试技巧

    • 使用 LogFeaturePack 日志分类查看 Feature Pack 的解析和安装日志
    • DefaultGame.ini[StartupActions] 段中可以使用 InsertPack 进行自动导入测试
    • .upack 文件本质上是 Pak 文件,可以使用 UnrealPak 工具查看其内容

常见问题解决

问题1:Feature Pack 没有出现在对话框中

  • 检查 .upack 文件是否放在 Engine/FeaturePacks/ 目录下
  • 检查 manifest.json 格式是否正确,所有必需字段是否齐全
  • 查看 Output Log 中 LogFeaturePack 分类的错误日志
  • 确认 IsDataValid() 返回 true(内部检查了 bPackValid 标志)

问题2:安装后资产没有保存

  • InstallToProject() 中调用了 FEditorFileUtils::PromptForCheckoutAndSave(),如果资产未保存,检查是否有版本控制冲突
  • 确保 FAssetCompilingManager::Get().FinishAllCompilation() 等待异步编译完成

问题3:图片不显示

  • 检查 manifest.json 中 ThumbnailScreenshots 字段是否正确
  • 图片必须是 PNG 格式
  • 对于 .upack 文件,确保图片路径在 Pak 内部为 Media/ 目录
  • 对于松散格式,确保图片相对于 manifest.json 的路径为 Media/ 目录

问题4:目录监控不生效

  • 确认 FDirectoryWatcher 模块已正确加载
  • 检查 Feature Pack 目录路径是否存在(FPaths::FeaturePackDir()
  • 如果目录不存在,StartUpDirectoryWatcher() 会自动创建

附录

A. 文件结构参考

AddContentDialog/
├── AddContentDialog.Build.cs
├── Public/
│   ├── IAddContentDialogModule.h          (模块接口)
│   └── FeaturePackContentSource.h         (Feature Pack 内容源声明)
├── Private/
│   ├── AddContentDialogModule.cpp         (模块实现)
│   ├── IContentSource.h                   (内容源接口 + 辅助类型)
│   ├── IContentSourceProvider.h           (内容源提供者接口)
│   ├── ContentSourceProviderManager.h     (内容源提供者管理器)
│   ├── ContentSourceProviderManager.cpp
│   ├── ContentSourceDragDropOp.h          (拖拽操作)
│   ├── ContentSourceDragDropOp.cpp
│   ├── SAddContentDialog.h               (对话框窗口)
│   ├── SAddContentDialog.cpp
│   ├── SAddContentWidget.h               (主内容 Widget)
│   ├── SAddContentWidget.cpp
│   ├── ViewModels/
│   │   ├── AddContentWidgetViewModel.h    (核心 ViewModel)
│   │   ├── AddContentWidgetViewModel.cpp
│   │   ├── CategoryViewModel.h           (分类 ViewModel)
│   │   ├── CategoryViewModel.cpp
│   │   ├── ContentSourceViewModel.h      (内容源 ViewModel)
│   │   └── ContentSourceViewModel.cpp
│   └── ContentSourceProviders/
│       └── FeaturePack/
│           ├── FeaturePackContentSource.cpp        (Feature Pack 解析与安装)
│           ├── FeaturePackContentSourceProvider.h
│           └── FeaturePackContentSourceProvider.cpp (Feature Pack 扫描与监控)

B. 相关资源

  • UE5 官方文档:Slate UI Framework
  • UE5 官方文档:Pak File System
  • UE5 官方文档:AssetTools Module
  • WidgetCarousel 模块源码
  • DirectoryWatcher 模块源码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值