Can unreal do custom editor windows to display debug info?

Hi everyone,
Right now I am looking into porting a project of mine from Unity to UnrealEngine.
I have something that I can do in Unity that I am not sure I can do in unreal Engine: custom editor windows.

Let me explain what I mean by that: My project uses a planner to determine NPC behaviours that returns results as a tree structure. It is paramount that I can visualize what the tree structure looks like at a given time ( for debug reasons ).

So In unity I have implented a custom editor window in which I display the tree structure ( see below ). To do this in Unity it is very easy.

Is there a way to do that in Unreal 4 ?
Note: I am running U4 from Visual Studio so a c++ solution is no problem at all.

Hey ,

Have you taken a look through Behavior Trees? If not, please follow this [link][1] and it’ll get you started with understanding and constructing a behavior tree.

Hope it helps! :slight_smile:

https://docs.unrealengine.com/latest/INT/Engine/AI/BehaviorTrees/QuickStart/

May I ask why this has been marked as resolved?
my question has nothing to do with behavior trees, BTs and Planners are two different things.

Also the question does not lie there, my question is: how can I do a custom window in the editor to display debug info (info may be from a planner or not).

Humm… Not marked as resolved anymore I have difficulty understanding how this system works… is it because I posted a comment?

Actually what I am looking for is an equivalent in Unreal for Unity EditorWindow c# class that you can extend and define your own editor windows with movable item and text and buttons etc.

Staff posts are marked as answer by default.
Comments re-opens an answered topic; two problems they should fix.

1 Like

Haa I see thanks for the info!

Ofcorse you can, your C++ module is no different from other engine modules, you practically extending engine code, so you can extend editor as other modules does. First, you need to create a new module, seperate from gameplay one, and build script make it so it only compiles when you build for editor use:

Then you need to learning now to use Slate, UE’s window UI system that editor uses (and you can use it in game too, in fact UMG use it), here you got docs about it:

In APIs all classes related to slate has “S” prefix, here you got API refrence of Slate:

Next, you need to figure how to create window in editor… and this is the tricky part as it’s not officailly documented, you will need to explore engine source code to see how other features been implmented. Each editor window is usally coded in single module, here you have Scene Outliner:

https://github.com/EpicGames/UnrealEngine/tree/c9f4efe690de8b3b72a11223865c623ca0ee7086/Engine/Source/Editor/SceneOutliner/Public

or Class Viewer:

https://github.com/EpicGames/UnrealEngine/tree/c9f4efe690de8b3b72a11223865c623ca0ee7086/Engine/Source/Editor/ClassViewer

In breath look it seems you need to extend SCompoundWidget to create window.

Also check out some tutorials, there was stream recently about editor extending by (i need to watch it too! :):

Also i find this how to add toolbar item:

That is a very nice answer thanks! I will have a look at all of this later next week. It relieves me to know that it is possible in practice.

Okay so with he help of and his tutorial Twitch I finally managed to create a new tabbed window with a horizontal tree, pretty nice!
It is possible to make a vertical tree but more code is needed.

For the benefit of the community here is the source code:

YaraGameEditor.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "Engine.h"
#include "ModuleInterface.h"
#include "EdGraphUtilities.h"

class YaraGameEditorModule : public IModuleInterface
{

public:
	YaraGameEditorModule();

private:
	// IModuleInterface
	void StartupModule() override;
	void ShutdownModule() override;

	// Member functions
	static void InvokePlannerDebugger(UClass * Class);
	TSharedRef<SDockTab> SpawnPlannerViewerTab(const FSpawnTabArgs& SpawnTabArgs);

	static FName m_PlannerViewerTabId;
	TSharedPtr< FGraphPanelNodeFactory > m_GraphPanelNodeFactory;
};

YaraGameEditor.cpp

// Fill out your copyright notice in the Description page of Project Settings.
#include "YaraGameEditorPCH.h"
#include "YaraGameEditor.h"
#include "LevelEditor.h"
#include "PropertyEditorModule.h"
#include "MultiBoxExtender.h"
#include "PlannerDebugger.h"
#include "PlannerViewer.h"
#include "SDockTab.h"
#include "PlannerNode.h"
#include "EdGraphNode_PlannerDebugger.h"

#define LOCTEXT_NAMESPACE "YaraGameEditorModule"

FName YaraGameEditorModule::m_PlannerViewerTabId = "PlannerViewer";


class FGraphPanelNodeFactory_PlannerDebugger : public FGraphPanelNodeFactory
{
	virtual TSharedPtr< class SGraphNode > CreateNode( UEdGraphNode* Node ) const override
	{
		if ( UEdGraphNode_PlannerDebugger* DependencyNode = Cast< UEdGraphNode_PlannerDebugger >( Node ) )
		{
			return SNew( UPlannerNode, DependencyNode );
		}

		return NULL;
	}
};

YaraGameEditorModule::YaraGameEditorModule()
{
}

void YaraGameEditorModule::StartupModule()
{
	FLevelEditorModule & LevelEditorModule = FModuleManager::LoadModuleChecked< FLevelEditorModule >( TEXT("LevelEditor") );
	struct Local
	{
		static void AddMenuCommands( FMenuBuilder & MenuBuilder)
		{
			FString FriendlyName	= "Planner Debugger";
			FText MenuDescription	= FText::Format( LOCTEXT( "ToolMenuDescription", "{0}" ), FText::FromString( FriendlyName ) );
			FText MenuToolTip		= FText::Format( LOCTEXT( "ToolMenuToolTip", "Execute the {0} tool" ), FText::FromString( FriendlyName ) );

			FUIAction Action(FExecuteAction::CreateStatic( &YaraGameEditorModule::InvokePlannerDebugger, UPlannerDebugger::StaticClass() ) );
			MenuBuilder.AddMenuEntry( MenuDescription, MenuToolTip, FSlateIcon(), Action );
		}
	};

	TSharedRef< FExtender > MenuExtender( new FExtender() );
	MenuExtender->AddMenuExtension( "LevelEditor", EExtensionHook::After, nullptr, FMenuExtensionDelegate::CreateStatic( &Local::AddMenuCommands ) );
	LevelEditorModule.GetMenuExtensibilityManager()->AddExtender( MenuExtender );

	// This overrides the node creation process and enables to make the link between the node and the slate representation of the node
	m_GraphPanelNodeFactory = MakeShareable( new FGraphPanelNodeFactory_PlannerDebugger() );
	FEdGraphUtilities::RegisterVisualNodeFactory(m_GraphPanelNodeFactory);

	FGlobalTabmanager::Get()->RegisterNomadTabSpawner(m_PlannerViewerTabId, FOnSpawnTab::CreateRaw(this, &YaraGameEditorModule::SpawnPlannerViewerTab))
		.SetDisplayName( LOCTEXT( "PlannerDebuggerTitle", "Planner Debugger" ) )
		.SetMenuType( ETabSpawnerMenuType::Hidden );
}

void YaraGameEditorModule::InvokePlannerDebugger( UClass * ToolClass )
{
	TSharedRef< SDockTab > DockTabRef			= FGlobalTabmanager::Get()->InvokeTab( m_PlannerViewerTabId );
	TSharedRef< UPlannerViewer > PlannerViewer	= StaticCastSharedRef< UPlannerViewer >( DockTabRef->GetContent() );
}

TSharedRef<SDockTab> YaraGameEditorModule::SpawnPlannerViewerTab( const FSpawnTabArgs& SpawnTabArgs )
{
	TSharedRef<SDockTab> NewTab = SNew( SDockTab )
		.TabRole( ETabRole::NomadTab );

	NewTab->SetContent( SNew( UPlannerViewer ) );

	return NewTab;
}

void YaraGameEditorModule::ShutdownModule()
{
	if ( !UObjectInitialized() )
	{
		return;
	}

	if ( m_GraphPanelNodeFactory.IsValid() )
	{
		FEdGraphUtilities::UnregisterVisualNodeFactory( m_GraphPanelNodeFactory );
		m_GraphPanelNodeFactory.Reset();
	}
}

IMPLEMENT_GAME_MODULE( YaraGameEditorModule, YaraGameEditor );

#undef LOCTEXT_NAMESPACE

PlannerViewer.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "Engine.h"
#include "GraphEditor.h"

class UPlannerDebugger;

class UPlannerViewer : public SCompoundWidget
{
public:
	SLATE_BEGIN_ARGS( UPlannerViewer ){}

	SLATE_END_ARGS()

	~UPlannerViewer();

	/** Constructs this widget with InArgs */
	void Construct(const FArguments& InArgs);

	// SWidget implementation
	void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
	// End SWidget implementation

private:

private:
	TSharedPtr< SGraphEditor >	m_GraphEditorPtr;
	UPlannerDebugger *			m_PlannerDebugger;
};

PlannerViewer.cpp

// Fill out your copyright notice in the Description page of Project Settings.
#include "YaraGameEditorPCH.h"
#include "PlannerViewer.h"
#include "PlannerDebugger.h"

UPlannerViewer::~UPlannerViewer()
{
	if ( !GExitPurge )
	{
		if ( ensure( m_PlannerDebugger ) )
		{
			m_PlannerDebugger->RemoveFromRoot();
		}
	}
}

void UPlannerViewer::Construct( const FArguments& InArgs )
{
	// Create an action list and register commands
	//RegisterActions();

	// Create the graph
	m_PlannerDebugger = ConstructObject< UPlannerDebugger >( UPlannerDebugger::StaticClass() );
	m_PlannerDebugger->Schema = UEdGraphSchema::StaticClass();
	m_PlannerDebugger->AddToRoot();

	// Create the graph editor
	m_GraphEditorPtr = SNew(SGraphEditor)
		.GraphToEdit(m_PlannerDebugger);

	ChildSlot
	[
		SNew(SVerticalBox)

		// Graph
		+ SVerticalBox::Slot()
			.FillHeight(1.f)
			[
				SNew(SOverlay)

				+ SOverlay::Slot()
				[
					m_GraphEditorPtr.ToSharedRef()
				]
			]
	];
	m_PlannerDebugger->RefreshGraph();
}

void UPlannerViewer::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
	SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);

	
		
	
}

PlannerNode.h

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

#pragma once

#include "SGraphNode.h"

class UEdGraphNode_PlannerDebugger;

class UPlannerNode : public SGraphNode
{
public:
	SLATE_BEGIN_ARGS( UPlannerNode ){}

	SLATE_END_ARGS()

	// Constructs this widget with InArgs
	void Construct( const FArguments& InArgs, UEdGraphNode_PlannerDebugger* InNode );

	// SGraphNode implementation
	virtual void UpdateGraphNode() override;
	// End SGraphNode implementation

private:
};

PlannerNode.cpp

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

#include "YaraGameEditorPCH.h"
#include "PlannerNode.h"
#include "EdGraphNode_PlannerDebugger.h"
#include "Text/SInlineEditableTextBlock.h"

#define LOCTEXT_NAMESPACE "PlannerDebugger"



void UPlannerNode::Construct(const FArguments& InArgs, UEdGraphNode_PlannerDebugger* InNode)
{
	GraphNode = InNode;
	UpdateGraphNode();
}

void UPlannerNode::UpdateGraphNode()
{
	OutputPins.Empty();

	// Reset variables that are going to be exposed, in case we are refreshing an already setup node.
	RightNodeBox.Reset();
	LeftNodeBox.Reset();

	UpdateErrorInfo();

	//
	//             ______________________
	//            |      TITLE AREA      |
	//            +-------+------+-------+
	//            | (>) L |      | R (>) |
	//            | (>) E |      | I (>) |
	//            | (>) F |      | G (>) |
	//            | (>) T |      | H (>) |
	//            |       |      | T (>) |
	//            |_______|______|_______|
	//
	TSharedPtr<SVerticalBox>	MainVerticalBox;
	TSharedPtr<SErrorText>		ErrorText;
	TSharedPtr<SNodeTitle>		NodeTitle = SNew( SNodeTitle, GraphNode );

	// No idea what that is
	ContentScale.Bind(this, &UPlannerNode::GetContentScale);

	GetOrAddSlot(ENodeZone::Center)
		.HAlign(HAlign_Center)
		.VAlign(VAlign_Center)
		[
			SAssignNew( MainVerticalBox, SVerticalBox )
			+ SVerticalBox::Slot()
			.AutoHeight()
			[
				SNew(SBorder)
				.BorderImage(FEditorStyle::GetBrush("Graph.Node.Body"))
				.Padding(0)
				[
					SNew(SVerticalBox)
					.ToolTipText(this, &UPlannerNode::GetNodeTooltip)
					+ SVerticalBox::Slot()
					.AutoHeight()
					.HAlign(HAlign_Fill)
					.VAlign(VAlign_Top)
					[
						SNew(SOverlay)
						+ SOverlay::Slot()
						[
							SNew(SImage)
							.Image(FEditorStyle::GetBrush("Graph.Node.TitleGloss"))
						]
						+ SOverlay::Slot()
							.HAlign(HAlign_Left)
							.VAlign(VAlign_Center)
							[
								SNew(SBorder)
								.BorderImage(FEditorStyle::GetBrush("Graph.Node.ColorSpill"))
								// The extra margin on the right
								// is for making the color spill stretch well past the node title
								.Padding(FMargin(10, 5, 30, 3))
								.BorderBackgroundColor(this, &UPlannerNode::GetNodeTitleColor)
								[
									SNew(SVerticalBox)
									+ SVerticalBox::Slot()
									.AutoHeight()
									[
										SNew( STextBlock )
										.Text( NodeTitle.Get(), &SNodeTitle::GetHeadTitle )
									]
									+ SVerticalBox::Slot()
									.AutoHeight()
									[
										NodeTitle.ToSharedRef()
									]
								]
							]
						+ SOverlay::Slot()
							.VAlign(VAlign_Top)
							[
								SNew(SBorder)
								.BorderImage(FEditorStyle::GetBrush("Graph.Node.TitleHighlight"))
								.Visibility(EVisibility::HitTestInvisible)
								[
									SNew(SSpacer)
									.Size(FVector2D(20, 20))
								]
							]
					]
					+ SVerticalBox::Slot()
						.AutoHeight()
						.Padding(1.0f)
						[
							// POPUP ERROR MESSAGE
							SAssignNew(ErrorText, SErrorText)
							.BackgroundColor(this, &UPlannerNode::GetErrorColor)
							.ToolTipText(this, &UPlannerNode::GetErrorMsgToolTip)
						]
					+ SVerticalBox::Slot()
						.AutoHeight()
						.HAlign(HAlign_Fill)
						.VAlign(VAlign_Top)
						[
							// NODE CONTENT AREA
							SNew(SBorder)
							.BorderImage(FEditorStyle::GetBrush("NoBorder"))
							.HAlign(HAlign_Fill)
							.VAlign(VAlign_Fill)
							.Padding(FMargin(0, 3))
							[
								SNew(SHorizontalBox)
								+ SHorizontalBox::Slot()
								.AutoWidth()
								.VAlign(VAlign_Center)
								[
									// LEFT
									SNew(SBox)
									.WidthOverride(40)
									[
										SAssignNew(LeftNodeBox, SVerticalBox)
									]
								]
								+ SHorizontalBox::Slot()
									.VAlign(VAlign_Center)
									.HAlign(HAlign_Center)
									.FillWidth(1.0f)
									[
										SNew(SVerticalBox)
										+ SVerticalBox::Slot()
										.AutoHeight()
										[
											SNew( STextBlock )
											.Text( static_cast< UEdGraphNode_PlannerDebugger * >( GraphNode )->GetFirstLine() )
										]
										+ SVerticalBox::Slot()
										.AutoHeight()
										[
											SNew(STextBlock)
											.Text(static_cast< UEdGraphNode_PlannerDebugger * >(GraphNode)->GetSecondLine())
										]
									]
								+ SHorizontalBox::Slot()
									.AutoWidth()
									.VAlign(VAlign_Center)
									[
										// RIGHT
										SNew(SBox)
										.WidthOverride(40)
										[
											SAssignNew(RightNodeBox, SVerticalBox)
										]
									]
							]
						]
				]
			]
		];
	

	//ErrorReporting = ErrorText;
	//ErrorReporting->SetError(ErrorMsg);
	CreateBelowWidgetControls(MainVerticalBox);

	CreatePinWidgets();
}

#undef LOCTEXT_NAMESPACE

PlannerDebugger.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GraphEditor.h"
#include "PlannerDebugger.generated.h"

UCLASS()
class UPlannerDebugger : public UEdGraph
{
	GENERATED_UCLASS_BODY()

public:
	void RefreshGraph();
private:
	void RemoveAllNodes();
	
};

PlannerDebugger.cpp

// Fill out your copyright notice in the Description page of Project Settings.
#include "YaraGameEditorPCH.h"
#include "PlannerDebugger.h"
#include "EdGraphNode_PlannerDebugger.h"

#define LOCTEXT_NAMESPACE "PlannerDebugger"

UPlannerDebugger::UPlannerDebugger( const FObjectInitializer& ObjectInitializer )
	: Super( ObjectInitializer )
{
}

void UPlannerDebugger::RefreshGraph()
{
	RemoveAllNodes();
	UEdGraphNode_PlannerDebugger *const PlannerDebuggerNodeA = static_cast< UEdGraphNode_PlannerDebugger * >( CreateNode( UEdGraphNode_PlannerDebugger::StaticClass(), false ) );
	UEdGraphNode_PlannerDebugger *const PlannerDebuggerNodeB = static_cast< UEdGraphNode_PlannerDebugger * >( CreateNode( UEdGraphNode_PlannerDebugger::StaticClass(), false ) );
	UEdGraphNode_PlannerDebugger *const PlannerDebuggerNodeC = static_cast< UEdGraphNode_PlannerDebugger * >( CreateNode( UEdGraphNode_PlannerDebugger::StaticClass(), false ) );

	PlannerDebuggerNodeA->SetupNode(FIntPoint(0, 50), "AndCondition" );
	PlannerDebuggerNodeB->SetupNode(FIntPoint(200, 0), "Action", "Goal: RetrieveItem(TeddyBear)", "Method: PickupItem" );
	PlannerDebuggerNodeC->SetupNode(FIntPoint(200, 100), "Action", "Goal: MoveToArea( SpotReception5 )", "Method: Walking" );

	PlannerDebuggerNodeA->GetChildrenPin()->MakeLinkTo( PlannerDebuggerNodeB->GetParentPin() );
	PlannerDebuggerNodeA->GetChildrenPin()->MakeLinkTo( PlannerDebuggerNodeC->GetParentPin() );
}

void UPlannerDebugger::RemoveAllNodes()
{
	TArray< UEdGraphNode* > NodesToRemove = Nodes;
	for (int32 NodeIndex = 0; NodeIndex < NodesToRemove.Num(); ++NodeIndex)
	{
		RemoveNode( NodesToRemove[NodeIndex] );
	}
}

#undef LOCTEXT_NAMESPACE

EdGraphNode_PlannerDebugger.cpp

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

#include "YaraGameEditorPCH.h"
#include "EdGraphNode_PlannerDebugger.h"

#define LOCTEXT_NAMESPACE "PlannerDebugger"

//////////////////////////////////////////////////////////////////////////
// UEdGraphNode_Reference

UEdGraphNode_PlannerDebugger::UEdGraphNode_PlannerDebugger( const FObjectInitializer& ObjectInitializer )
	: Super( ObjectInitializer )
	, m_NodeTitle( )
{
	m_ChildrenPin	= nullptr;
	m_ParentPin		= nullptr;
}


void UEdGraphNode_PlannerDebugger::SetupNode(const FIntPoint& NodePosition, const FString & Title, const FString & FirstLine, const FString & SecondLine)
{
	m_NodeTitle		= FText::FromString( Title );
	m_FirstLine		= FirstLine;
	m_SecondLine	= SecondLine;

	NodePosX = NodePosition.X;
	NodePosY = NodePosition.Y;

	AllocateDefaultPins();
}


void UEdGraphNode_PlannerDebugger::AddChild(UEdGraphNode_PlannerDebugger* ChildPlannerNode)
{
	UEdGraphPin* ParentPinOfChild = ChildPlannerNode->GetParentPin();

	if ( ensure( ParentPinOfChild ) )
	{
		ParentPinOfChild->bHidden	= false;
		m_ChildrenPin->bHidden		= false;
		m_ChildrenPin->MakeLinkTo( ParentPinOfChild );
	}
}




FText UEdGraphNode_PlannerDebugger::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
	return m_NodeTitle;
}

void UEdGraphNode_PlannerDebugger::AllocateDefaultPins()
{
	m_ChildrenPin	= CreatePin( EEdGraphPinDirection::EGPD_Output, TEXT(""), TEXT(""), NULL, false, false, TEXT("") );
	m_ParentPin		= CreatePin( EEdGraphPinDirection::EGPD_Input, TEXT(""), TEXT(""), NULL, false, false, TEXT("") );

	m_ChildrenPin->bHidden	= false;
	m_ParentPin->bHidden	= false;
}

UEdGraphPin* UEdGraphNode_PlannerDebugger::GetChildrenPin()
{
	return m_ChildrenPin;
}

UEdGraphPin* UEdGraphNode_PlannerDebugger::GetParentPin()
{
	return m_ParentPin;
}



#undef LOCTEXT_NAMESPACE

EdGraphNode_PlannerDebugger.h

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

#pragma once

#include "EdGraphNode_PlannerDebugger.generated.h"

UCLASS( )
class UEdGraphNode_PlannerDebugger : public UEdGraphNode
{
	GENERATED_UCLASS_BODY()
public :
	void AddChild( UEdGraphNode_PlannerDebugger* ChildPlannerNode );
	void SetupNode(const FIntPoint& NodePosition, const FString & Title, const FString & FirstLine = FString(""), const FString & SecondLine = FString(""));
	virtual UEdGraphPin* GetChildrenPin();
	virtual UEdGraphPin* GetParentPin();

	const FString & GetFirstLine()const { return m_FirstLine; }
	const FString & GetSecondLine()const { return m_SecondLine; }

private:
	// UEdGraphNode implementation
	virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
	virtual void AllocateDefaultPins() override;
	// End UEdGraphNode implementation

	
private:
	

	FText			m_NodeTitle;
	FString			m_FirstLine;
	FString         m_SecondLine;
	
	UEdGraphPin*	m_ChildrenPin;
	UEdGraphPin*	m_ParentPin;
};
2 Likes

You are a life saver