Unreal Engine Custom Asset & Editor System
커스텀 에셋과 에디터에 대한 기본적인 구현 방법을 학습했으며,
추 후 협업 환경에서 디자이너가 필요한 데이터만 모아 커스텀 에셋으로 관리하는 것도 가능해집니다.
UObject 기반 커스텀 에셋 정의 및 에셋 타입 액션 구현
Slate 기반 에디터 레이아웃 설계 및 뷰포트/디테일 탭 구현
디테일 탭의 값 수정 시 뷰포트에 즉시 반영되도록 구현
CustomMesh 모듈에서 UObject를 상속받는 커스텀 에셋 클래스를 정의합니다.
CustomMeshEditor 모듈에서 FCustomMeshAssetTypeActions 클래스를 구현합니다.
class FCustomMeshAssetTypeActions : public FAssetTypeActions_Base { public: // IAssetTypeActions interface virtual UClass* GetSupportedClass() const override; virtual FText GetName() const override; virtual FColor GetTypeColor() const override; virtual uint32 GetCategories() override; virtual void OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor) override; // End of IAssetTypeActions interface };
에셋 타입 액션은 UObject에서 파생되지 않으므로 수동으로 등록해야 합니다.
플러그인 활성화 시 AssetToolsModule에 등록하도록 StartupModule() 함수에 구현합니다.
void FCustomMeshEditorModule::StartupModule() { CustomMeshAssetTypeActions = MakeShared<FCustomMeshAssetTypeActions>(); FAssetToolsModule::GetModule().Get().RegisterAssetTypeActions(CustomMeshAssetTypeActions.ToSharedRef()); ... }
UFactory를 상속받는 클래스를 구현하여 콘텐츠 브라우저에서 새 에셋을 생성할 수 있도록 합니다.
class UCustomMeshFactory : public UFactory { ... virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; }; UObject* UCustomMeshFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) { return NewObject<UCustomMesh>(InParent, Class, Name, Flags, Context); }
클래스 구현만으로 자동으로 브라우저에 표시됩니다
에셋을 더블 클릭하면 OpenAssetEditor가 호출되어 커스텀 에디터가 실행됩니다.
에셋 실행 시 OpenAssetEditor에서 커스텀 에디터의 InitEditor를 호출합니다.
void FCustomMeshAssetTypeActions::OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor) { MakeShared<FCustomMeshEditor>()->InitEditor(EToolkitMode::Standalone, TSharedPtr<IToolkitHost>(), InObjects); UE_LOG(LogTemp, Error, TEXT("OpenEditor")); GEditor->RedrawAllViewports(); }
void FCustomMeshEditor::InitEditor(const EToolkitMode::Type Mode, const TSharedPtr< class IToolkitHost >& InitToolkitHost, const TArray<UObject*>& InCustomMesh) { ... FAssetEditorToolkit::InitAssetEditor(Mode, InitToolkitHost, UCustomMeshAppIdentifier, EditorDefaultLayout.ToSharedRef(), bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, InCustomMesh); }
FAssetEditorToolkit의 InitAssetEditor를 통해 에디터를 초기화하며, EditorDefaultLayout이 에디터의 레이아웃을 결정합니다.
Slate 문법을 사용하여 에디터 레이아웃을 구성합니다.
void FCustomMeshEditor::CreateLayouts() { EditorDefaultLayout = FTabManager::NewLayout("CustomMeshEditor_Layout") ->AddArea ( FTabManager::NewPrimaryArea()->SetOrientation(Orient_Horizontal) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.4f) ->AddTab(FCustomMeshEditorTabs::ViewportTabId, ETabState::OpenedTab) ) ->Split ( FTabManager::NewSplitter()->SetOrientation(Orient_Vertical) ->SetSizeCoefficient(0.3f) ->Split ( FTabManager::NewStack() ->AddTab(FCustomMeshEditorTabs::DetailTabId, ETabState::OpenedTab) ) ) ); }
첫 번째 탭은 ViewportTabId, 두 번째 탭은 DetailTabId로 설정했으며, 각 탭의 구체적인 형태는 RegisterTabSpawners 함수에서 구현합니다.
void FCustomMeshEditor::RegisterTabSpawners(const TSharedRef<class FTabManager>& inTabManager) { WorkspaceMenuCategory = TabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_CustomMeshAssetEditor", "CustomMesh Asset Editor")); const auto WorkspaceMenuCategoryRef = WorkspaceMenuCategory.ToSharedRef(); FAssetEditorToolkit::RegisterTabSpawners(inTabManager); TabManager->RegisterTabSpawner(FCustomMeshEditorTabs::ViewportTabId, FOnSpawnTab::CreateSP(this, &FCustomMeshEditor::SpawnTab_Viewport)) .SetGroup(WorkspaceMenuCategoryRef) .SetIcon(FSlateIcon(FExtensionStyle::GetStyleSetName(), "Extensions.Command1")); TabManager->RegisterTabSpawner(FCustomMeshEditorTabs::DetailTabId, FOnSpawnTab::CreateSP(this, &FCustomMeshEditor::SpawnTab_Detail)) .SetGroup(WorkspaceMenuCategoryRef) .SetIcon(FSlateIcon(FExtensionStyle::GetStyleSetName(), "Extensions.Command2")); }
SpawnTab_Viewport와 SpawnTab_Detail 함수에서 Slate 문법으로 각 탭의 형태를 결정합니다.
TSharedRef<SDockTab> FCustomMeshEditor::SpawnTab_Viewport(const FSpawnTabArgs& Args) const { check(Args.GetTabId() == FCustomMeshEditorTabs::ViewportTabId) return SNew(SDockTab) [ Viewport.ToSharedRef() ]; } TSharedRef<SDockTab> FCustomMeshEditor::SpawnTab_Detail(const FSpawnTabArgs& Args) { check(Args.GetTabId() == FCustomMeshEditorTabs::DetailTabId) return SNew(SDockTab) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(5) [ DetailsView.ToSharedRef() ] ]; }
프로퍼티 모듈을 사용하여 DetailsView를 설정합니다.
void FCustomMeshEditor::CreateDetailsView() { //Create Detail View constexpr bool bIsUpdatable = false; constexpr bool bAllowFavorites = true; constexpr bool bIsLockable = false; //프로퍼티 모듈 = 오브젝트의 상세 값을 설정할 수 있게 해줌. FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor"); //디테일 뷰 생성 const FDetailsViewArgs DetailsViewArgs(bIsUpdatable, bIsLockable, true, FDetailsViewArgs::ObjectsUseNameArea, false); DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs); //디테일 뷰에 CustomMesh 객체 설정 if (DetailsView.IsValid() == true) { DetailsView->SetObject(CustomMeshBeingEdited); } }
SEditorViewport를 상속받는 클래스를 구현하여 3D 프리뷰를 표시합니다.
class SCustomMeshViewport : public SEditorViewport, public FGCObject { .. // SEditorViewport interface virtual TSharedRef<FEditorViewportClient> MakeEditorViewportClient() override; virtual void BindCommands() override; // End of SEditorViewport interface }; TSharedRef<FEditorViewportClient> SCustomMeshViewport::MakeEditorViewportClient() { TypedViewportClient = MakeShareable(new FCustomMeshViewportClient(CustomMeshEditorPtr, SharedThis(this), CustomMeshBeingEdited)); return TypedViewportClient.ToSharedRef(); }
ViewportClient에서 PreviewScene을 초기화하여 실제 프리뷰를 렌더링합니다.
FCustomMeshViewportClient::FCustomMeshViewportClient(const TWeakPtr<class FCustomMeshEditor>& ParentCustomMeshEditor, const TSharedRef<class SCustomMeshViewport>& CustomMeshViewport, UCustomMesh* ObjectToEdit) : FEditorViewportClient(nullptr, nullptr, StaticCastSharedRef<SEditorViewport>(CustomMeshViewport)) , CustomMeshEditorPtr(ParentCustomMeshEditor) , CustomMeshEditorViewportPtr(CustomMeshViewport) , CustomMeshBeingEdited(ObjectToEdit) { const FPreviewScene::ConstructionValues ConstructionValue; AdvancedPreviewScene = new FAdvancedPreviewScene(ConstructionValue, 0); PreviewScene = AdvancedPreviewScene; // Viewport Setting FEditorViewportClient::SetViewMode(VMI_Lit); // Preview Camera Setting SetViewLocation(FVector(0.0f, 0.0f, 3.0f)); SetViewRotation(FRotator(-30.0f, -90.0f, 0.0f)); SetViewLocationForOrbiting(FVector::ZeroVector, 1000.0f); // Preview Mesh Setting UStaticMesh* StaticMesh = CustomMeshBeingEdited->Mesh; UMaterialInterface* Material = CustomMeshBeingEdited->Material; const FTransform Transform = CustomMeshBeingEdited->Transform; PreviewMeshComponent = NewObject<UStaticMeshComponent>(GetTransientPackage(), NAME_None, RF_Transient); PreviewMeshComponent->SetStaticMesh(StaticMesh); PreviewMeshComponent->SetMaterial(0, Material); PreviewMeshComponent->SetSimulatePhysics(true); AdvancedPreviewScene->AddComponent(PreviewMeshComponent, Transform); }
스카이박스와 바닥이 기본적으로 제공되어 더 나은 프리뷰 환경을 구성할 수 있습니다.
디테일 탭에 표시되는 값들은 CustomMesh 모듈의 UCustomMesh 프로퍼티들입니다.
class CUSTOMMESH_API UCustomMesh : public UObject { GENERATED_BODY() UCustomMesh(); virtual ~UCustomMesh() override; public: UPROPERTY(EditAnywhere, Category = "Mesh") FTransform Transform; UPROPERTY(EditAnywhere, Category = "Mesh", meta = (DisplayName = "Static Mesh")) UStaticMesh* Mesh; UPROPERTY(EditAnywhere, Category = "Mesh", meta = (DisplayName = "Material")) UMaterialInterface* Material; public: DECLARE_MULTICAST_DELEGATE_OneParam(FPropertyChanged, struct FPropertyChangedEvent&); FPropertyChanged OnPropertyChanged; private: virtual void PostEditChangeProperty(struct FPropertyChangedEvent& P ropertyChangedEvent) override;
프로퍼티 값 변경 시 PostEditChangeProperty가 호출되며, OnPropertyChanged 이벤트를 에디터에서 처리합니다.
void FCustomMeshEditor::InitEditor(const EToolkitMode::Type Mode, const TSharedPtr< class IToolkitHost >& InitToolkitHost, const TArray<UObject*>& InCustomMesh) { //Set Being Edited Object CustomMeshBeingEdited = Cast<UCustomMesh>(InCustomMesh[0]); // 다중 선택한 것 중 첫번째 CustomMeshBeingEdited->SetFlags(RF_Transactional); //Undo, Redo 지원 CustomMeshBeingEdited->OnPropertyChanged.AddRaw(this, &FCustomMeshEditor::OnPropertyChanged); //프로퍼티 변경 감지
OnPropertyChanged 함수에서 프리뷰 메시를 업데이트하여 변경사항을 즉시 반영합니다.
void FCustomMeshEditor::OnPropertyChanged(FPropertyChangedEvent& PropertyChangedEvent) { if (PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(UCustomMesh, Mesh)) { if (UStaticMesh* Mesh = CustomMeshBeingEdited->Mesh; Mesh != nullptr) { Viewport->GetPreviewMesh()->SetStaticMesh(Mesh); } else { Viewport->GetPreviewMesh()->SetStaticMesh(nullptr); } } ... }