NiceTry12138 2276de8a97 feat: 添加对象关系 il y a 5 mois
..
Image 2276de8a97 feat: 添加对象关系 il y a 5 mois
README.md 2276de8a97 feat: 添加对象关系 il y a 5 mois

README.md

FlowGraph

全新的资产类型

前置内容:https://zhuanlan.zhihu.com/p/639752004

蓝图编辑器的作用主要就是为了帮助使用者方便的进行数据配置

我觉得蓝图本身就是一种配置

虚幻提供了为自定义类型扩展编辑界面的方法

本质上来说,这一套流程分为三个部分

  1. UObject,也就是资源对象本体所代表的数据
  2. 编辑器类,UObject 对应的编辑器类,可能存储一些额外的数据
  3. UI类,编辑器类对应的显示类,用于方便用户使用

定于原始数据类型

FlowGraph 插件来说,UFlowAsset 就是那个用于存储资源对象原始数据的类,所有的操作都是基于这个类来运行的

UCLASS(BlueprintType, hideCategories = Object)
class FLOW_API UFlowAsset : public UObject

关联 Asset

如何基于这个 UObject 创建资产(Asset) 呢?】

根据 前置内容 来看,继承 IAssetTypeActions 来定义 Asset 的效果

FlowGraph 的插件中,使用 IAssetTypeActions 是下面这样的

class FLOWEDITOR_API FAssetTypeActions_FlowAsset : public FAssetTypeActions_Base
{
public:
	virtual FText GetName() const override;
	virtual uint32 GetCategories() override;
	virtual FColor GetTypeColor() const override { return FColor(255, 196, 128); }

	virtual UClass* GetSupportedClass() const override;
	virtual void OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor = TSharedPtr<IToolkitHost>()) override;

	virtual void PerformAssetDiff(UObject* OldAsset, UObject* NewAsset, const FRevisionInfo& OldRevision, const FRevisionInfo& NewRevision) const override;
};
函数名称 作用 示例
GetName 显示名称 "Flow Asset"
GetCategories 所属类型 static_castEAssetTypeCategories::Type(0)
GetTypeColor 资产下面的颜色条 FColor(255, 196, 128)
GetSupportedClass 该资产关联的 Class UFlowAsset::StaticClass()
OpenAssetEditor 打开资产编辑面板

FAssetTypeActions_Base 是继承自 IAssetTypeActions 的,预先定义好很多默认内容

与此同时,UE5 也提供了新的方法,来定义 Asset,其名为 UAssetDefinition

class FLOWEDITOR_API UAssetDefinition_FlowAsset : public UAssetDefinition
{
	GENERATED_BODY()

public:
	virtual FText GetAssetDisplayName() const override;
	virtual FLinearColor GetAssetColor() const override;
	virtual TSoftClassPtr<UObject> GetAssetClass() const override;
	virtual TConstArrayView<FAssetCategoryPath> GetAssetCategories() const override;
	virtual FAssetSupportResponse CanLocalize(const FAssetData& InAsset) const override;

	virtual EAssetCommandResult OpenAssets(const FAssetOpenArgs& OpenArgs) const override;
	virtual EAssetCommandResult PerformAssetDiff(const FAssetDiffArgs& DiffArgs) const override;
};
函数名称 作用 示例
GetAssetDisplayName 显示名称 "Flow Asset"
GetAssetColor 资产下面的颜色条 FColor(255, 196, 128)
GetAssetClass 资产关联的 Class UFlowAsset::StaticClass()
GetAssetCategories 所属类型 { FFLowAssetCategoryPaths::Flow }
OpenAssets 打开资产对应的编辑面板

FlowGraph 中,无论是 OpenAssets 还是 OpenAssetEditor,统一调用的接口都是

const FFlowEditorModule* FlowModule = &FModuleManager::LoadModuleChecked<FFlowEditorModule>("FlowEditor");
FlowModule->CreateFlowAssetEditor(OpenArgs.GetToolkitMode(), OpenArgs.ToolkitHost, FlowAsset);

创建 Asset

这里倒是与 前置知识 相同,通过继承 UFactory 来 Asset 对象

UCLASS(HideCategories = Object)
class FLOWEDITOR_API UFlowAssetFactory : public UFactory
{
	GENERATED_UCLASS_BODY()

	UPROPERTY(EditAnywhere, Category = Asset)
	TSubclassOf<class UFlowAsset> AssetClass;

	virtual bool ConfigureProperties() override;
	virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;

protected:
	// Parameterized guts of ConfigureProperties()
	bool ConfigurePropertiesInternal(const FText& TitleText);
};

当你在资源管理器中右键,会先触发 ConfigureProperties,根据返回的 bool 值判断能否创建

此时会创建一个虚拟的 Asset,当你确定文件名称之后,才会执行 FactoryCreateNew 用于表示一个 Asset 真正创建了

其实也很正常,毕竟在 确定名称 之后才能真正创建。当你右键创建资产,但是不按回车或者鼠标来设定名称,而是一直按 ESC,资产不会被创建

定义图表编辑器

https://blog.csdn.net/u013412391/article/details/107945507

与蓝图编辑器类似,定义类似的编辑器需要

  1. UEdGraph 代表图表对象,存储所有的节点
  2. UEdGraphNode 代表图标对象中的一个节点
  3. UEdGraphSchema 定义图表的规则,比如 鼠标右键点击 或者 拖拽一个引脚并释放
  4. SFlowGraphNode 图表编辑器中具体的 UI 界面
  5. SGraphEditor 封装了图表编辑器的界面
  6. FEdGraphSchemaAction 定义资产编辑器的操作,比如复制、新建节点,添加注释等

FFlowAssetEditor

当想要打开一个 Asset 的时候,会触发下面这个 CreateFlowAssetEditor 函数,创建一个 FFlowAssetEditor 资产编辑器

TSharedRef<FFlowAssetEditor> FFlowEditorModule::CreateFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr<IToolkitHost>& InitToolkitHost, UFlowAsset* FlowAsset)
{
	TSharedRef<FFlowAssetEditor> NewFlowAssetEditor(new FFlowAssetEditor());
	NewFlowAssetEditor->InitFlowAssetEditor(Mode, InitToolkitHost, FlowAsset);
	return NewFlowAssetEditor;
}

FFlowAssetEditor 的定义如下

class FLOWEDITOR_API FFlowAssetEditor : public FAssetEditorToolkit, public FEditorUndoClient, public FGCObject, public FNotifyHook

提供了一套完整的框架来处理编辑器的生命周期、UI布局、菜单系统、撤销重做等功能

Undo Redo

使用 FEditorUndoClient 来实现 Undo 功能

void FFlowAssetEditor::PostUndo(bool bSuccess)
{
	HandleUndoTransaction();
}

void FFlowAssetEditor::PostRedo(bool bSuccess)
{
	HandleUndoTransaction();
}

void FFlowAssetEditor::HandleUndoTransaction()
{
	SetUISelectionState(NAME_None);
	GraphEditor->NotifyGraphChanged();
	FSlateApplication::Get().DismissAllMenus();
}

PostUndoPostRedo 都是 FEditorUndoClient 提供的接口

HandleUndoTransaction 本质是声明:"状态已变,请更新显示"

变化事件

使用 FNotifyHook 提供 属性变化 接口,也就是 NotifyPostChangeNotifyPreChange 函数

void FFlowAssetEditor::NotifyPostChange(const FPropertyChangedEvent& PropertyChangedEvent, FProperty* PropertyThatChanged)
{
	if (PropertyChangedEvent.ChangeType != EPropertyChangeType::Interactive)
	{
		GraphEditor->NotifyGraphChanged();
	}
}

这个属性可能是成员属性,也可能是 Graph 中某个 Node 的配置属性

窗口控件

当然,最重要的应该是基于 FAssetEditorToolkit 而实现的接口

使用 FAssetEditorToolkit,它是构建自定义资产编辑器的核心基类,用于创建类似蓝图编辑器、材质编辑器等专业编辑环境

virtual void RegisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override;
virtual void UnregisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override;

virtual void InitToolMenuContext(FToolMenuContext& MenuContext) override;
virtual void PostRegenerateMenusAndToolbars() override;
virtual void SaveAsset_Execute() override;
virtual void SaveAssetAs_Execute() override;
函数名称 作用
RegisterTabSpawners 义编辑器布局结构
UnregisterTabSpawners 编辑器关闭时反注册标签页
InitToolMenuContext 扩展编辑器菜单/工具栏
PostRegenerateMenusAndToolbars 菜单/工具栏刷新后执行额外操作
SaveAsset_Execute 执行"保存"命令的核心逻辑
SaveAssetAs_Execute 执行"另存为"命令

当打开了 Standalone 的编辑窗口,有哪些窗口?这个实现的地方就是 RegisterTabSpawners

void FFlowAssetEditor::RegisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager)
{
	WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_FlowAssetEditor", "Flow Editor"));
	const auto WorkspaceMenuCategoryRef = WorkspaceMenuCategory.ToSharedRef();

	FAssetEditorToolkit::RegisterTabSpawners(InTabManager);

	InTabManager->RegisterTabSpawner(DetailsTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_Details))
				.SetDisplayName(LOCTEXT("DetailsTab", "Details"))
				.SetGroup(WorkspaceMenuCategoryRef)
				.SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Details"));

	InTabManager->RegisterTabSpawner(GraphTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_Graph))
				.SetDisplayName(LOCTEXT("GraphTab", "Graph"))
				.SetGroup(WorkspaceMenuCategoryRef)
				.SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "GraphEditor.EventGraph_16x"));

    // 其他 Tab 的创建
}

注册和绑定资产事件,刷新 UFlowGraph

FlowAsset = CastChecked<UFlowAsset>(ObjectToEdit);

UFlowGraph* FlowGraph = Cast<UFlowGraph>(FlowAsset->GetGraph());
if (IsValid(FlowGraph))
{
    // Call the OnLoaded event for the flowgraph that is being edited
    FlowGraph->OnLoaded();
}

// 支持撤销和还原
FlowAsset->SetFlags(RF_Transactional);
GEditor->RegisterForUndo(this);

UFlowGraphSchema::SubscribeToAssetChanges();
FlowAsset->OnDetailsRefreshRequested.BindThreadSafeSP(this, &FFlowAssetEditor::RefreshDetails);

InitFlowAssetEditor

打开 Asset 创建了 FFlowAssetEditor 之后,执行的第一个函数就是 InitFlowAssetEditor

这是在 RegisterTabSpawners 之前执行的,毕竟 RegisterTabSpawners 只是注册了有哪些 Tab 窗口

Tab 窗口如何布局是在 InitFlowAssetEditor 中定义的

CreateWidgets();

// 定于布局
const TSharedRef<FTabManager::FLayout> StandaloneDefaultLayout = FTabManager::NewLayout("FlowAssetEditor_Layout_v5.1")
    ->AddArea
    (
        FTabManager::NewPrimaryArea()
        ->SetOrientation(Orient_Horizontal)
        ->Split
        (
            // 其他窗口的布局设置
        )
        ->Split
        (
            FTabManager::NewStack()
            ->SetSizeCoefficient(0.125f)
            ->AddTab(PaletteTab, ETabState::OpenedTab)
        )
    );

注意:CreateWidgets 并没有创建 Tab 控件,而是自定义控件,Tab 控件在 RegisterTabSpawners 注册之后由系统调用创建

Palette

正如前面所说,PaletteTabRegisterTabSpawners 函数中注册

InTabManager->RegisterTabSpawner(PaletteTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_Palette))
			.SetDisplayName(LOCTEXT("PaletteTab", "Palette"))
			.SetGroup(WorkspaceMenuCategoryRef)
			.SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "Kismet.Tabs.Palette"));

这里注册了 PaletteTab 对应的创建函数是 FFlowAssetEditor::SpawnTab_Palette,不过从函数内容来看,其实就是 SDockTab 包了个 Palette 对象

TSharedRef<SDockTab> FFlowAssetEditor::SpawnTab_Palette(const FSpawnTabArgs& Args) const
{
	check(Args.GetTabId() == PaletteTab);

	return SNew(SDockTab)
		.Label(LOCTEXT("FlowPaletteTitle", "Palette"))
		[
			Palette.ToSharedRef()
		];
}

Palette 对象也很简单

Palette = SNew(SFlowPalette, SharedThis(this));

SFlowPalette 的内容很简单

  • 使用 SGraphActionMenu 作为列表容器,显示 Item,提供 Item 拖拽功能
  • 使用 STextComboBox 提供文本输入功能,作为列表筛选
this->ChildSlot
[
	SNew(SBorder)
	.Padding(2.0f)
	.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
	[
		SNew(SVerticalBox)
		+ SVerticalBox::Slot() // Filter UI
			.AutoHeight()
			[
				SNew(SHorizontalBox)
				+ SHorizontalBox::Slot()
					.VAlign(VAlign_Center)
					[
						SAssignNew(CategoryComboBox, STextComboBox)
							.OptionsSource(&CategoryNames)
							.OnSelectionChanged(this, &SFlowPalette::CategorySelectionChanged)
							.InitiallySelectedItem(CategoryNames[0])
					]
			]
		+ SVerticalBox::Slot() // Content list
			.HAlign(HAlign_Fill)
			.VAlign(VAlign_Fill)
			[
				SAssignNew(GraphActionMenu, SGraphActionMenu)
					.OnActionDragged(this, &SFlowPalette::OnActionDragged)
					.OnActionSelected(this, &SFlowPalette::OnActionSelected)
					.OnCreateWidgetForAction(this, &SFlowPalette::OnCreateWidgetForAction)
					.OnCollectAllActions(this, &SFlowPalette::CollectAllActions)
					.OnCreateCustomRowExpander_Static(&LocalUtils::CreateCustomExpanderStatic, false)
					.AutoExpandActionMenu(true)
			]
	]
];
SGraphActionMenu 回调接口
OnActionDragged 设置动作拖拽事件的处理函数
OnActionSelected 选中处理函数
OnCreateWidgetForAction 自定义每个 Item 的显示控件
OnCollectAllActions 设置菜单数据源收集函数

通过 SFlowPalette::CollectAllActions 收集到了数据源,核心内容在 UFlowGraphSchema::GetFlowNodeActions 中,收集到的数据源是基于 UFlowNodeBase 的类,包括 C++ 和 蓝图的

TArray<UClass*> UFlowGraphSchema::NativeFlowNodes;				// 基于 UFlowNodeBase 的 C++ 类
TMap<FName, FAssetData> UFlowGraphSchema::BlueprintFlowNodes;	// 基于 UFlowNodeBase 的 蓝图类

好奇 NativeFlowNodesBlueprintFlowNodes 是怎么得到的,核心就是 GetDerivedClasses

GetDerivedClasses(FlowNodeBaseClass, FlowNodesOrAddOns);

通过 GetDerivedClasses 得到基于 FlowNodeBaseClassTArray<UClass*>,核心代码大概如下

FUObjectHashTables& ThreadHash = FUObjectHashTables::Get();
TSet<UClass*>* DerivedClasses = ThreadHash.ClassToChildListMap.Find(ClassToLookFor);
Results.Append( DerivedClasses->Array() );

Graph

TSharedRef<SDockTab> FFlowAssetEditor::SpawnTab_Graph(const FSpawnTabArgs& Args) const
{
	check(Args.GetTabId() == GraphTab);

	TSharedRef<SDockTab> SpawnedTab = SNew(SDockTab)
		.Label(LOCTEXT("FlowGraphTitle", "Graph"));

	if (GraphEditor.IsValid())
	{
		SpawnedTab->SetContent(GraphEditor.ToSharedRef());
	}

	return SpawnedTab;
}

通过上述代码创建 Graph 对应的 Tab,具体的控件对象是 GraphEditor

void FFlowAssetEditor::CreateGraphWidget()
{
	SAssignNew(GraphEditor, SFlowGraphEditor, SharedThis(this))
		.DetailsView(DetailsView);
}

功能实现是 SFlowGraphEditor 完成的

UEdGraph

InitFlowAssetEditorUEdGraph 进行过一些操作

UFlowGraph* FlowGraph = Cast<UFlowGraph>(FlowAsset->GetGraph());
if (IsValid(FlowGraph))
{
    // Call the OnLoaded event for the flowgraph that is being edited
    FlowGraph->OnLoaded();
}

在创建 SFlowGraphEditor 也就是 SGraphEditor 时,顺便将 FlowGraph 也传递进去了

// 创建 SFlowGraphEditor 对象
SAssignNew(GraphEditor, SFlowGraphEditor, SharedThis(this))
		.DetailsView(DetailsView);

// 触发 SFlowGraphEditor 构造函数
void SFlowGraphEditor::Construct(const FArguments& InArgs, const TSharedPtr<FFlowAssetEditor> InAssetEditor)
{
	FlowAssetEditor = InAssetEditor;                        // 得到 FFlowAssetEditor
	FlowAsset = FlowAssetEditor.Pin()->GetFlowAsset();      // 得到 UFlowAsset
	SGraphEditor::FArguments Arguments;
	Arguments._GraphToEdit = FlowAsset->GetGraph();         // 得到 UEdGraph

    // 其他参数设置
	SGraphEditor::Construct(Arguments);
}