[Unreal_Editor]Unreal_커스텀 뷰포트 에디터 제작

2023. 4. 13. 23:34unreal/UnrealCode

언리얼에서 뷰틸리티블루프린트가 생긴뒤로 크게 필요하지 않지만 가끔 뷰틸리티에서 사용불가능한 함수나 프로퍼티 또는 뷰포트 제어를 위해 별도의 에디터 윈도우를 제작할 필요가 있습니다.

 

일단 최종 목적지는 이것입니다.

간단한 메뉴버튼을 제작해 누르면 플로팅 윈도우(dock)을 띄워 월드에 특정 오브젝트들을 스폰하거나 미리 설정한 레벨을 불러들어오는 기능입니다.

또한 플로팅 윈도우에 버튼을 눌러 윈도우상 월드에 원하는 액션을 진행할 수 있습니다.

 

 

1. 메뉴 생성

메뉴생성은 튜토리얼도 다양하고 엄청 많으니 참고하실만한 링크를 남기고 간략히 넘어가도록 하겠습니다.

 

기본 메뉴 생성시 참고한 베이스는 

https://lxjk.github.io/2019/10/01/How-to-Make-Tools-in-U-E.html

 

How to Make Tools in UE4

 

lxjk.github.io

- 메뉴 제작을 a-z까지 전부 알려줍니다 따라하기만 해도 대부분 되며 몇몇 코드는 과거의 코드라 수정을 해야 합니다.

단점은 너무 복잡한 방식으로 메뉴를 만들고 있고 기본적으로 상당히 c++ 뿐만아닌 슬레이트 코드에 대한 지식이 있다는 사실을 전제로 내용이 설명되어 있어 난이도가 높습니다.

 

위 블로그를 베이스로 제작을 하였고 쭉 따라하게 되면

이런형태의 메뉴창을 얻을수 있습니다.

일부로 저런식으로 메뉴(example이란 메뉴 버튼)를 빌드(생성)하지 않고 플러그인 방식을 사용하면 매우 쉽게 누를수 있는 툴바 버튼과 액션함수를 사용할 수 있으니

간단히 테스트를 하기위해서는 그 방식을 사용하면 쉽습니다.

 

 

 

 

2. 슬레이트 문법

https://ikrima.dev/ue4guide/editor-extensions/slate/useful-slate-code-samples/

 

Useful slate code samples - Gamedev Guide

Useful slate code samples Go to Window -> Developer Tools -> Debug Tools -> Test Suite. Shows all the different widgets UWidgetBlueprintLibrary & UWiddgetLayoutLibrary, & USlateBlueprintLibrary great reference for looking at slate drawing functions & helpe

ikrima.dev

슬레이트 문법을 찾아볼수 있는 참고 자료모음입니다.

더 쉬운 문서들도 많긴했는데 가장 도움이 많이 된곳입니다.

참고로 위 슬레이트 테스트 창을 여는방법은 이곳입니다.

이런식으로 테스트창 혹은 에디터의 어디라도 위젯 리플렉터를 활성화 한 뒤 붉은 네모박스부분의 코드파일을 열면

위젯 관련 코드를 확인할 수 있습니다.

사실 엔진에서 실제 사용되는 코드는 간단한 기능도 상당히 복잡하게 구현되어 있어 저런식의 테스트창에서 확인해보는것이 더 쉽게 익힐 수 있습니다.

 

 

저의가 사용할 tabtool부터 시작하도록 하겠습니다.

탭툴 모듈안에 포함될 파일들입니다.

 

 

 

 

 

//ExampleTabToolBase.h

#include "ToolExampleEditor/ToolExampleEditor.h"
#include "ToolExampleEditor/IExampleModuleInterface.h"
#include "Framework/Docking/TabManager.h"
#include "Widgets/Docking/SDockTab.h"

class FExampleTabToolBase : public IExampleModuleListenerInterface, public TSharedFromThis< FExampleTabToolBase >
{

public:

// IPixelopusToolBase
virtual void OnStartupModule() override
{
Initialize();
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(TabName, FOnSpawnTab::CreateRaw(this, &FExampleTabToolBase::SpawnTab))
.SetGroup(FToolExampleEditor::Get().GetMenuRoot())
.SetDisplayName(TabDisplayName)
.SetTooltipText(ToolTipText);
};

virtual void OnShutdownModule() override
{
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(TabName);
};

// In this function set TabName/TabDisplayName/ToolTipText
virtual void Initialize() {};

virtual TSharedRef<SDockTab> SpawnTab(const FSpawnTabArgs& TabSpawnArgs) { return SNew(SDockTab); };
virtual void MakeMenuEntry(FMenuBuilder &menuBuilder)
{  FGlobalTabmanager::Get()->PopulateTabSpawnerMenu(menuBuilder, TabName); };

virtual ~FExampleTabToolBase() { }; 


protected:
FName TabName;
FText TabDisplayName;
FText ToolTipText;
};

탭툴을 만들때 사용될 기능(spawntab과 makemenuentry 등)의 함수의 원형입니다.

위에 썻듯이 위 블로그를 보고 따라진행하게되면 리스너를 만들어 메뉴를 등록하는 등 복잡한 단계를 거치게 되므로

따로 플러그인 등을 사용하게 되면 

virtual TSharedRef SpawnTab(const FSpawnTabArgs& TabSpawnArgs) { return SNew(SDockTab); };

이 함수만 실행해 주면 됩니다.(현재는 원형이고 실제 구현은 아래에서 진행됩니다)

 

taptool.cpp/h

-> 모듈을 등록하고 해제하는 파일입니다.또한 메뉴 자체를 위이미지의 section2에  생성 해 줍니다

 

//taptool.h

#include "ToolExampleEditor/ExampleTabToolBase.h"

class TabTool : public FExampleTabToolBase
{
public:
virtual ~TabTool() {}
virtual void OnStartupModule() override;
virtual void OnShutdownModule() override;
virtual void Initialize() override;
virtual TSharedRef<SDockTab> SpawnTab(const FSpawnTabArgs& TabSpawnArgs) override;

};


//taptool.cpp

#include "TabTool.h"
//#include "AssetRegistryModule.h"
//#include "ScopedTransaction.h"
//#include "SDockTab.h"
//#include "SDockableTab.h"
//#include "SDockTabStack.h"
//#include "SlateApplication.h"
#include "ToolExampleEditor/ToolExampleEditor.h"
#include "TabToolPanel.h"

void TabTool::OnStartupModule()//모듈 실행시(등록시) 실행됩니다.
{
FExampleTabToolBase::OnStartupModule();
FToolExampleEditor::Get().AddMenuExtension(FMenuExtensionDelegate::CreateRaw(this, &TabTool::MakeMenuEntry), FName("Section_2"));
}

void TabTool::OnShutdownModule()
{
FExampleTabToolBase::OnShutdownModule();
}

void TabTool::Initialize()
{
TabName = "TabTool";
TabDisplayName = FText::FromString("Tab Tool");
ToolTipText = FText::FromString("Tab Tool Window");
}

TSharedRef<SDockTab> TabTool::SpawnTab(const FSpawnTabArgs& TabSpawnArgs)
{
TSharedRef<SDockTab> SpawnedTab = SNew(SDockTab)
.TabRole(ETabRole::NomadTab)
[

SNew(TabToolPanel)
//.Tool(SharedThis(this))
];

return SpawnedTab;
}

TabTool::SpawnTab()으로 DockTab(플로팅된 독립창)을 생성했습니다.

 

taptoolpanel.cpp 의 include에 TabToolPanel.h를 포함해서 생성했으니 이제 독립된 창을 갖게 되었고 그 창에 필요한 뷰포트 및 버튼을 생성할 차례 입니다.

 

//TabToolPanel.h

#include "Widgets/Docking/SDockTab.h"
#include "Widgets/Docking/SDockableTab.h"
#include "Widgets/Docking/SDockTabStack.h"
#include "Framework/Application/SlateApplication.h"
#include "ToolExampleEditor/TabTool/TestViewPort.h"
#include "TabTool.h"

class TabToolPanel : public SCompoundWidget
{
	SLATE_BEGIN_ARGS(TabToolPanel)
	{}
	SLATE_END_ARGS()

	void Construct(const FArguments& InArgs);

public:
	FReply ClickSpawnBTN();
protected:
	TWeakPtr<TabTool> tool;
	TSharedPtr<STestViewPort> viewportPtr;
};

//TabToolPanel.cpp
#include "TabToolPanel.h"
#include "ToolExampleEditor/TabTool/TestViewPort.h"
#include "ToolExampleEditor/ToolExampleEditor.h"

void TabToolPanel::Construct(const FArguments& InArgs)
{
	
	ChildSlot
	[

		SNew(SHorizontalBox)
		+ SHorizontalBox::Slot().Padding(4.0f).FillWidth(0.8f)
		[
			SNew(SBorder).BorderImage(FEditorStyle::GetBrush("ToolPanel.DarkGroupBorder")).Padding(4.0f)
			[
				SNew(SHorizontalBox)
				+ SHorizontalBox::Slot().FillWidth(1.0f)
				[
					SNew(SVerticalBox)
					+ SVerticalBox::Slot().FillHeight(1.0f)
					[

						SAssignNew(viewportPtr, STestViewPort)
						//SNew(STestViewPort)
					]

				]
			]
		]

	+ SHorizontalBox::Slot().FillWidth(0.2f)
		[
			SNew(SBorder).BorderImage(FEditorStyle::GetBrush("ToolPanel.DarkGroupBorder")).Padding(4.0f)
			[
				SNew(SHorizontalBox)
				+ SHorizontalBox::Slot().FillWidth(1.0f)
				[
					SNew(SVerticalBox)
					+ SVerticalBox::Slot().MaxHeight(40.0f)
					[
						SNew(SButton).Text(FText::FromString("CalcWorldT")).OnClicked(this, &TabToolPanel::ClickSpawnBTN)
					]
				]
			]
		]

	];
}

FReply TabToolPanel::ClickSpawnBTN()
{
	UE_LOG(LogClass, Log, TEXT("clicked SpawnBTN"));
	
	return FReply::Handled();
}

헤더파일에는 위젯 생성자와 버튼을 클릭했을시 반응할 함수가 선언되어있고 뷰포트 클래스가 선언되어 있습니다.

소스 파일에서는 생성자안에 어떤 형태로 창을 나눌지 설정되어있고 좌측 패널에는 뷰포트를 우측 패널에는 버튼을 배치햇습니다.

 

일단 패널자체의 창을 쪼개고 나누는것은 워낙 다양한 방식이 있기에 꼭 위 코드를 따라할 필요는 없습니다.

뷰포트는 SAssignNew(viewportPtr, STestViewPort) 로 생성했고 버튼은 SNew로 생성했습니다.

두 차이점은 생성하며 생성한 객체에 포인터를 같이 설정하느냐와(SAssingNew)와 단순 생성이냐로 나뉩니다.

대부분 위젯에서 제공하는 버튼이나 horizontalbox / verticalbox 등은 대부분의 기능이 구현되있으므로 따로 포인터가 필요한 경우가 적어 SNew로 생성하면되나 특정 목정의 커스텀 윈도우 등을 윈도우에서 기능을 따로 구현하는경우가 많으니

생성과 같이 포인터를 등록해주면 따로 등록하지 않아도 됩니다.

\Engine\Source\Runtime\SlateCore\Public\Widgets\DeclarativeSyntaxSupport.h

엔진코드를 보면 Snew로 생성해도 TSharePtr<SButton> 의 방식으로 추가로 등록해 줄 수 있습니다.

버튼에 바인딩된 함수내부에 필요 코드를 등록해주면 됩니다. 마지막에 예시로 액터를 스폰해보도록 하겠습니다.

 

 

 

 

여기까지 언리얼 엔진에서 별도로 만든 버튼 혹은 메뉴를 눌럿을시 독립된 윈도우 창이 열리며(보통 스켈레탈메시에디터/ 블루프린트 에디터 등등의 형식) 창에 필요한 버튼과 뷰포트를 등록하고 버튼을 눌럿을시 특정 함수를 실행하도록 만들어 두었습니다.

 

///////////////////////////// 커스텀 뷰포트 만들기

커스텀 에디터 제작은 튜토리얼도 방대하고 워낙 다양하고 다양해 전문적이고 어려운 튜토리얼부터 초심자도 쉽게 이해할만한 내용이 다양하게 있지만

커스텀 뷰포트를 붙이는 방법은 정말 튜토리얼도 거의 없고 있는 튜토리얼들도 설명이 거의 건너뛰어 있는 초심자가 보기에는 상당히 어려운 내용들이어 초심자인 제가 직접 느끼고 필요한 부분만 간단히 알려드리고자 적게 되었습니다.

 

커스텀 윈도우 제작에 참고한 내용입니다.

 

https://forums.unrealengine.com/t/create-custom-ue4-editor-viewport/349916

 

Create custom UE4 editor viewport?

I want to create a custom viewport (to edit UVs). I looked at SStaticMeshEditorViewport and FStaticMeshEditorViewportClient. I made a Viewport which inherits from SEditorViewport, and a ViewportClient which inherits from FEditorViewportClient. When I creat

forums.unrealengine.com

작성자가 질문하고 본인의 코드를 올려둔 포럼의 질문글입니다.

https://easycomplex-tech.com/blog/Unreal/AssetEditor/UEAssetEditorDev-AssetEditorPreview/

 

UE4 Asset Editor Preview

## 1 Overview --- If the custom asset has some properties that are needed to be previewed (ex. animation, mesh...), then you should create a viewport to present these properties. To create a viewport, there are serveral key classes: + FPreviewScene : The c

easycomplex-tech.com

에셋 에디터 프리뷰를 만드는 튜토리얼이며 중간 과정의 한부분으로 뷰포트를 생성하는 과정이 포함되어 있습니다.

물론 저 위의 두 글을 보고 엔진 코드를 조금만 더 보면 뷰포트를 바로 제작해서 붙이실 수 있습니다.

 

//TestViewPort.h

#pragma once
#include "CoreMinimal.h"
#include "UnrealEd.h"

//뷰포트 생성 기본 include
#include "EditorViewportClient.h"
#include "PreviewScene.h"
#include "Editor/UnrealEd/Public/SCommonEditorViewportToolbarBase.h"
#include "Widgets/SCompoundWidget.h"

#include "Editor/Kismet/Public/WorkflowOrientedApp/WorkflowTabFactory.h"
#include "Editor/UnrealEd/Public/SEditorViewport.h"

class FTesePreviewScene;
class FTeseViewPortClient;

//뷰포트
class STestViewPort : public SEditorViewport, public ICommonEditorViewportToolbarInfoProvider
{
public:
	SLATE_BEGIN_ARGS(STestViewPort) {}
	SLATE_END_ARGS()
		
		STestViewPort();
		~STestViewPort();

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

	//툴바 인터페이스
	virtual TSharedRef<class SEditorViewport>GetViewportWidget() override;
	virtual TSharedPtr<FExtender>GetExtenders() const override;
	virtual void OnFloatingButtonClicked() override;


	//뷰포트 클라이언트 및 뷰포트 툴바 생성
	//End SEditorViewport interface
	virtual TSharedRef<FEditorViewportClient>MakeEditorViewportClient() override;
	//virtual TSharedPtr<SWidget>MakeViewportToolbar() override;
	//End SEditorViewport interface

private:
	//뷰포트 구성요소
	TSharedPtr<FTeseViewPortClient> LevelViewportClient;
	TSharedPtr<FTesePreviewScene> TestPreviewScene;
};


//프리뷰씬
class FTesePreviewScene : public FPreviewScene
{
public:
	FTesePreviewScene();
	TArray<FString> PreviewLevelName;
};

//뷰포트 클라이언트
class FTeseViewPortClient	: public FEditorViewportClient, public TSharedFromThis<FTeseViewPortClient>
{
public:
	FTeseViewPortClient(FPreviewScene& InPreviewScene, const TSharedRef<STestViewPort>& InViewport);
	
	//FTeseViewPortClient();
	
	virtual void Draw(FViewport* InViewport, FCanvas* Canvas) override;
	virtual void Tick(float DeltaSeconds) override;
	virtual bool InputKey(FViewport* InViewport, int32 ControllerId, FKey Key, EInputEvent Event, float AmountDepressed /*= 1.f*/, bool bGamepad /*= false*/) override;
	virtual bool InputAxis(FViewport* InViewport, int32 ControllerId, FKey Key, float Delta, float DeltaTime, int32 NumSamples/* =1 */, bool bGamepad/* =false */) override;
};


//TestViewPort.cpp

#include "ToolExampleEditor/TabTool/TestViewPort.h"

//위젯
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Input/STextComboBox.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SButton.h"

#include "Kismet/GameplayStatics.h"
#include "EditorLevelLibrary.h"
#include "EditorLevelUtils.h"

#define LOCTEXT_NAMESPACE "STestViwport"

STestViewPort::STestViewPort()
	: TestPreviewScene(MakeShareable(new FTesePreviewScene()))
{ }

STestViewPort::~STestViewPort()
{ }

void STestViewPort::Construct(const FArguments& InArgs)
{
	// Setup Client 
	SEditorViewport::Construct(SEditorViewport::FArguments());

	//Spawn actor here ex)APostProcessVolume
	//PostProcessVolumeActor = GetWorld()->SpawnActor<APostProcessVolume>(APostProcessVolume::StaticClass(), FTransform::Identity);
}

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

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

void STestViewPort::OnFloatingButtonClicked()
{

}

TSharedRef<FEditorViewportClient> STestViewPort::MakeEditorViewportClient()
{
	//LevelViewportClient = MakeShareable(new FTestViewportClient(*scene, context, SharedThis(this)));
	//FEditorViewportClient(FEditorModeTools * InModeTools, FPreviewScene * InPreviewScene = nullptr, const TWeakPtr<SEditorViewport>&InEditorViewportWidget = nullptr);
	LevelViewportClient = MakeShareable(new FTeseViewPortClient(*TestPreviewScene, SharedThis(this)));
	//LevelViewportClient = MakeShareable(new FTestViewportClient(*scene, context, SharedThis(this)));
	//EditorViewportClient = MakeShareable(new FSimpleViewportClient(nullptr, *PreviewScene.Get(), SharedThis(this)));
	LevelViewportClient->ViewportType = LVT_Perspective;
	LevelViewportClient->bSetListenerPosition = false;

	LevelViewportClient->MoveViewportCamera(FVector(-1500.0f, 0.0f, 700.0f), FRotator(0.0f, 90.0f, 0.0f));
	LevelViewportClient->SetViewLocationForOrbiting(FVector(-1500.0f, 0.0f, 700.0f));
	LevelViewportClient->ViewFOV = 90.0f;

	return LevelViewportClient.ToSharedRef();

	

}

//TSharedPtr<SWidget> STestViewPort::MakeViewportToolbar()
//{
//
//	return SNew(STestEditorViewportToolBar, SharedThis(this))
//		.Cursor(EMouseCursor::Default)
//		;
//}
#undef LOCTEXT_NAMESPACE



//프리뷰씬 기본 세팅
FTesePreviewScene::FTesePreviewScene():FPreviewScene()
{
	
	PreviewLevelName = { FString("/Game/EditorResources/PreviewLevel") };
	//월드 세팅
	
	EditorLevelUtils::AddLevelsToWorld(GetWorld(), PreviewLevelName, ULevelStreamingDynamic::StaticClass());
	//GetWorld()->SetCurrentLevel();
	GetWorld()->GetWorldSettings()->NotifyBeginPlay();
	GetWorld()->GetWorldSettings()->NotifyMatchStarted();
	GetWorld()->GetWorldSettings()->SetActorHiddenInGame(false);
	GetWorld()->bBegunPlay = true; 

	// set light options 
	DirectionalLight->SetRelativeLocation(FVector(-1024.f, 1024.f, 2048.f));
	DirectionalLight->SetRelativeScale3D(FVector(15.f));
		
	SetLightBrightness(4.f);
	DirectionalLight->InvalidateLightingCache();
	DirectionalLight->RecreateRenderState_Concurrent();

	// creae a sky sphere

	UStaticMeshComponent* SkyComp = NewObject<UStaticMeshComponent>();
	UStaticMesh* StaticMesh = LoadObject<UStaticMesh>(NULL, TEXT("/Engine/MapTemplates/Sky/SM_SkySphere.SM_SkySphere"), NULL, LOAD_None, NULL);
	SkyComp->SetStaticMesh(StaticMesh);
	UMaterial* SkyMaterial = LoadObject<UMaterial>(NULL, TEXT("/Engine/EditorMaterials/PersonaSky.PersonaSky"), NULL, LOAD_None, NULL);
	SkyComp->SetMaterial(0, SkyMaterial);
	const float SkySphereScale = 1000.f;
	const FTransform SkyTransform(FRotator(0, 0, 0), FVector(0, 0, 0), FVector(SkySphereScale));
	AddComponent(SkyComp, SkyTransform);



	// now add floor
	UStaticMesh* FloorMesh = LoadObject<UStaticMesh>(NULL, TEXT("/Engine/EditorMeshes/EditorCube.EditorCube"), NULL, LOAD_None, NULL);
	UStaticMeshComponent* FloorComp = NewObject<UStaticMeshComponent>();
	FloorComp->SetStaticMesh(FloorMesh);
	AddComponent(FloorComp, FTransform::Identity);
	FloorComp->SetRelativeScale3D(FVector(3.f, 3.f, 1.f));
	UMaterial* Material = LoadObject<UMaterial>(NULL, TEXT("/Engine/EditorMaterials/PersonaFloorMat.PersonaFloorMat"), NULL, LOAD_None, NULL);
	FloorComp->SetMaterial(0, Material);
	FloorComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
	FloorComp->SetCollisionObjectType(ECC_WorldStatic);

}

//뷰포트 클라이언트 레퍼런스 생성
FTeseViewPortClient::FTeseViewPortClient(FPreviewScene& InPreviewScene, const TSharedRef<STestViewPort>& InViewport)

	: FEditorViewportClient(nullptr, &InPreviewScene, StaticCastSharedRef<SEditorViewport>(InViewport))
{
	SetViewMode(VMI_Lit);
	
	SetRealtime(true);
	EngineShowFlags.DisableAdvancedFeatures();
	EngineShowFlags.SetSnap(0);
	EngineShowFlags.CompositeEditorPrimitives = true;
	OverrideNearClipPlane(1.0f);
	bUsingOrbitCamera = true;
}

//뷰포트 클라이언트 기능
bool FTeseViewPortClient::InputKey(FViewport* InViewport, int32 ControllerId, FKey Key, EInputEvent Event, float AmountDepressed /*= 1.f*/, bool bGamepad /*= false*/)
{
	return FEditorViewportClient::InputKey(InViewport, ControllerId, Key, Event, AmountDepressed, false);
}

bool FTeseViewPortClient::InputAxis(FViewport* InViewport, int32 ControllerId, FKey Key, float Delta, float DeltaTime, int32 NumSamples/* =1 */, bool bGamepad/* =false */)
{
	return FEditorViewportClient::InputAxis(InViewport, ControllerId, Key, Delta, DeltaTime, NumSamples, bGamepad);
}

void FTeseViewPortClient::Draw(FViewport* InViewport, FCanvas* Canvas)
{
	FEditorViewportClient::Draw(InViewport, Canvas);
}

void FTeseViewPortClient::Tick(float DeltaSeconds)
{
	if (!GIntraFrameDebuggingGameThread)
	{
		PreviewScene->GetWorld()->Tick(LEVELTICK_All, DeltaSeconds);
	}

}

뷰포트는 기본적으로 

SEditorViewport 클래스를 상속받아 

class FEditorViewportClient를 생성해주며 뷰포트 클라이언트 생성시 class FPreviewScene을 사용하게 됩니다.

프리뷰 씬은 따로 생성하지 않고 기본 프리뷰 씬을 사용할수도 있어 실제로는

SEditorViewport 와 FEditorViewportClient 두 클래스만 존재하면 뷰포트를 띄울수 있습니다.

 

FPreviewScene 는 보여지는 부분을 담당하며

FEditorViewportClient 는 기능적인 모든 부분을 담당하게되며 마우스 컨트롤이나 액터 선택등은 모두 FEditorViewportClient 를 통해 진행하게 됩니다.

 

class STestViewPort : public SEditorViewport 

이클래스에는 생성자와 뷰포트 생성 함수가 들어있습니다.

 

생성자

STestViewPort::STestViewPort()
: TestPreviewScene(MakeShareable(new FTesePreviewScene()))

뷰포트 클라이언트를 생성할때(MakeEditorViewportClient()) 사용할 프리뷰씬의 포인터를 설정해 줍니다.
{}

 

뷰포트 클라이언트 생성

TSharedRef<FEditorViewportClient> STestViewPort::MakeEditorViewportClient()
{
LevelViewportClient = MakeShareable(new FTeseViewPortClient(*TestPreviewScene, SharedThis(this)));
//프리뷰 씬과 이 클래스를 사용해 뷰포트 클라이언트를 생성합니다.

이후는 뷰포트클라이언트의 기본 세팅입니다.


LevelViewportClient->ViewportType = LVT_Perspective;
LevelViewportClient->bSetListenerPosition = false;
LevelViewportClient->MoveViewportCamera(FVector(-1500.0f, 0.0f, 700.0f), FRotator(0.0f, 90.0f, 0.0f));
LevelViewportClient->SetViewLocationForOrbiting(FVector(-1500.0f, 0.0f, 700.0f));
LevelViewportClient->ViewFOV = 90.0f;

return LevelViewportClient.ToSharedRef();
}

두개가 에디터 뷰포트 클래스에서 하는 내용입니다.

 

그외에 기타 기능을 구현할 수도 있지만 현재는 아무것도 구현되어 있지 않습니다.

 

 

 

프리뷰씬 생성자

//프리뷰씬 기본 세팅
FTesePreviewScene::FTesePreviewScene():FPreviewScene()
{

PreviewLevelName = { FString("/Game/EditorResources/PreviewLevel") };
//월드 세팅
//////아래 코드는 콘텐츠 내 특정 레벨을 불러오는 내용입니다.
EditorLevelUtils::AddLevelsToWorld(GetWorld(), PreviewLevelName, ULevelStreamingDynamic::StaticClass());

 

기본 월드세팅입니다.
//GetWorld()->SetCurrentLevel();
GetWorld()->GetWorldSettings()->NotifyBeginPlay();
GetWorld()->GetWorldSettings()->NotifyMatchStarted();
GetWorld()->GetWorldSettings()->SetActorHiddenInGame(false);
GetWorld()->bBegunPlay = true; 

 

////////아래 코드는 레벨을 불러오지 않더라도 기본월드에 라이트등을 세팅해주는 코드 입니다.

제외하더라도 기본 레벨을 불러온다면 레벨에 포함된 라이트 등이 적용되므로 필요에따라적용할 수 있습니다.


// set light options 
DirectionalLight->SetRelativeLocation(FVector(-1024.f, 1024.f, 2048.f));
DirectionalLight->SetRelativeScale3D(FVector(15.f));

SetLightBrightness(4.f);
DirectionalLight->InvalidateLightingCache();
DirectionalLight->RecreateRenderState_Concurrent();

// creae a sky sphere

UStaticMeshComponent* SkyComp = NewObject<UStaticMeshComponent>();
UStaticMesh* StaticMesh = LoadObject<UStaticMesh>(NULL, TEXT("/Engine/MapTemplates/Sky/SM_SkySphere.SM_SkySphere"), NULL, LOAD_None, NULL);
SkyComp->SetStaticMesh(StaticMesh);
UMaterial* SkyMaterial = LoadObject<UMaterial>(NULL, TEXT("/Engine/EditorMaterials/PersonaSky.PersonaSky"), NULL, LOAD_None, NULL);
SkyComp->SetMaterial(0, SkyMaterial);
const float SkySphereScale = 1000.f;
const FTransform SkyTransform(FRotator(0, 0, 0), FVector(0, 0, 0), FVector(SkySphereScale));
AddComponent(SkyComp, SkyTransform);



// now add floor
UStaticMesh* FloorMesh = LoadObject<UStaticMesh>(NULL, TEXT("/Engine/EditorMeshes/EditorCube.EditorCube"), NULL, LOAD_None, NULL);
UStaticMeshComponent* FloorComp = NewObject<UStaticMeshComponent>();
FloorComp->SetStaticMesh(FloorMesh);
AddComponent(FloorComp, FTransform::Identity);
FloorComp->SetRelativeScale3D(FVector(3.f, 3.f, 1.f));
UMaterial* Material = LoadObject<UMaterial>(NULL, TEXT("/Engine/EditorMaterials/PersonaFloorMat.PersonaFloorMat"), NULL, LOAD_None, NULL);
FloorComp->SetMaterial(0, Material);
FloorComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
FloorComp->SetCollisionObjectType(ECC_WorldStatic);

}

 

 

뷰포트 클라이언트 생성자

//뷰포트 클라이언트 레퍼런스 생성

FTeseViewPortClient::FTeseViewPortClient(FPreviewScene& InPreviewScene, const TSharedRef<STestViewPort>& InViewport)

: FEditorViewportClient(nullptr, &InPreviewScene, StaticCastSharedRef<SEditorViewport>(InViewport))

 

뷰포트 클라이언트의 레퍼런스를 만들어줍니다

 

: FEditorViewportClient(nullptr, &InPreviewScene, StaticCastSharedRef(InViewport)) 구문에서 프리뷰씬의 참조와 뷰포트 클래스 레퍼런스가 미리 설정 되어 있어야 합니다.

 

 

하단코드는 생성될때 설정된 초기 값입니다.

 

{
SetViewMode(VMI_Lit);
SetRealtime(true);
EngineShowFlags.DisableAdvancedFeatures();
EngineShowFlags.SetSnap(0);
EngineShowFlags.CompositeEditorPrimitives = true;
OverrideNearClipPlane(1.0f);
bUsingOrbitCamera = true;
}

 

//뷰포트 클라이언트 기능
void FTeseViewPortClient::Draw(FViewport* InViewport, FCanvas* Canvas)
{
FEditorViewportClient::Draw(InViewport, Canvas);

}
//화면을 그려주는 명령


void FTeseViewPortClient::Tick(float DeltaSeconds)
{
if (!GIntraFrameDebuggingGameThread)
{
PreviewScene->GetWorld()->Tick(LEVELTICK_All, DeltaSeconds);
}

틱이 실행되는 명령

}

 

위 코드를 적용하면 맨처음 나왓던 에디터에 독립된 창에 뷰포트를 띄워둘 수 있습니다.

또한 뷰포트와 뷰포트 클라이언트 생성자에서 요구하는 레퍼런스들만 잘 맞춰준다면 어렵지 않게 뷰포트를 띄울 수 있습니다.

 

이제 마지막으로

뷰포트에서 액터를 스폰하는 과정을 진행하도록 하겠습니다.(자동 소환)

 

tabtoolpanel.cpp에서 버튼과 윈도우를 만들어준 부분 하단에 소환하는 코드를 집어 넣습니다.

 

 

FViewportClient* ActiveViewPort = GEditor->GetActiveViewport()->GetClient();//일반 에디터 뷰포트입니다.

//블루프린트 스폰

//c++코드는 include 후 생성해주면되고 블루프린트의 경우 아래와 같이 staticClass 를 통해 맞는 클래스를 선언해주어야 합니다.
FName path = TEXT("Blueprint'/Game/Resources/AnimBlueprints/Helper/BakeTActor.BakeTActor'");
UBlueprint* ObjectToSpawn = Cast<UBlueprint>(StaticLoadObject(UBlueprint::StaticClass(), NULL, *path.ToString()));

 

스폰할 위치에 사용될 파라메터들 기본값
FActorSpawnParameters spawnParams;
FRotator rotator;
FVector spawnLocation = FVector::ZeroVector;

둘다 같은내용입니다.

 

뷰포트 클라이언트를 얻어 그 월드에 액터를 스폰하는 내용입니다.
ActiveViewPort->GetWorld()->SpawnActor<AActor>(ObjectToSpawn->GeneratedClass, spawnLocation, rotator, spawnParams);//일반 뷰포트에 소환


viewportPtr->GetViewportClient()->GetWorld()->SpawnActor<AActor>(ObjectToSpawn->GeneratedClass, spawnLocation, rotator, spawnParams);//커스텀뷰포트에 소환 

 

다음은 히트 프록시 를 사용한 오브젝트 픽을 제작중이며 마무리가되면 또 튜토리얼을 남길 수 있으면 좋겠습니다 

조금이라도 도움이 되는 분이 있길 바랍니다.