ue4官方网站_UE4自定义状态机资源及其编辑器

af27c07499b76f4f416d78bbbe0f5dcc.png

开始于2018.9.18 14:28

这是我写的第一篇文章,略乱,感谢各位的谅解。

Code:

1762757171/UE4_Plugin_FSMNameDrived​github.com
d13116684b1362ae4b714f4e27d332df.png

〇、先决及声明

1.先决条件:

1.了解UE4的C++、Slate

2.用过几天UE4(蓝图、动画蓝图状态机什么的知道怎么用)

3.了解各种UE4的变量类型,如FText FName FString及三者关系等等,很基础的就可以

4.编写插件,暂时不会写也没事,至少知道UE4存在插件这个东西

5.有自行Debug的能力

6.会清理并重新生成.sln

2.声明

1.有可能省略UPROPERTY、UCLASS、GENERATED_BODYXXXX_API等宏,以及各种#include,什么地方加什么不加什么请自行判断。

2.我遇到的花费时间来解决的问题我都会列举出来,其他小问题自己查一查,什么都能解决。

3.这不是一篇教程,只是一次接触研究新鲜事物的经历的记录。不过希望也可以能帮到什么。

4.随时可能更新修改,也欢迎大家捉虫。

5.本文较长,因为我想尽可能写的详细一些,写完后全文读完不出意外可能要一天时间,第一次写不懂行文有点啰嗦多多包涵。

6.虽然尽可能详细,但官方网站可以查到的、以及很容易就能搜索到的东西就不写了。

7.所有图表都是自己截屏或者手绘的,不够美观请多谅解。作图网站:

ProcessOn - 免费在线作图,思维导图,流程图,实时协作​www.processon.com
9f8861728eec6c7e8186e9ffaabb835e.png

一、动机

在自己制作的游戏中很多地方可以用到状态机,比如动画(Paper2D暂不支持动画蓝图)、对话、行为树(这个还是建议用官方的),状态的转换是很多组件的核心。因此打算自己手撸一个状态机资源与编辑器,方便编辑。

二、创建插件

这部分很好找参考资料,几笔带过。

4930eb058f28a489d9b2176bf0d2154a.png

00d6b748e8f6a0e3ce01bb444ff660e1.png

507a138ec429582d78235c062434b2a9.png

选空白,填了就行了,我的叫FSMNameDrived。可能会提示自动编译失败,退出去从VS运行一下就可以了。

在其下分为两个Module,也即在/Plugins/FSMNameDrived/Source/文件夹下新建两个文件夹,FSMAsset和FSMEditor,分别存储资源和编辑器(原来应该有个Source/FSMNameDrived文件夹,直接删了或者把它改成其中一个都行)。之所以这么分是因为游戏运行是不需要Editor部分的,这部分只是在开发的时候方便开发用的。

因此FSMNameDrived.uplugin中"Modules"修改为

"Modules": [
		{
			"Name": "FSMAsset",
			"Type": "Runtime",
			"LoadingPhase": "Default"
		},
		{
			"Name": "FSMEditor",
			"Type": "Editor",
			"LoadingPhase": "PreDefault"
		}
	]

并在这两个文件夹下都分别创建一个.Build.cs文件,这个文件是用来描述模块间链接和依赖的,填错是会出奇怪的不容易调试的问题的。以FSMAsset模块举例:

FSMAsset.Build.cs

// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class FSMAsset : ModuleRules
{
	public FSMAsset(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
		
		PublicIncludePaths.AddRange(
			new string[] {
				// ... add public include paths required here ...
            }
			);
				
		
		PrivateIncludePaths.AddRange(
			new string[] {
				// ... add other private include paths required here ...
            }
			);
			
		
		PublicDependencyModuleNames.AddRange(
			new string[]
			{
				"Core",
				// ... add other public dependencies that you statically link with here ...
			}
			);
			
		
		PrivateDependencyModuleNames.AddRange(
			new string[]
			{
				"CoreUObject",
				"Engine",
				"Slate",
				"SlateCore",
				// ... add private dependencies that you statically link with here ...	
            }
			);

        DynamicallyLoadedModuleNames.AddRange(
			new string[]
			{
				// ... add any modules that your module loads dynamically here ...
			}
			);
	}
}

FSMEditor.Build.cs与之类似。

再在每个模块文件夹下添加文件夹Classes、Public和Private,分别用于储存.h和.cpp。大家都这么干,可能是觉得好管理并且一致性高吧。

和写DLL很像,UE4的模块需要自定义一个类来实现模块加载卸载接口

FSMAsset.h于Public文件夹下

// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"

class FFSMAssetModule : public IModuleInterface
{
public:

	/** IModuleInterface implementation */
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;

};

FSMAsset.cpp于Private文件夹下

// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.

#include "FSMAsset.h"

#define LOCTEXT_NAMESPACE "FFSMAssetModule"

void FFSMAssetModule::StartupModule()
{
	// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
}

void FFSMAssetModule::ShutdownModule()
{
	// This function may be called during shutdown to clean up your module.  For modules that support dynamic reloading,
	// we call this function before unloading the module.
}

#undef LOCTEXT_NAMESPACE
	
IMPLEMENT_MODULE(FFSMAssetModule, FSMAsset)

FSMEditor与之类似。

其实通过新插件按钮来创建插件这些都是有的,只不过分成两个Module需要分别写一个。

最后,Plugins文件夹内文件夹结构应该是这样的:

e4f9c5c97448ffec3b044e0101021342.png

Classes我是看别的模块也有我就加了,用来储存后面会提到的各种自定义类,Public就只存了一个模块相关的.h(如FSMAsset.h)

另:

Level Editor的热编译不能编译插件,插件热编译在

3433a55eb8c38391197db64e877c6ce6.png

f38aeb33a89e223cc5d3e8696ac62893.png

这里重新编译。

三、数据结构设计

不打算采用设计模式里的状态机,因为更希望把数据和逻辑分开,数据储存到资源中,逻辑储存到状态机对应的ActorComponent中。还是考虑到如果一个状态一个class的话会很多复用性不高的class,如果各位持有不同意见欢迎讨论。

在继承自UObject类的自定义类(位于/FSMAsset/Classes/UFSM.h):

class UFSM : public UObject

中含有成员变量

FFSMData StateMachine;

其中FFSMData类型的数据结构为:

using FCurrentNodeName = FName;    //当前节点名
using FTransRuleName = FName;    //转换规则名
using FTargetNodeName = FName;    //目标节点名

TMap<FCurrentNodeName, TMap<FTransRuleName, FTargetNodeName>> StateMachine;

可以看到是完全由FName驱动的一个状态机,当

void UFSM::MeetCondition(FTransRuleName TransRuleName)

被调用,寻找对应的目标节点名,赋值给当前节点名,并触发自定义delegate

OnStateChanged.Broadcast(Owner, FromNodeName, TargetNodeName);

可以说是非常简单的设计。估计也是考虑到太复杂了做起来能力不够=)

四、如何在UE4资源管理器中可以创建资源

目的是让自己的UFSM可以像动画蓝图那样创建(内容浏览器中右键然后选)。这部分也应该很简单,参考资料很多,几笔带过。

核心是创建自己的UFactory类,.h于/FSMEditor/Classes/,.cpp于/FSMEditor/Private/

class FSMEDITOR_API UFSMFactory : public UFactory

并实现成员函数

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

实现也很简单,NewObject一个UFSM并且Flags | RF_Transactional(好像是为了Undo Buffer),然后返回就行了。

再来实现一个类继承自FAssetTypeActions_Base类,目的是告诉编辑器UFSM这个资源是在什么类下的以及图标什么样描述什么样双击了会怎么样(资源行为)。(/FSMEditor/Classes/)

//=============================
//FSMAssetTypeAction.h于Classes

    struct FAssetTypeAction_FSM : public FAssetTypeActions_Base
{
public:
	// IAssetTypeActions Implementation
	virtual FText GetName() const override { return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_FSM", "FSM"); }
	virtual FColor GetTypeColor() const override { return FColor(192, 192, 192); }
	virtual UClass* GetSupportedClass() const override;
	virtual uint32 GetCategories() override;
};

//=============================
//FSMAssetTypeAction.cpp于Private

UClass * FAssetTypeAction_FSM::GetSupportedClass() const
{
	return UFSM::StaticClass();
}

uint32 FAssetTypeAction_FSM::GetCategories()
{
	return EAssetTypeCategories::Gameplay;
}

都是我乱填了,以后再改呗。

然后再在模块加载时把这个FAssetTypeAction_FSM类注册到FAssetToolsModule中

void FFSMEditorModule::StartupModule()
{
	// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
	IAssetTools& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
	TSharedPtr<FAssetTypeAction_FSM> FSMAssetTypeAction = MakeShareable(new FAssetTypeAction_FSM);
	AssetToolsModule.RegisterAssetTypeActions(FSMAssetTypeAction.ToSharedRef());
}

此时编译会发现有错误不能成功编译,这是因为UFactory所在的模块UnrealEd没有被包含,同时FSMEditor模块用到了位于FSMAsset模块中的UFSM类。因此需要在FSMEditor.Build.cs中的PrivateDependencyModuleNames项中加入"UnrealEd",PublicDependencyModuleNames项中加入"FSMAsset"。

这里是我遇到的一次小坎坷:

由于FSMEditor忘记在FSMEditor.Build.cs的PublicDependencyModuleNames中添加"FSMAsset",
导致每次编译都会出现无法找到FSM.generated.h,
而VS又可以通过#include正常打开FSM.generated.h,
所以用了整整三个小时来定位这个问题。
最后是发现把所有FSMEditor模块中对FSM.h的引用删除(注释掉)后可以正常编译,
因此问题得以定位:
FSMEditor没能正常包含FSM.h,
此时反应过来是dependency的问题。
其实并不是毫无预兆的,
在参考的代码中都直接跨模块include,
但自己只能FSMAsset/Classes/FSM.h来引用,
其实已经暗示了问题。

到这应该就可以:

3f880885d3bf5f3386ee104d8c2eb3f8.png

bc1913985ddfbd382c745e5bd5dcb8c8.png

但是双击会打开一个

3aa8c683f2e3ab029a1389f98e9586d4.png

看不了编辑不了什么都做不了,那么接下来来做编辑器部分。

五、制作编辑器

一、想好自己想要什么样子的编辑器

我想做的编辑器很简单,类似于动画蓝图中的状态机

7df1fde4f839bd93514c3c357bc6393d.png

但是相对来说还简单很多,只保留:

1.工具栏,保存和浏览功能就可以,没有逻辑在这个资源里,所以不需要编译功能;

2.图表部分,也就是带网格能够放节点、连线的地方,并且也不需要状态机这样双击状态/规则进去写逻辑的功能,取而代之是简简单单的显示名字就可以。

c512d4aa2c9820cb0003979b8c3e83b0.png
样例原型图

二、创建自定义编辑器窗体

首先了解一下几个关键词,同时也是会用到的几种class:

1.Asset Editor

双击内容浏览器里的资源打开的那个窗体,每种资源有各自不同的Asset Editor;

2.Graph

编辑蓝图时的面板,就是带网格的那种

139dfdb379cddec2a1d7f66581001593.png

3.Schema

翻译的意思是模式、图解、概要,我理解是一种规则,定义这个蓝图内 创建/删除 节点/连接 等等一切操作的逻辑;

4.Node

Graph中的一个节点,蓝图中的函数、状态机中的一个状态,都是一种Node;

5.Pin

Graph中可以相连接的引脚,比如蓝图中函数节点的exec引脚和参数引脚、状态机中状态节点最外面那一圈;

5feddc2a4041e26cb1a280465b5705db.png

6.Connection Drawing Policy

定义连接两个Pin的线(通常情况下都是线对吧)的外观

7.Schema Action Menu

在Graph内点击右键或从Pin拉出连接线到空白地方时会出现的选择框

a90b914c9eaa75468adf9b6ba52f974e.png

8.Schema Action

Schema Action Menu中的每一项就是一个Schema Action,定义着每一项在Action Menu中所属的目录(Category)(比如上图第一个Category就是AI)、Action的名字、以及点击之后的操作;


我们接下来是代码顺序是:

1.AssetEditor

2.Node相关的三个类(后面会提到U、F、S类)

3.Pin、ConnectionDrawingPolicy类

4.Schema Action

5.Schema Action Menu

6.Transition Node,类比于第2点Node相关类

7.右键点击节点显示的菜单

8. Name Validator

9. Entry Node


当在资源编辑器中双击资源时,会调用其对应的AssetTypeEditor的

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

因此要在这个函数中写入打开自定义资源编辑器的逻辑。

首先,创建资源管理器相关的类,

class FSMEDITOR_API FFSMAssetEditor :public FNotifyHook, public FEditorUndoClient, public FAssetEditorToolkit

注意要有XXXX_API这个宏,这样才可以在DLL中暴露,UE4才可以调用以打开编辑器。

FNotifyHook作用是什么暂时没了解,剩下两个字面意思应该可以猜到。

首先要做的肯定是初始化AssetEditor,FAssetEditor中的初始化函数:

void FAssetEditorToolkit::InitAssetEditor( const EToolkitMode::Type Mode, const TSharedPtr< class IToolkitHost >& InitToolkitHost, const FName AppIdentifier, const TSharedRef<FTabManager::FLayout>& StandaloneDefaultLayout, const bool bCreateDefaultStandaloneMenu, const bool bCreateDefaultToolbar, UObject* ObjectToEdit, const bool bInIsToolbarFocusable )

但我的做法并不是直接继承它,而是自定义一个初始化函数,因为并不需要暴露那么多参数,而且以后可能会增加额外的参数:

void FFSMAssetEditor::InitializeAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr<IToolkitHost>& InitToolkitHost, UObject * ObjectToEdit)

在这个函数里就可以把正在编辑的UFSM对象和新创建的Graph保存为成员变量,其中创建Graph的代码如下:

//InitializeAssetEditor函数中
GraphView = CreateGraphEditorWidget();
//CreateGraphEditorWidget函数中
TSharedRef<class SGraphEditor> FFSMAssetEditor::CreateGraphEditorWidget()
{
	auto Graph = FBlueprintEditorUtils::CreateNewGraph(TargetFSM, NAME_None, UFSMGraph::StaticClass(), UFSMGraphSchema::StaticClass());
	Graph->bAllowDeletion = false;

	// Customize the appereance of the graph.
	FGraphAppearanceInfo AppearanceInfo;
	// The text that appears on the bottom right corner in the graph view.
	AppearanceInfo.CornerText = LOCTEXT("AppearanceCornerText_FSM", "FSM");
	AppearanceInfo.InstructionText = LOCTEXT("AppearanceInstructionText_FSM", "Right Click to add new nodes.");

	// Bind graph events actions from the editor
	SGraphEditor::FGraphEditorEvents InEvents;
	InEvents.OnTextCommitted = FOnNodeTextCommitted::CreateSP(this, &FFSMAssetEditor::OnNodeNameCommitted);
	//InEvents.OnSelectionChanged = SGraphEditor::FOnSelectionChanged::CreateSP(this, &FFSMAssetEditor::OnSelectedNodesChanged);
	InEvents.OnCreateActionMenu = SGraphEditor::FOnCreateActionMenu::CreateSP(this, &FFSMAssetEditor::OnCreateGraphActionMenu);
	
	return SNew(SGraphEditor)
		//.AdditionalCommands(GraphEditorCommands)
		.IsEditable(true)
		.Appearance(AppearanceInfo)
		.GraphToEdit(Graph)
		.GraphEvents(InEvents)
		.ShowGraphStateOverlay(false);
}

其中OnSelectedNodesChanged和OnCreateGraphActionMenu函数看名字就可以知道是什么意思,这两个自定义函数先创建但先留空函数体,等稍后来实现,函数签名如下:

void OnNodeNameCommitted(const FText& NewText, ETextCommit::Type CommitInfo, UEdGraphNode* NodeBeingChanged) const;
FActionMenuContent OnCreateGraphActionMenu(UEdGraph* Graph, const FVector2D& NodePosition, const TArray<UEdGraphPin*>& DraggedPins, bool bAutoExpand, SGraphEditor::FActionMenuClosed OnMenuClosed);

其中OnNodeNameCommitted是在有节点被重命名时调用的,OnCreateGraphActionMenu是在 右键点击到空白处 或者 从Pin拉出引线到空白处 时被调用的。

注意两点:

1.一个资源同时只允许一个编辑器编辑,尽管在这里只有这一种AssetEditor会编辑UFSM,但还是最好在初始化函数的一开始加上一句

	// close all other editors editing this asset
	FAssetEditorManager::Get().CloseOtherEditors(ObjectToEdit, this);

2.AssetEditor的布局应该是封装了Slate,采用以下方式描述布局:

const TSharedRef<FTabManager::FLayout> Layout = FTabManager::NewLayout("FSMAssetEditor_Layout")
		->AddArea
		(//PrimaryArea Begin
			FTabManager::NewPrimaryArea()
			->SetOrientation(Orient_Vertical)
			->Split
			(//Toolbar Begin
				FTabManager::NewStack()
				->SetSizeCoefficient(0.1f)
				->SetHideTabWell(true)
				->AddTab(GetToolbarTabId(), ETabState::OpenedTab)
			)//Toolbar End
			->Split
			(//Editor Begin
				FTabManager::NewStack()
				->SetSizeCoefficient(0.9f)
				->SetHideTabWell(true)
				->AddTab(TabID_EditorGraphCanvas, ETabState::OpenedTab)
			)//Editor End
		);//PrimaryArea End

其中AddTab的第一个参数是个FName,作用是ID,如果是GetToolbarId()获得的ID就会自动将这个Tab作为工具栏,如果是自定义的ID,如上述代码中用到的

/*static*/ const FName FFSMAssetEditor::TabID_EditorGraphCanvas(TEXT("EditorGraphCanvas"))

就需要在:

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

的实现中用到,传入创建Graph的自定义函数,作为这个自定义ID对应的Tab的内容:

	TabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_FSMAssetEditor", "FSM Asset Editor"));
	const TSharedRef<FWorkspaceItem> WorkspaceMenuCategoryRef = WorkspaceMenuCategory.ToSharedRef();

	FAssetEditorToolkit::RegisterTabSpawners(TabManager);

	TabManager->RegisterTabSpawner(TabID_EditorGraphCanvas, FOnSpawnTab::CreateSP(this, &FFSMAssetEditor::SpawnTab_GraphCanvas))
		.SetDisplayName(LOCTEXT("GraphCanvasTab", "Viewport"))
		.SetGroup(WorkspaceMenuCategoryRef)
		.SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "GraphEditor.EventGraph_16x"));

其中,SpawnTab_GraphCanvas:

TSharedRef<SDockTab> FFSMAssetEditor::SpawnTab_GraphCanvas(const FSpawnTabArgs & Args) const
{
	check(Args.GetTabId() == TabID_EditorGraphCanvas);

	return SNew(SDockTab)
		.Label(LOCTEXT("EditorGraphCanvas", "Viewport"))
		[
			GraphView.ToSharedRef()
		]; 
}

实现了RegisterTabSpawners别忘了也实现一下UnregisterTabSpawner

void FFSMAssetEditor::UnregisterTabSpawners(const TSharedRef<class FTabManager>& TabManager)
{
	FAssetEditorToolkit::UnregisterTabSpawners(TabManager);

	TabManager->UnregisterTabSpawner(TabID_EditorGraphCanvas);
}

这里附上几个纯虚函数的实现(乱实现的,打算以后如果有兴趣的话再改)

//class FFSMAssetEditor内
	virtual FName GetToolkitFName() const override { return FName("FSMAssetEditor"); }
#define LOCTEXT_NAMESPACE "FSMEditorNativeNames" 
	virtual FText GetBaseToolkitName() const override { return LOCTEXT("BaseToolKitName", "FSMAssetEditor"); }
#undef LOCTEXT_NAMESPACE 
	virtual FString GetWorldCentricTabPrefix() const override { return "GameplayCustomized"; }
	virtual FLinearColor GetWorldCentricTabColorScale() const override { return FLinearColor::White; }

至此,编辑器AssetEditor类的框架就差不多了,在之前的FAssetTypeAction_FSM中override以下虚函数:

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

实现:

void FAssetTypeAction_FSM::OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor)
{
	EToolkitMode::Type Mode = EditWithinLevelEditor.IsValid() ? EToolkitMode::WorldCentric : EToolkitMode::Standalone;

	for (auto Object : InObjects)
	{
		auto FSM = Cast<UFSM>(Object);
		if (FSM != nullptr)
		{
			TSharedRef<FFSMAssetEditor> Editor(new FFSMAssetEditor);
			Editor->InitializeAssetEditor(Mode, EditWithinLevelEditor, FSM);
		}
	}
}

这样双击资源就可以打开编辑器了,当然还什么都编辑不了就是了。

d1689e68f6e7a23ae49989e5e60e4ac8.png

接下来了解一下一个Node在蓝图中是如何包含信息并显示在Graph中的:

5583c3a4d0f0a7ae1ab56cd6e5459c51.png
我称之为Graph的USF(U类、S类、F类)关系图=)

在上图中提到的

virtual TSharedPtr<class SGraphNode> CreateNode(class UEdGraphNode* InNode) const override;

中如下实现:

TSharedPtr<class SGraphNode> FFSMGraphNodeFactory::CreateNode(class UEdGraphNode* InNode) const
{
	if (UFSMGraphNode* Node = Cast<UFSMGraphNode>(InNode))
	{
		return SNew(SFSMGraphNode, Node);
	}
	return nullptr;
}

就相当于利用这个FFSMGraphNodeFactory告诉了模块,每当我创建了一个UFSMGraphNode并将其添加到当前Editor中的Graph(后面会提到,在SchemaAction的Perform函数中利用FGraphNodeCreator类进行创建并添加),就用这个Factory创建一个SFSMGraphNode添加到Graph的Slate中的SGraphPanel中。当然,让这个Factory生效还需要将其注册一下:

// Create factories
GraphNodeFactory = MakeShared<FFSMGraphNodeFactory>();
FEdGraphUtilities::RegisterVisualNodeFactory(GraphNodeFactory);

将这两行添加到FFSMEditorModule::StartupModule()中即可。

其中,UFSMGraphNode只存了一个FName NodeName;变量。其实可以用Object的Name的,但是总觉得埋在引擎里的可操控程度不高,不够安心。

接下来设计其中的Node和Pin的外观。将其Slate写入到SFSMGraphNode::UpdateGraphNode()中。

由于参考的是动画蓝图状态机的设计,打开控件反射器查看状态Node的Slate结构

e96928db0d5fded8b6c048ec80979811.png

这里Slate就不贴上来了,太长了,而且由于是参考动画蓝图状态机的,可能会违反某些协议,咱也不懂,还是不乱粘贴了,有机会的话Github见。

Node显示的名字:我这里的做法是实现了virtual FText UFSMGraphNode::GetNodeTitle(ENodeTitleType::Type TitleType) const override,然后在SFSMGraphNode::UpdateGraphNode()中利用SNodeTitle来获取。

Pin、Connection Drawing Policy与之类似,GraphFactory变为继承自:

struct FSMEDITOR_API FFSMGraphPinFactory : public FGraphPanelPinFactory
{
public:
	virtual TSharedPtr<class SGraphPin> CreatePin(class UEdGraphPin* Pin) const override;
};

struct FSMEDITOR_API FFSMGraphPinConnectionFactory : public FGraphPanelPinConnectionFactory
{
public:
	virtual class FConnectionDrawingPolicy* CreateConnectionPolicy(const class UEdGraphSchema* Schema, int32 InBackLayerID, int32 InFrontLayerID, float ZoomFactor, const class FSlateRect& InClippingRect, class FSlateWindowElementList& InDrawElements, class UEdGraph* InGraphObj) const override;
};

不同的是Pin并不需要存储任何信息,只是外观上依附于Node,作为连接线的起点(OutputPin)和终点(InputPin),因此Pin不需要自定义U类,直接使用UEdGraphPin就可以。之所以会有很多不同种类的Pin,是因为他们的PinType不同,于是才有了比如Struct是蓝色节点、ClassType是紫色节点、Exec是白色三角等等。在Factory中做如下判断:

	if (Pin->PinType.PinCategory == UFSMGraphSchema::PC_Trans)
	{
		return SNew(SFSMGraphPin, Pin);
	}

	return nullptr;

来绘制自己的Pin控件。

而FFSMConnectionDrawingPolicy类会override很多虚函数,通过定义线的法线方向等等的方式来描述线的形状外观。


现在相当于做好了各种需要的样式,但是还不能在Graph中创建,现在在Graph中点击右键出现的Schema Action Menu还是一片空白,

6ca3ea9249c88bab92659a76342b4141.png

那我们接下来来做会用到的Schema Action。

首先做的肯定是创建一个Node。那么就创造一个功能是创建Node,叫做New Node,分类Category为Node的继承自FEdGraphSchemaAction的类

USTRUCT()
struct FFSMSchemaAction_NewNode : public FEdGraphSchemaAction

一旦点击菜单中的一项,就会调用FEdGraphSchemaAction的函数:

UEdGraphNode* PerformAction(UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, bool bSelectNewNode = true) override;

因此要实现这个函数,也就是把创建新Node的逻辑写在这里。

这里会用到一个模板类FGraphNodeCreator,这是这个类的注释:

/** 
 * Helper object to ensure a graph node is correctly constructed
 *
 * Typical use pattern is:
 * FNodeGraphNodeCreate<NodeType> NodeCreator(Graph);
 * NodeType* Node = NodeCreator.CreateNode();
 * // calls to build out node 
 * Node->MemberVar = ...
 * NodeCreator.Finalize
 */

如上所述,只要Perform被调用,FGraphNodeCreator创建一个UFSMGraphNode,那么注册好的FFSMGraphNodeFactory就会添加一个SFSMGraphNode到SGraphPanel,也就有一个节点被绘制了。

但是这个自定义的SchemaAction还没有被添加到菜单中,那么我们接下来新建一个自定义的ActionMenu菜单类,并把这个自定义的FFSMSchemaAction_NewNode添加进来。

在UFSMGraphSchema中实现虚函数:

virtual void GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const override;

注:如果是从Pin拉出来引线而产生的菜单,那么ContextMenuBuilder.FromPin就会是对应的UEdGraphPin,否则就是nullptr

在这个函数的实现里

TSharedPtr<FFSMSchemaAction_NewComment> NewAction(new FFSMSchemaAction_NewComment(GRAPH_CATEGORY, MenuDescription, ToolTip, Grouping));
ContextMenuBuilder.AddAction(NewAction);

就可以将FFSMSchemaAction_NewComment添加到菜单中。

至于菜单项,自己创建一个SFSMActionMenu(继承自SBorder)抑或是直接用SGraphEditorMenu都可以,我这里自定义了一个SFSMActionMenu封装了一下,和SGraphEditorMenu并没有什么区别,不再多说。

再在之前提到的留白的OnCreateGraphActionMenu中实现

FActionMenuContent FFSMAssetEditor::OnCreateGraphActionMenu(UEdGraph* Graph, const FVector2D& NodePosition,
	const TArray<UEdGraphPin*>& DraggedPins, bool bAutoExpand, SGraphEditor::FActionMenuClosed OnMenuClosed)
{
    const TSharedRef<SFSMActionMenu> ActionMenu = SNew(SFSMActionMenu)
        //Assign slate args here
    ;
    return FActionMenuContent(ActionMenu, ActionMenu->GetFilterTextBox());
}

好了,至此,SchemaActionMenu就已经创建好了。再如法炮制添加一个自定义的创建Comment的FFSMSchemaAction_NewComment。

9bba837cdca3bfc8c87b3eb46d11ec2e.png

点击New Node和Add Comment后:

2698dd36113ad7c3cfa3465e53fef6e0.png
别问我为什么边上一圈的Pin是蓝色的,快乐

现在UFSMGraphNode和UFSM还没有建立起联系,接下来要做的就是在New Node的同时在对应的UFSM对象,也就是自定义的状态机数据结构中添加一个名字为UFSMGraphNode::NodeName(之前我画的那个USF关系图中提到过这个FName成员变量)的状态。


接下来该做的是TransitionNode,他应该是像动画蓝图状态机中的那个圆形转换图标一样在连线上的,同时一根线上可以同时放置很多个,只要从状态A重复拉线到状态B就可以了。

首先希望连线后创建TransitionNode,我(其实也是Animation)的做法是UFSMGraphSchema::CanCreateConnection中返回的不是CONNECT_RESPONSE_MAKE而是CONNECT_RESPONSE_MAKE_WITH_CONVERSION_NODE

这样就可以在UFSMGraphSchema::CreateAutomaticConversionNodeAndConnections中创建TransitionNode,并将前后两个Node都与之相连。


节点上右键菜单需要override:

void UFSMGraphSchema::GetContextMenuActions(const UEdGraph * CurrentGraph, const UEdGraphNode * InGraphNode, const UEdGraphPin * InGraphPin, FMenuBuilder * MenuBuilder, bool bIsDebugging) const
{
	check(CurrentGraph);

	if (InGraphNode != NULL)
	{
		MenuBuilder->BeginSection("FSMNodeActions", LOCTEXT("NodeActionsMenuHeader", "Node Actions"));
		{
			if (!bIsDebugging)
			{
				// Node contextual actions
				MenuBuilder->AddMenuEntry(FGenericCommands::Get().Delete);
				//These three are not allowed
				//MenuBuilder->AddMenuEntry(FGenericCommands::Get().Cut);
				//MenuBuilder->AddMenuEntry(FGenericCommands::Get().Copy);
				//MenuBuilder->AddMenuEntry(FGenericCommands::Get().Duplicate);
				MenuBuilder->AddMenuEntry(FGraphEditorCommands::Get().ReconstructNodes);
				MenuBuilder->AddMenuEntry(FGraphEditorCommands::Get().BreakNodeLinks);
				if (InGraphNode->bCanRenameNode)
				{
					MenuBuilder->AddMenuEntry(FGenericCommands::Get().Rename);
				}
			}
		}
		MenuBuilder->EndSection();
	}

	Super::GetContextMenuActions(CurrentGraph, InGraphNode, InGraphPin, MenuBuilder, bIsDebugging);
}

之后在AssetEditor之前写过的CreateGraphEditorWidget中添加:(由于太长,vs中我用region缩起来了)

#pragma region GraphEditorCommands
	// No need to regenerate the commands.
	if (!GraphEditorCommands.IsValid())
	{
		GraphEditorCommands = MakeShareable(new FUICommandList);
		{

			GraphEditorCommands->MapAction(FGenericCommands::Get().Rename,
				FExecuteAction::CreateSP(this, &FFSMAssetEditor::OnRenameNode),
				FCanExecuteAction::CreateSP(this, &FFSMAssetEditor::CanRenameNodes)
			);

			// Editing commands
			GraphEditorCommands->MapAction(FGenericCommands::Get().SelectAll,
				FExecuteAction::CreateSP(this, &FFSMAssetEditor::SelectAllNodes),
				FCanExecuteAction::CreateSP(this, &FFSMAssetEditor::CanSelectAllNodes)
			);

			GraphEditorCommands->MapAction(FGenericCommands::Get().Delete,
				FExecuteAction::CreateSP(this, &FFSMAssetEditor::DeleteSelectedNodes),
				FCanExecuteAction::CreateSP(this, &FFSMAssetEditor::CanDeleteNodes)
			);

			//I don't think it's allowed to cut/copy/paste
			/*GraphEditorCommands->MapAction(FGenericCommands::Get().Copy,
				FExecuteAction::CreateSP(this, &FFSMAssetEditor::CopySelectedNodes),
				FCanExecuteAction::CreateSP(this, &FFSMAssetEditor::CanCopyNodes)
			);

			GraphEditorCommands->MapAction(FGenericCommands::Get().Cut,
				FExecuteAction::CreateSP(this, &FFSMAssetEditor::CutSelectedNodes),
				FCanExecuteAction::CreateSP(this, &FFSMAssetEditor::CanCutNodes)
			);

			GraphEditorCommands->MapAction(FGenericCommands::Get().Paste,
				FExecuteAction::CreateSP(this, &FFSMAssetEditor::PasteNodes),
				FCanExecuteAction::CreateSP(this, &FFSMAssetEditor::CanPasteNodes)
			);

			GraphEditorCommands->MapAction(FGenericCommands::Get().Duplicate,
				FExecuteAction::CreateSP(this, &FFSMAssetEditor::DuplicateNodes),
				FCanExecuteAction::CreateSP(this, &FFSMAssetEditor::CanDuplicateNodes)
			);*/

			GraphEditorCommands->MapAction(FGraphEditorCommands::Get().CreateComment,
				FExecuteAction::CreateSP(this, &FFSMAssetEditor::OnCreateComment)
			);

			//override for append command
			//OnCreateGraphEditorCommands(GraphEditorCommands);
		}
	}

	// Append play world commands
	//GraphEditorCommands->Append(FPlayWorldCommands::GlobalPlayWorldActions.ToSharedRef());
#pragma endregion 

然后记得在return的SNew后面

		.AdditionalCommands(GraphEditorCommands)

再分别实现上述CreateSP中提到的函数就行了。


NameValidator的主要作用是限制命名,防止出现非法字符或重复名称,这个我觉得没啥写的必要了。


Entry Node和普通Node很相似,有以下几点不同:

1.连接选择CONNECT_RESPONSE_BREAK_OTHERS_A;

2.在Graph Factory中添加Pin的样式,Pin的样式我选择了SGraphPinExec,也就是蓝图中常见的白色三角形;

3.DrawingPolicy中如果对于连接的两个Node类型有判断,别忘记在DetermineLinkGeometry加入EntryNode类型的判断;

4.记得连接后修改FSM中的EntryState成员变量。

常见问题:

1.Q:关闭AssetEditor之后再打开图表是空的。

A:我的做法是UFSM里面存了个UEdGraph,FFSMAssetEditor::CreateGraphEditorWidget里判断一下,UFSM里没有UEdGraph再Create,有就直接用。

2.Q:再次打开UE4后图表变了,比如名字全变成None 或者 连接线全断了

A:这是由于某些必要成员变量没有标上UPROPERTY()导致其没有序列化,下次打开自然就是默认值。


有什么问题欢迎提问,接下来随缘补充更新。

参考:

hanbingzhipo:UE4项目记录(十一)自定义资源及​zhuanlan.zhihu.com
自定义基本UE4编辑器 - Free communication​www.freeexc.me
b32b2f9deb1b3cbc7a96a3cca7fb8a22.png
https://forums.unrealengine.com/community/community-content-tools-and-tutorials/1424923-open-source-dialogue-system-plugin​forums.unrealengine.com Writing a Custom Asset Editor for Unreal Engine 4 - Part 1​cairansteverink.nl Creating Custom Editor Assets | Orfeas Eleftheriou​orfeasel.com
31e633b815b833d78f78c1e51c5a829f.png
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值