目标
UE4关卡编辑器的场景视窗,还有双击StaticMesh后打开的预览场景视窗,其实都是一个SEditorViewport
控件。只不过它们属于不同的子类,派生关系如下:
本篇的目标是尝试在一个独立的窗口中,显示一个预览视窗(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
。
但现在有个问题是,SCommonEditorViewportToolbarBase
的Construct
要求一个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)
];
}