📜 ⬆️ ⬇️

Creating a system of placing objects by level using the blueprint editor

image

Hello, my name is Dmitry. I create computer games on the Unreal Engine as a hobby. For my project, I am developing a procedurally generated level. My algorithm puts points in space in a certain order (which I call the roots “roots”), after which I attach meshes to these points. But here there is a problem in that you need to attach the mesh from the beginning, then compile the project, and after that you can see how it got up. It is natural to constantly run from the editor window to the VS window for a very long time. And I thought that it would be possible for this to use the blueprint editor, the more I caught the eye of the Dungeon architect plugin, in which the placement of objects by level is implemented via blueprint. Actually here I will talk about the creation of such a system, a screenshot of which is shown in the first picture.


So from the beginning we will create our own file type (you can see this article in more detail). In the AssetAction class, we override the OpenAssetEditor function.
void FMyObjectAssetAction::OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor) { const EToolkitMode::Type Mode = EditWithinLevelEditor.IsValid() ? EToolkitMode::WorldCentric : EToolkitMode::Standalone; for (auto ObjIt = InObjects.CreateConstIterator(); ObjIt; ++ObjIt) { UMyObject* PropData = Cast<UMyObject>(*ObjIt); if (PropData) { TSharedRef<FCustAssetEditor> NewCustEditor(new FCustAssetEditor()); NewCustEditor->InitCustAssetEditor(Mode, EditWithinLevelEditor, PropData); } } } 

')
Now, if we try to open this file, it will open not the usual window, but the window that we define in the class FCustAssetEditor.
 class FCustAssetEditor : public FAssetEditorToolkit, public FNotifyHook { public: ~FCustAssetEditor(); // IToolkit interface virtual void RegisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override; virtual void UnregisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) override; // FAssetEditorToolkit virtual FName GetToolkitFName() const override; virtual FText GetBaseToolkitName() const override; virtual FLinearColor GetWorldCentricTabColorScale() const override; virtual FString GetWorldCentricTabPrefix() const override; void InitCustAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr< class IToolkitHost >& InitToolkitHost, UMyObject* PropData); int N; protected: void OnGraphChanged(const FEdGraphEditAction& Action); void SelectAllNodes(); bool CanSelectAllNodes() const; void DeleteSelectedNodes(); bool CanDeleteNode(class UEdGraphNode* Node); bool CanDeleteNodes() const; void DeleteNodes(const TArray<class UEdGraphNode*>& NodesToDelete); void CopySelectedNodes(); bool CanCopyNodes() const; void PasteNodes(); void PasteNodesHere(const FVector2D& Location); bool CanPasteNodes() const; void CutSelectedNodes(); bool CanCutNodes() const; void DuplicateNodes(); bool CanDuplicateNodes() const; void DeleteSelectedDuplicatableNodes(); /** Called when the selection changes in the GraphEditor */ void OnSelectedNodesChanged(const TSet<class UObject*>& NewSelection); /** Called when a node is double clicked */ void OnNodeDoubleClicked(class UEdGraphNode* Node); void ShowMessage(); TSharedRef<class SGraphEditor> CreateGraphEditorWidget(UEdGraph* InGraph); TSharedPtr<SGraphEditor> GraphEditor; TSharedPtr<FUICommandList> GraphEditorCommands; TSharedPtr<IDetailsView> PropertyEditor; UMyObject* PropBeingEdited; TSharedRef<SDockTab> SpawnTab_Viewport(const FSpawnTabArgs& Args); TSharedRef<SDockTab> SpawnTab_Details(const FSpawnTabArgs& Args); FDelegateHandle OnGraphChangedDelegateHandle; TSharedPtr<FExtender> ToolbarExtender; TSharedPtr<FUICommandList> MyToolBarCommands; bool bGraphStateChanged; void AddToolbarExtension(FToolBarBuilder &builder); }; 

The most important method for us of this class is InitCustAssetEditor. First, this method creates a new editor about which below, then it creates two new empty tabs:
 const TSharedRef<FTabManager::FLayout> StandaloneDefaultLayout = FTabManager::NewLayout("CustomEditor_Layout") ->AddArea ( FTabManager::NewPrimaryArea() ->SetOrientation(Orient_Vertical) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.1f) ->SetHideTabWell(true) ->AddTab(GetToolbarTabId(), ETabState::OpenedTab) ) ->Split ( FTabManager::NewSplitter() ->SetOrientation(Orient_Horizontal) ->SetSizeCoefficient(0.2f) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.8f) ->SetHideTabWell(true) ->AddTab(FCustomEditorTabs::ViewportID, ETabState::OpenedTab) ) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.2f) ->SetHideTabWell(true) ->AddTab(FCustomEditorTabs::DetailsID, ETabState::OpenedTab) ) ) ); 

One of these tabs will be the tab of our blueprint editor, and the second is needed to display the properties of the nodes. The actual tabs created need to fill them with something. Fills tabs with the contents of the RegisterTabSpawners method
 void FCustAssetEditor::RegisterTabSpawners(const TSharedRef<class FTabManager>& TabManager) { WorkspaceMenuCategory = TabManager->AddLocalWorkspaceMenuCategory(FText::FromString("Custom Editor")); auto WorkspaceMenuCategoryRef = WorkspaceMenuCategory.ToSharedRef(); FAssetEditorToolkit::RegisterTabSpawners(TabManager); TabManager->RegisterTabSpawner(FCustomEditorTabs::ViewportID, FOnSpawnTab::CreateSP(this, &FCustAssetEditor::SpawnTab_Viewport)) .SetDisplayName(FText::FromString("Viewport")) .SetGroup(WorkspaceMenuCategoryRef) .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tabs.Viewports")); TabManager->RegisterTabSpawner(FCustomEditorTabs::DetailsID, FOnSpawnTab::CreateSP(this, &FCustAssetEditor::SpawnTab_Details)) .SetDisplayName(FText::FromString("Details")) .SetGroup(WorkspaceMenuCategoryRef) .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tabs.Details")); } TSharedRef<SDockTab> FCustAssetEditor::SpawnTab_Viewport(const FSpawnTabArgs& Args) { return SNew(SDockTab) .Label(FText::FromString("Mesh Graph")) .TabColorScale(GetTabColorScale()) [ GraphEditor.ToSharedRef() ]; } TSharedRef<SDockTab> FCustAssetEditor::SpawnTab_Details(const FSpawnTabArgs& Args) { FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor"); const FDetailsViewArgs DetailsViewArgs(false, false, true, FDetailsViewArgs::HideNameArea, true, this); TSharedRef<IDetailsView> PropertyEditorRef = PropertyEditorModule.CreateDetailView(DetailsViewArgs); PropertyEditor = PropertyEditorRef; // Spawn the tab return SNew(SDockTab) .Label(FText::FromString("Details")) [ PropertyEditorRef ]; } 

The properties panel will suit us the standard one, but we will create our own bluprin editor. It is created in the CreateGraphEditorWidget method.
 TSharedRef<SGraphEditor> FCustAssetEditor::CreateGraphEditorWidget(UEdGraph* InGraph) { // Create the appearance info FGraphAppearanceInfo AppearanceInfo; AppearanceInfo.CornerText = FText::FromString("Mesh tree Editor"); GraphEditorCommands = MakeShareable(new FUICommandList); { GraphEditorCommands->MapAction(FGenericCommands::Get().SelectAll, FExecuteAction::CreateSP(this, &FCustAssetEditor::SelectAllNodes), FCanExecuteAction::CreateSP(this, &FCustAssetEditor::CanSelectAllNodes) ); GraphEditorCommands->MapAction(FGenericCommands::Get().Delete, FExecuteAction::CreateSP(this, &FCustAssetEditor::DeleteSelectedNodes), FCanExecuteAction::CreateSP(this, &FCustAssetEditor::CanDeleteNodes) ); GraphEditorCommands->MapAction(FGenericCommands::Get().Copy, FExecuteAction::CreateSP(this, &FCustAssetEditor::CopySelectedNodes), FCanExecuteAction::CreateSP(this, &FCustAssetEditor::CanCopyNodes) ); GraphEditorCommands->MapAction(FGenericCommands::Get().Paste, FExecuteAction::CreateSP(this, &FCustAssetEditor::PasteNodes), FCanExecuteAction::CreateSP(this, &FCustAssetEditor::CanPasteNodes) ); GraphEditorCommands->MapAction(FGenericCommands::Get().Cut, FExecuteAction::CreateSP(this, &FCustAssetEditor::CutSelectedNodes), FCanExecuteAction::CreateSP(this, &FCustAssetEditor::CanCutNodes) ); GraphEditorCommands->MapAction(FGenericCommands::Get().Duplicate, FExecuteAction::CreateSP(this, &FCustAssetEditor::DuplicateNodes), FCanExecuteAction::CreateSP(this, &FCustAssetEditor::CanDuplicateNodes) ); } SGraphEditor::FGraphEditorEvents InEvents; InEvents.OnSelectionChanged = SGraphEditor::FOnSelectionChanged::CreateSP(this, &FCustAssetEditor::OnSelectedNodesChanged); InEvents.OnNodeDoubleClicked = FSingleNodeEvent::CreateSP(this, &FCustAssetEditor::OnNodeDoubleClicked); TSharedRef<SGraphEditor> _GraphEditor = SNew(SGraphEditor) .AdditionalCommands(GraphEditorCommands) .Appearance(AppearanceInfo) .GraphToEdit(InGraph) .GraphEvents(InEvents) ; return _GraphEditor; } 

Here, from the beginning, actions and events are defined to which our editor will respond, and then the editor widget itself is created. The most interesting parameter is .GraphToEdit (InGraph); it passes a pointer to the class UEdGraphSchema_CustomEditor
 UCLASS() class UEdGraphSchema_CustomEditor : public UEdGraphSchema { GENERATED_UCLASS_BODY() // Begin EdGraphSchema interface virtual void GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const override; virtual void GetContextMenuActions(const UEdGraph* CurrentGraph, const UEdGraphNode* InGraphNode, const UEdGraphPin* InGraphPin, FMenuBuilder* MenuBuilder, bool bIsDebugging) const override; virtual const FPinConnectionResponse CanCreateConnection(const UEdGraphPin* A, const UEdGraphPin* B) const override; virtual class FConnectionDrawingPolicy* CreateConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float InZoomFactor, const FSlateRect& InClippingRect, class FSlateWindowElementList& InDrawElements, class UEdGraph* InGraphObj) const override; virtual FLinearColor GetPinTypeColor(const FEdGraphPinType& PinType) const override; virtual bool ShouldHidePinDefaultValue(UEdGraphPin* Pin) const override; // End EdGraphSchema interface }; 

This class defines such things as the context menu items of the editor, determines how nodes will be connected, etc. For us, the most important thing is the ability to create your own nodes. This is done in the GetGraphContextActions method.
 void UEdGraphSchema_CustomEditor::GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const { FFormatNamedArguments Args; const FName AttrName("Attributes"); Args.Add(TEXT("Attribute"), FText::FromName(AttrName)); const UEdGraphPin* FromPin = ContextMenuBuilder.FromPin; const UEdGraph* Graph = ContextMenuBuilder.CurrentGraph; TArray<TSharedPtr<FEdGraphSchemaAction> > Actions; CustomSchemaUtils::AddAction<URootNode>(TEXT("Add Root Node"), TEXT("Add root node to the prop graph"), Actions, ContextMenuBuilder.OwnerOfTemporaries); CustomSchemaUtils::AddAction<UBranchNode>(TEXT("Add Brunch Node"), TEXT("Add brunch node to the prop graph"), Actions, ContextMenuBuilder.OwnerOfTemporaries); CustomSchemaUtils::AddAction<URuleNode>(TEXT("Add Rule Node"), TEXT("Add ruleh node to the prop graph"), Actions, ContextMenuBuilder.OwnerOfTemporaries); CustomSchemaUtils::AddAction<USwitcherNode>(TEXT("Add Switch Node"), TEXT("Add switch node to the prop graph"), Actions, ContextMenuBuilder.OwnerOfTemporaries); for (TSharedPtr<FEdGraphSchemaAction> Action : Actions) { ContextMenuBuilder.AddAction(Action); } } 


As you can see, so far I have created only four nodes in the list:
1) The URootNode node is the display of the element root on the graph. URootNode as well as elements of type root have type.
2) The UBranchNode node this node places at the static level of the mesh (so far only the meshes, but you can easily create the nodes for other furnishing elements or characters)
3) URuleNode node This node can be either open or closed depending on the specified condition. The condition is naturally set in blueprint.
4) The USwitcherNode node This node has one input and two outputs, depending on the condition, can open either the right output or the left one.

So far, only four nodes but if you have any ideas you can write them in the comments. Let's see how they work. (To save space, I will give here only the base class for them, the source code can be downloaded from the link at the end of the article)
 UCLASS() class UICUSTOM_API UCustomNodeBase : public UEdGraphNode { GENERATED_BODY() public: virtual TArray<UCustomNodeBase*> GetChildNodes(FRandomStream& RandomStream); virtual void CreateNodesMesh(UWorld* World, FName ActorTag, FRandomStream& RandomStream, FVector AbsLocation, FRotator AbsRotation); virtual void PostEditChangeProperty(struct FPropertyChangedEvent& e) override; TSharedPtr<FNodePropertyObserver> PropertyObserver; FVector Location; FRotator Rotation; }; 


Here we see the GetChildNodes method in which the node passes an array of objects attached to its outputs. And the CreateNodesMesh method in which the node creates a mesh or does not create it, but simply passes on the AbsLocation and AbsRotation values. The PostEditChangeProperty method as you probably guessed is executed when someone changes the properties of the node.

But as you probably noticed the nodes on the title picture differ in appearance from those that we used to see. How to achieve this. For this you need to create for each node the heir of SGraphNode. Like last time, here I will give only the base class.

 class SGraphNode_CustomNodeBase : public SGraphNode, public FNodePropertyObserver { public: SLATE_BEGIN_ARGS(SGraphNode_CustomNodeBase) { } SLATE_END_ARGS() /** Constructs this widget with InArgs */ void Construct(const FArguments& InArgs, UCustomNodeBase* InNode); // SGraphNode interface virtual void UpdateGraphNode() override; virtual void CreatePinWidgets() override; virtual void AddPin(const TSharedRef<SGraphPin>& PinToAdd) override; virtual void CreateNodeWidget(); // End of SGraphNode interface // FPropertyObserver interface virtual void OnPropertyChanged(UEdGraphNode* Sender, const FName& PropertyName) override; // End of FPropertyObserver interface protected: UCustomNodeBase* NodeBace; virtual FSlateColor GetBorderBackgroundColor() const; virtual const FSlateBrush* GetNameIcon() const; TSharedPtr<SHorizontalBox> OutputPinBox; FLinearColor BackgroundColor; TSharedPtr<SOverlay> NodeWiget; }; 


The inheritance of the FNodePropertyObserver class is required solely for the OnPropertyChanged method. The most important method is the UpdateGraphNode method in it and the widget that we see on the screen is created. The remaining methods are called from it to create certain parts of this widget.

Please do not confuse the SGraphNode class with the UEdGraphNode class. SGraphNode defines solely the appearance of the node, while the UEdGraphNode class defines the properties of the node itself.

But even now if you run the project, the nodes will have the same look. For appearance changes to take effect, you need to register them. Where to do it? Of course, at the start of the module:
 void FUICustomEditorModule::StartupModule() { //Registrate asset actions for MyObject FMyObjectAssetAction::RegistrateCustomPartAssetType(); //Registrate detail pannel costamization for TestActor FMyClassDetails::RegestrateCostumization(); // Register custom graph nodes TSharedPtr<FGraphPanelNodeFactory> GraphPanelNodeFactory = MakeShareable(new FGraphPanelNodeFactory_Custom); FEdGraphUtilities::RegisterVisualNodeFactory(GraphPanelNodeFactory); //Registrate ToolBarCommand for costom graph FToolBarCommandsCommands::Register(); //Create pool for icon wich show on costom nodes FCustomEditorThumbnailPool::Create(); } 

I want to note that a storage is also created here, for storing icons that will be displayed on the UBranchNode nodes. Nodes are registered in the CreateNode method of the FGraphPanelNodeFactory_Custom class.
 TSharedPtr<class SGraphNode> FGraphPanelNodeFactory_Custom::CreateNode(UEdGraphNode* Node) const { if (URootNode* RootNode = Cast<URootNode>(Node)) { TSharedPtr<SGraphNode_Root> SNode = SNew(SGraphNode_Root, RootNode); RootNode->PropertyObserver = SNode; return SNode; } else if (UBranchNode* BranchNode = Cast<UBranchNode>(Node)) { TSharedPtr<SGraphNode_Brunch> SNode = SNew(SGraphNode_Brunch, BranchNode); BranchNode->PropertyObserver = SNode; return SNode; } else if (URuleNode* RuleNode = Cast<URuleNode>(Node)) { TSharedPtr<SGraphNode_Rule> SNode = SNew(SGraphNode_Rule, RuleNode); RuleNode->PropertyObserver = SNode; return SNode; } else if (USwitcherNode* SwitcherNode = Cast<USwitcherNode>(Node)) { TSharedPtr<SGraphNode_Switcher> SNode = SNew(SGraphNode_Switcher, SwitcherNode); SwitcherNode->PropertyObserver = SNode; return SNode; } return NULL; } 


Generation is performed in the TestActor class.
 bool ATestAct::GenerateMeshes() { FRandomStream RandomStream = FRandomStream(10); if (!MyObject) { return false; } for (int i = 0; i < Roots.Num(); i++) { URootNode* RootBuf; RootBuf = MyObject->FindRootFromType(Roots[i].RootType); if (RootBuf) { RootBuf->CreateNodesMesh(GetWorld(), ActorTag, RandomStream, Roots[i].Location, FRotator(0, 0, 0)); } } return true; } 

Here we loop through all the root objects, each of them is characterized by a coordinate in space and a type. Having received this object, we are looking for in the graph the node URootNode of the same type. Having found it, we pass it the initial coordinates and run the method CreateNodesMesh which will go through the chain through the entire graph. We do this until all root objects are processed.

Actually that's all. For further reference I recommend watching the source.

Source code project here

In the meantime, I will tell you how this farm works. The generation is carried out in the TestActor object, from the beginning it is necessary to manually set the positions and types of root objects (and what you would like a training project).
image
After that, select in the properties the file MyObject, in which we must build a graph that determines which meshes will be created.

So, how to set the rule for the rule node and switcher. To do this, click the plus sign in the properties to create a new blueprint.
image
But it turns out to be empty to do next? You need to click Override NodeBool.
image
Now you can either open or close the node.
image
Everything is the same for switchera. The Brunch node has the same rule for specifying the coordinates and rotation. In addition, it has a way out, which means if you attach another Brunch to it, then it will use the coordinate of the previous one as a reference.

It remains only to click the Generate Meshes button on the TestActor properties panel, and enjoy the result.
image

Hope you enjoyed this article. It turned out to be much longer than before, I was afraid that I would not finish it until the end.

PS After I wrote the article, I tried to assemble the game and it did not. In order for the game to be collected it is necessary to make the following corrections in the CustomNods.h file:
 class UICUSTOM_API UCustomNodeBase : public UEdGraphNode { GENERATED_BODY() public: virtual TArray<UCustomNodeBase*> GetChildNodes(FRandomStream& RandomStream); virtual void CreateNodesMesh(UWorld* World, FName ActorTag, FRandomStream& RandomStream, FVector AbsLocation, FRotator AbsRotation); #if WITH_EDITORONLY_DATA virtual void PostEditChangeProperty(struct FPropertyChangedEvent& e) override; #endif //WITH_EDITORONLY_DATA TSharedPtr<FNodePropertyObserver> PropertyObserver; }; 

That is, we must exclude all functions except GetChildNodes and CreateNodesMesh from the node class using the #if WITH_EDITORONLY_DATA operator. In the remaining nodes, you must do the same.

And accordingly CustomNods.cpp:

 TArray<UCustomNodeBase*> UCustomNodeBase::GetChildNodes(FRandomStream& RandomStream) { TArray<UCustomNodeBase*> ChildNodes; return ChildNodes; } void UCustomNodeBase::CreateNodesMesh(UWorld* World, FName ActorTag, FRandomStream& RandomStream, FVector AbsLocation, FRotator AbsRotation) { TArray<UCustomNodeBase*>ChailNodes = GetChildNodes(RandomStream); for (int i = 0; i < ChailNodes.Num(); i++) { ChailNodes[i]->CreateNodesMesh(World, ActorTag, RandomStream, AbsLocation, AbsRotation); } } #if WITH_EDITORONLY_DATA void UCustomNodeBase::PostEditChangeProperty(struct FPropertyChangedEvent& e) { if (PropertyObserver.IsValid()) { FName PropertyName = (e.Property != NULL) ? e.Property->GetFName() : NAME_None; PropertyObserver->OnPropertyChanged(this, PropertyName); } Super::PostEditChangeProperty(e); } #endif //WITH_EDITORONLY_DATA 


If you have already downloaded the project file, please transfer it again.

PPS Continued

Source: https://habr.com/ru/post/277515/


All Articles