【UE4编辑器扩展】实践在编辑器界面上创建一个预览视窗(SEditorViewport)

目标

UE4关卡编辑器的场景视窗,还有双击StaticMesh后打开的预览场景视窗,其实都是一个SEditorViewport控件。只不过它们属于不同的子类,派生关系如下:

SCompoundWidget
SEditorViewport
SLevelViewport
SStaticMeshEditorViewport

本篇的目标是尝试在一个独立的窗口中,显示一个预览视窗(SEditorViewport控件)。

参考了资料:UE4自定义资源编辑器开发-预览窗口,但是代码量将少得多(由于本着尽量使用默认的类,不创建新类的原则)

实践0. 创建插件

编辑器Standalone窗口为模板,这样可以有一个独立的窗口作为起点。
在这里插入图片描述
插件名字是TestPreviewViewport

它将在编辑器的工具栏中生成一个按钮,点击它会显示一个窗口分页控件(SDockTab),而其内容则指定在FTestPreviewViewportModule::OnSpawnPluginTab中:
在这里插入图片描述

实践1. 定义新的SEditorViewport

首先,我需要一个SEditorViewport,然而它本身有纯虚函数:

virtual TSharedRef<FEditorViewportClient> MakeEditorViewportClient() = 0;

因此它是抽象类,不能直接实例化。

我也想到使用它的一个子类,而对于它的所有子类:
在这里插入图片描述
我一一查看后,发现只有SLevelViewport模块_API宏标记了,这意味着只有它可以被外部使用。但我不想使用关卡的视窗,因为我担心它过于复杂。
最终的结论是,没有合适的子类供我使用,我只能新定义一个SEditorViewport的子类,新定义的子类名字是STestEditorViewport


暂时的STestEditorViewport.h

#pragma once

#include "CoreMinimal.h"
#include "SEditorViewport.h"

class STestEditorViewport : public SEditorViewport
{
public:
	SLATE_BEGIN_ARGS(STestEditorViewport) {}
	SLATE_END_ARGS()

	void Construct(const FArguments& InArgs);
	STestEditorViewport();
	~STestEditorViewport();
	
protected:
	/** SEditorViewport interface */
	virtual TSharedRef<FEditorViewportClient> MakeEditorViewportClient() override;
};

暂时的STestEditorViewport.cpp

#include"STestEditorViewport.h"

#include"EditorViewportClient.h"

void STestEditorViewport::Construct(const FArguments& InArgs)
{
	SEditorViewport::Construct(SEditorViewport::FArguments());
}
STestEditorViewport::STestEditorViewport()
{
}
STestEditorViewport::~STestEditorViewport()
{
}

TSharedRef<FEditorViewportClient> STestEditorViewport::MakeEditorViewportClient()
{
	TSharedPtr<FEditorViewportClient> EditorViewportClient = MakeShareable(new FEditorViewportClient(nullptr));

	return EditorViewportClient.ToSharedRef();
}

注意MakeEditorViewportClient(),他需要得到一个FEditorViewportClient对象,由于它本身不是抽象类,所以我可以直接使用这个类。


当前预览窗口打开后,是一个和关卡编辑器里的场景一模一样的场景:
在这里插入图片描述
调试源代码发现,原来在未传入预览场景的情况下,GetWorld()将会返回GWorld,此即为关卡编辑器内显示的场景。
在这里插入图片描述

实践2. 创建FPreviewScene

为了使用自己的场景而不是默认的关卡编辑器内的场景,需要自己提供一个FPreviewScene对象。

为此,先创建一个FPreviewScene的智能指针(防止对象被销毁)在STestEditorViewport中:

TSharedPtr<class FPreviewScene> PreviewScene;

然后,在创建FEditorViewportClient时就可以将自己的场景传递到其构造函数中了:

TSharedRef<FEditorViewportClient> STestEditorViewport::MakeEditorViewportClient()
{
	PreviewScene = MakeShareable(new FPreviewScene());

	TSharedPtr<FEditorViewportClient> EditorViewportClient = MakeShareable(new FEditorViewportClient(nullptr, PreviewScene.Get()));

	return EditorViewportClient.ToSharedRef();
}

效果:
在这里插入图片描述
看起来空无一物,但是左下角的坐标表示现在确实在一个3D场景中。

实践3. 显示视窗工具栏

SEditorViewport中有一个虚函数,它将指定视窗的工具栏:

// Implement this to add a viewport toolbar to the inside top of the viewport
virtual TSharedPtr<SWidget> MakeViewportToolbar() { return TSharedPtr<SWidget>(nullptr); }

默认情况下如果不在子类中实现这个函数,它将返回空,即不生成工具栏。

查看官方的代码SStaticMeshEditorViewport,它将返回SStaticMeshEditorViewportToolbar
在这里插入图片描述
看定义发现它似乎在SStaticMeshEditorViewportToolbar的基础上增加了LOD相关的功能。
当前我不需要其他附加的功能,因此对于我的STestEditorViewport,我希望直接使用视窗工具栏的基类,即SCommonEditorViewportToolbarBase


但现在有个问题是,SCommonEditorViewportToolbarBaseConstruct要求一个ICommonEditorViewportToolbarInfoProvider指针:

void Construct(const FArguments& InArgs, TSharedPtr<class ICommonEditorViewportToolbarInfoProvider> InInfoProvider);

查看ICommonEditorViewportToolbarInfoProvider的定义:

// This is the interface that the host of a SCommonEditorViewportToolbarBase must implement
class ICommonEditorViewportToolbarInfoProvider
{
public:
	// Get the viewport widget
	virtual TSharedRef<class SEditorViewport> GetViewportWidget() = 0;

// 	FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));
// 	TSharedPtr<FExtender> LevelEditorExtenders = LevelEditorModule.GetMenuExtensibilityManager()->GetAllExtenders();
	virtual TSharedPtr<FExtender> GetExtenders() const = 0;

	// Called to inform the host that a button was clicked (typically used to focus on a particular viewport in a multi-viewport setup)
	virtual void OnFloatingButtonClicked() = 0;
};

它似乎是和视窗工具栏所对接而必须实现的接口。观察官方使用,是直接让SStaticMeshEditorViewport继承了这个接口。因此我也学着让我的STestEditorViewport继承它:

class STestEditorViewport : public SEditorViewport, public ICommonEditorViewportToolbarInfoProvider
{
	...
public:
	// ICommonEditorViewportToolbarInfoProvider interface
	virtual TSharedRef<class SEditorViewport> GetViewportWidget() override;
	virtual TSharedPtr<FExtender> GetExtenders() const override;
	virtual void OnFloatingButtonClicked() override;
	// End of ICommonEditorViewportToolbarInfoProvider interface
	...
};

而实现上,就是和SStaticMeshEditorViewport中的一样:

TSharedRef<class SEditorViewport> STestEditorViewport::GetViewportWidget()
{
	return SharedThis(this);
}

TSharedPtr<FExtender> STestEditorViewport::GetExtenders() const
{
	TSharedPtr<FExtender> Result(MakeShareable(new FExtender));
	return Result;
}

void STestEditorViewport::OnFloatingButtonClicked()
{
}

最后,在MakeViewportToolbar中指定返回一个SCommonEditorViewportToolbarBase,并将自己转换为一个ICommonEditorViewportToolbarInfoProvider接口作为参数:

TSharedPtr<SWidget> STestEditorViewport::MakeViewportToolbar()
{
	return SNew(SCommonEditorViewportToolbarBase, SharedThis(this));
}

效果,可以看到左上角出现了视窗工具栏。
在这里插入图片描述

实践4. 向预览场景中添加组件

向预览场景中添加组件是通过FPreviewScene::AddComponent完成的:

/**
 * Adds a component to the preview scene.  This attaches the component to the scene, and takes ownership of it.
 */
virtual void AddComponent(class UActorComponent* Component,const FTransform& LocalToWorld, bool bAttachToRoot=false);

顺带一提,不用担心组件会被意外销毁,因为FPreviewScene是一个FGCObject,它会将所有组件添加引用:

void FPreviewScene::AddReferencedObjects( FReferenceCollector& Collector )
{
	Collector.AddReferencedObjects( Components );
	Collector.AddReferencedObject( PreviewWorld );
}

现在,向预览场景中添加一个测试用的模型:

//读取模型
UStaticMesh* SM = LoadObject<UStaticMesh>(NULL, TEXT("StaticMesh'/Engine/EngineMeshes/Cube.Cube'"), NULL, LOAD_None, NULL);
//创建组件
UStaticMeshComponent* SMC = NewObject<UStaticMeshComponent>();
SMC->SetStaticMesh(SM);
//向预览场景中增加组件
PreviewScene->AddComponent(SMC, FTransform::Identity);	

效果:
在这里插入图片描述

扩展

上面的代码可以说是无法更省略的最简单创建一个预览视窗的代码了。
然而实际使用中,可能会出现一些必须要新定义一些子类来指定更个性化的行为的情况(就像《UE4自定义资源编辑器开发-预览窗口》所做的一样),这时候可能需要对下面的类进行继承:

  • FPreviewScene
  • FEditorViewportClient
  • SCommonEditorViewportToolbarBase

最终代码

STestEditorViewport.h

#pragma once

#include "CoreMinimal.h"
#include "SEditorViewport.h"
#include "SCommonEditorViewportToolbarBase.h"

class STestEditorViewport : public SEditorViewport, public ICommonEditorViewportToolbarInfoProvider
{
public:
	SLATE_BEGIN_ARGS(STestEditorViewport) {}
	SLATE_END_ARGS()

	void Construct(const FArguments& InArgs);
	STestEditorViewport();
	~STestEditorViewport();
	
protected:
	/** SEditorViewport interface */
	virtual TSharedRef<FEditorViewportClient> MakeEditorViewportClient() override;
	virtual TSharedPtr<SWidget> MakeViewportToolbar() override;

public:
	// ICommonEditorViewportToolbarInfoProvider interface
	virtual TSharedRef<class SEditorViewport> GetViewportWidget() override;
	virtual TSharedPtr<FExtender> GetExtenders() const override;
	virtual void OnFloatingButtonClicked() override;
	// End of ICommonEditorViewportToolbarInfoProvider interface

private:
	//预览窗口
	TSharedPtr<class FPreviewScene> PreviewScene;
};

STestEditorViewport.cpp


#include"STestEditorViewport.h"

#include"EditorViewportClient.h"
#include"PreviewScene.h"
#include"SCommonEditorViewportToolbarBase.h"

void STestEditorViewport::Construct(const FArguments& InArgs)
{
	SEditorViewport::Construct(SEditorViewport::FArguments());
}
STestEditorViewport::STestEditorViewport()
{
}
STestEditorViewport::~STestEditorViewport()
{
}

TSharedRef<FEditorViewportClient> STestEditorViewport::MakeEditorViewportClient()
{
	PreviewScene = MakeShareable(new FPreviewScene());

	//向预览场景中加一个测试模型
	{
		//读取模型
		UStaticMesh* SM = LoadObject<UStaticMesh>(NULL, TEXT("StaticMesh'/Engine/EngineMeshes/Cube.Cube'"), NULL, LOAD_None, NULL);
		//创建组件
		UStaticMeshComponent* SMC = NewObject<UStaticMeshComponent>();
		SMC->SetStaticMesh(SM);
		//向预览场景中增加组件
		PreviewScene->AddComponent(SMC, FTransform::Identity);		
	}

	TSharedPtr<FEditorViewportClient> EditorViewportClient = MakeShareable(new FEditorViewportClient(nullptr, PreviewScene.Get()));

	return EditorViewportClient.ToSharedRef();
}

TSharedPtr<SWidget> STestEditorViewport::MakeViewportToolbar()
{
	return SNew(SCommonEditorViewportToolbarBase, SharedThis(this));
}

TSharedRef<class SEditorViewport> STestEditorViewport::GetViewportWidget()
{
	return SharedThis(this);
}

TSharedPtr<FExtender> STestEditorViewport::GetExtenders() const
{
	TSharedPtr<FExtender> Result(MakeShareable(new FExtender));
	return Result;
}

void STestEditorViewport::OnFloatingButtonClicked()
{
}

TestPreviewViewport.cpp中的OnSpawnPluginTab函数:

TSharedRef<SDockTab> FTestPreviewViewportModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{
	FText WidgetText = FText::Format(
		LOCTEXT("WindowWidgetText", "Add code to {0} in {1} to override this window's contents"),
		FText::FromString(TEXT("FTestPreviewViewportModule::OnSpawnPluginTab")),
		FText::FromString(TEXT("TestPreviewViewport.cpp"))
		);

	return SNew(SDockTab)
		.TabRole(ETabRole::NomadTab)
		[
			//创建预览视窗控件
			SNew(STestEditorViewport)
		];
}
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值