sidian 发表于 2023-3-19 17:59

《调教UE5:编辑器拓展指南》自定义世界大纲

《调教UE5》系列记录的是笔者在开发UE引擎中总结的些许经验。文中所有观点和结论仅代表个人见解,作为自学笔记和日后反思之参考,亦可能存在谬误或过时之处。如有错漏和不当,恳请多加指正。<hr/><hr/>目录

不可忽略的事前准备 ~ ♡
ㅤ创建 ExtendSceneOutliner 模块
ㅤ创建 ExtendSceneOutlinerStyle 样式集
注册 ISceneOutlinerColumn ~ ♡
ㅤ创建 SceneOutlinerLockColumn 类
ㅤ注册 SceneOutlinerLockColumn 类
实现 ConstructRowWidget ~ ♡
程序示例 ~ ♡
ㅤ锁定 Actor 移动
ㅤㅤ单件锁定移动
ㅤㅤ批量锁定移动
ㅤ锁定 Actor 选择
ㅤㅤ单件锁定选择
ㅤㅤ批量锁定选择<hr/>\large\textbf{若是繁星的孩子,定不会被这点须臾的小事难倒。} \\
<hr/>本章将探索自定义世界大纲的方法。
有关拓展世界大纲模块的方法相关的资料较少,笔者仅从实用的角度编写此章,尚未深度探索源码。在此总结的方法难免存在疏漏,权作抛砖引玉之能效,如有理解不当之处,恳请多加指正。
不可忽略的事前准备 ~ ♡

在正式开始之前,我们来进行一些不可或缺的预前准备。
目前,本指南基于UE5.0.3引擎版本进行编写,读者需前往下载对应的引擎版本,以免在尝试过程中因引擎版本差异造成不必要的麻烦。
为了拥有相对易读的内容,我们来为本章内容准备一个独立的模块。
创建 ExtendSceneOutliner 模块

在项目 Source 文件夹下新建“ExtendSceneOutliner”文件夹,并组织如下文件结构。我们不打算将此模块用于其他模块中,因此可以不需要“Public”和“Private”文件夹。



来到 ExtendSceneOutliner.h 中声明模块。
// ExtendSceneOutliner.h

#pragma once

#include "Modules/ModuleInterface.h"

class FExtendSceneOutliner : public IModuleInterface
{
public:

        virtual void StartupModule() override;
        virtual void ShutdownModule() override;
        virtual ~FExtendSceneOutliner() {}
};

来到 ExtendSceneOutliner.cpp 中实现模块。
// ExtendSceneOutliner.cpp

#pragma once

#include "ExtendSceneOutliner.h"

IMPLEMENT_MODULE(FExtendSceneOutliner, ExtendSceneOutliner)

void FExtendSceneOutliner::StartupModule()
{
        IModuleInterface::StartupModule();
}

void FExtendSceneOutliner::ShutdownModule()
{
        IModuleInterface::ShutdownModule();
}

来到 ExtendSceneOutliner.Build.cs 中设置模块依赖项。
// ExtendSceneOutliner.Build.cs

using UnrealBuildTool;

public class ExtendSceneOutliner : ModuleRules
{
        public ExtendSceneOutliner(ReadOnlyTargetRules Target) : base(Target)
        {
                PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
       
                PublicDependencyModuleNames.AddRange(new string[]
                {
                        "Core",
                        "CoreUObject",
                        "Engine",
                        "InputCore",
                        "SceneOutliner",
                        "Slate",
                        "SlateCore",
                        "UnrealEd"
                });

                PrivateDependencyModuleNames.AddRange(new string[] { });
        }
}

来到 Editor.Target.cs 文件中,添加项目模块依赖。此处我们的项目名为“ExtendEditor”。
// ExtendEditorEditor.Target.cs

using UnrealBuildTool;
using System.Collections.Generic;

public class ExtendEditorEditorTarget : TargetRules
{
        public ExtendEditorEditorTarget( TargetInfo Target) : base(Target)
        {
                Type = TargetType.Editor;
                DefaultBuildSettings = BuildSettingsVersion.V2;
                ExtraModuleNames.AddRange( new string[]
                {
                        "ExtendEditor",
                        "ExtendSceneOutliner"
                } );
        }
}

来到 .uproject 文件中,配置模块的启动方式。
// ExtendEditor.uproject

{
        "FileVersion": 3,
        "EngineAssociation": "5.0",
        "Category": "",
        "Description": "",
        "Modules": [
                {
                        "Name": "ExtendEditor",
                        "Type": "Runtime",
                        "LoadingPhase": "Default"
                },
                {
                        "Name": "ExtendSceneOutliner",
                        "Type": "Editor",
                        "LoadingPhase": "Default"
                }
        ],
        "Plugins": [
                {
                        "Name": "ModelingToolsEditorMode",
                        "Enabled": true,
                        "TargetAllowList": [
                                "Editor"
                        ]
                }
        ]
}

重启代码编辑器并编译,完成“FExtendSceneOutliner”模块创建。

{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

创建 ExtendSceneOutlinerStyle 样式集

接着来创建一个样式集,以供在 SceneOutliner 的界面上显示图标。



可以在 《调教UE5:编辑器拓展指南》编辑器拓展基础 中的“FSlateStyleSet”小节查看有关样式集的详细说明,此处不再重复展开。

在 ExtendSceneOutliner 文件夹下新建文件 ExtendSceneOutlinerStyle.h 和 ExtendSceneOutlinerStyle.cpp。



来到 ExtendSceneOutlinerStyle.h 中声明 FExtendSceneOutlinerStyle 类。
// ExtendSceneOutlinerStyle.h

#pragma once

class FExtendSceneOutlinerStyle
{
public:
        // 模块启动时,注册该样式集到中央管理库
        static void Initialize();

        static FName GetStyleSetName();

        static TSharedPtr<FSlateStyleSet> GetStyleSet();

private:
       
        static TSharedRef<FSlateStyleSet> CreateSlateStyleSet();

private:
       
        inline static TSharedPtr<FSlateStyleSet> StyleSet = nullptr;
        inline static const FName StyleSetName = FName("ExtendSceneOutlinerStyle");
};

来到 ExtendSceneOutlinerStyle.cpp 中实现 FExtendSceneOutlinerStyle 类。我们将图标资源放在项目文件夹下的“Resource”文件夹下。该文件夹需要自己创建。



// ExtendSceneOutlinerStyle.cpp

#pragma once

#include "ExtendSceneOutlinerStyle.h"

#include "Styling/SlateStyle.h"
#include "Styling/SlateStyleRegistry.h"
#include "Styling/StyleColors.h"

FName FExtendSceneOutlinerStyle::GetStyleSetName()
{
        return StyleSetName;
}

TSharedPtr<FSlateStyleSet> FExtendSceneOutlinerStyle::GetStyleSet()
{
        return StyleSet;
}

void FExtendSceneOutlinerStyle::Initialize()
{
        if(!StyleSet.IsValid())
        {
                StyleSet = CreateSlateStyleSet();
                FSlateStyleRegistry::RegisterSlateStyle(*StyleSet);
        }
}

TSharedRef<FSlateStyleSet> FExtendSceneOutlinerStyle::CreateSlateStyleSet()
{
        TSharedRef<FSlateStyleSet> SlateStyleSet = MakeShareable(new FSlateStyleSet(StyleSetName));

        const FString RootPath = FPaths::ProjectDir() + TEXT("/Resource/");
        SlateStyleSet->SetContentRoot(RootPath);
       
        {
                const FVector2D IconeSize(16.f, 16.f);
                FSlateImageBrush* SlateImageBrush = new FSlateImageBrush(RootPath + TEXT("Lock.png"), IconeSize);
                SlateStyleSet->Set("SceneOutliner.Lock", SlateImageBrush);
        }

        {
                const FVector2D IconeSize(16.f, 16.f);
                FSlateImageBrush* SlateImageBrush = new FSlateImageBrush(RootPath + TEXT("Unlock.png"), IconeSize);
                SlateStyleSet->Set("SceneOutliner.Unlock", SlateImageBrush);
        }

        {
                const FVector2D IconeSize(16.f, 16.f);
                const FCheckBoxStyle SelectionLockToggleButtonStyle =
                   FCheckBoxStyle()
                   .SetCheckBoxType(ESlateCheckBoxType::ToggleButton)
                   .SetPadding(FMargin(10.f))
                   .SetUncheckedImage(FSlateImageBrush(RootPath + TEXT("/Unlock.png"), IconeSize, FStyleColors::White25))
                   .SetUncheckedHoveredImage(FSlateImageBrush(RootPath + TEXT("/Unlock.png"), IconeSize, FStyleColors::AccentBlue))
                   .SetUncheckedPressedImage(FSlateImageBrush(RootPath + TEXT("/Unlock.png"), IconeSize, FStyleColors::Foreground))
                   .SetCheckedImage(FSlateImageBrush(RootPath + TEXT("/Lock.png"), IconeSize, FStyleColors::Foreground))
                   .SetCheckedHoveredImage(FSlateImageBrush(RootPath + TEXT("/Lock.png"), IconeSize, FStyleColors::AccentBlack))
                   .SetCheckedPressedImage(FSlateImageBrush(RootPath + TEXT("/Lock.png"), IconeSize, FStyleColors::AccentGray));

                SlateStyleSet->Set("SceneOutliner.LockToggle", SelectionLockToggleButtonStyle);
        }
       
        return SlateStyleSet;
}

来到 ExtendSceneOutliner.cpp 的模块启动函数,将样式集初始化函数添加到其中。
// ExtendSceneOutliner.cpp

#include "ExtendSceneOutlinerStyle.h"

void FExtendSceneOutliner::StartupModule()
{
        FExtendSceneOutlinerStyle::Initialize();
}

至此我们的准备工作就完成了。

{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

注册 ISceneOutlinerColumn ~ ♡

SceneOutliner 的拓展是以“列”为单位进行的。ISceneOutlinerColumn 是我们用于拓展的接口,它允许我们以列为单位拓展 SceneOutliner 上的 Widget。



为了实现拓展,我们需要创建一个继承自 ISceneOutlinerColumn 的自定义类,并实现一些必须的函数。然后,我们将这个自定义类注册到 SceneOutlinerModule 管理的 ColumnMap 中。
创建 SceneOutlinerLockColumn 类

首先来创建 FSceneOutlinerLockColumn 类。在 ExtendSceneOutliner 文件夹下新建文件 SceneOutlinerLockColumn.h 和 SceneOutlinerLockColumn.cpp。



来到 SceneOutlinerLockColumn.h 中,声明 FSceneOutlinerLockColumn 类。
FSceneOutlinerLockColumn 需要包含三个必要的重载函数,和一个构造函数。但除此之外我们还需要提供一个 GetID() 静态函数,否则在注册 FSceneOutlinerLockColumn 会提示找不到该函数。



ConstructHeaderRowColumn() 和 ConstructRowWidget() 是两个重要函数。
ConstructHeaderRowColumn() 帮助我们在标题头生成新的 Widget。
ConstructRowWidget() 帮助我们在每个项目行生成新的 Widget。



// SceneOutlinerLockColumn.h

#pragma once

#include "ISceneOutlinerColumn.h"

class FSceneOutlinerLockColumn : public ISceneOutlinerColumn
{
public:

        FSceneOutlinerLockColumn(ISceneOutliner& SceneOutliner) {}

        static FName GetID() {return FName("SceneOutlinerExtendColumn");}

        virtual FName GetColumnID() override {return GetID();}

        // 在标题头添加新 Widget
        virtual SHeaderRow::FColumn::FArguments ConstructHeaderRowColumn() override;

        // 在项目行添加新 Widget
        virtual const TSharedRef<SWidget> ConstructRowWidget(FSceneOutlinerTreeItemRef TreeItem, const STableRow<FSceneOutlinerTreeItemPtr>& Row) override;
};

来到 SceneOutlinerLockColumn.cpp 实现 FSceneOutlinerLockColumn 类。
我们在标题头处添加一个 SImage 小部件,并在每个项目行暂时添加一个空的小部件。
// SceneOutlinerLockColumn.cpp

#pragma once

#include "SceneOutlinerLockColumn.h"

#include "ExtendSceneOutlinerStyle.h"
#include "Styling/SlateStyle.h"

SHeaderRow::FColumn::FArguments FSceneOutlinerLockColumn::ConstructHeaderRowColumn()
{
        SHeaderRow::FColumn::FArguments ConstructedHeaderRowColumn =
                SHeaderRow::Column(GetColumnID())
                .FixedWidth(24.f)
                .HAlignHeader(HAlign_Center)
                .VAlignHeader(VAlign_Center)
                .HAlignCell(HAlign_Center)
                .VAlignCell(VAlign_Center)
                .DefaultTooltip(FText::FromString(TEXT("Lock the transformation of actor")))
                [
                        SNew(SImage)
                        .ColorAndOpacity(FSlateColor::UseForeground())
                        .Image(FExtendSceneOutlinerStyle::GetStyleSet().Get()->GetBrush("SceneOutliner.Lock"))
                ];

        return ConstructedHeaderRowColumn;
}

const TSharedRef<SWidget> FSceneOutlinerLockColumn::ConstructRowWidget(FSceneOutlinerTreeItemRef TreeItem,
        const STableRow<FSceneOutlinerTreeItemPtr>& Row)
{
        // 暂时返回一个空 Widget
        return SNullWidget::NullWidget;
}

注册 SceneOutlinerLockColumn 类

然后我们来到 ExtendSceneOutliner 文件中为这个 Column 进行注册。
我们要将 FSceneOutlinerColumnInfo 添加到 SceneOutlinerModule 管理的 ColumnMap 中。FSceneOutlinerColumnInfo 记录了该 Column 的可见性,应该出现的位置等等。
// ExtendSceneOutliner.h

#pragma once

#include "Modules/ModuleInterface.h"

class FExtendSceneOutliner : public IModuleInterface
{
public:

        virtual void StartupModule() override;
        virtual void ShutdownModule() override;
        virtual ~FExtendSceneOutliner() {}

private:

        void InitSceneOutlinerColumn();
        void UninitSceneOutlinerColumn();

        TSharedRef<class ISceneOutlinerColumn> OnCreateSceneOutlinerLockColumnInfo(class ISceneOutliner& SceneOutliner);
};

// ExtendSceneOutliner.cpp

#include "Modules/ModuleManager.h"
#include "SceneOutlinerModule.h"
#include "SceneOutlinerLockColumn.h"

void FExtendSceneOutliner::StartupModule()
{
        IModuleInterface::StartupModule();

        FExtendSceneOutlinerStyle::Initialize();
        InitSceneOutlinerColumn();
}

void FExtendSceneOutliner::ShutdownModule()
{
        IModuleInterface::ShutdownModule();

        UninitSceneOutlinerColumn();
        FExtendSceneOutlinerStyle::Uninitialize();
}

void FExtendSceneOutliner::InitSceneOutlinerColumn()
{
        FSceneOutlinerColumnInfo SceneOutlinerLockColumnInfo(
                ESceneOutlinerColumnVisibility::Visible,// 可见性
                1,                                          // Column 出现的位置
                FCreateSceneOutlinerColumn::CreateRaw(
                        this,
                        &FExtendSceneOutliner::OnCreateSceneOutlinerLockColumnInfo)
                );
       
        FSceneOutlinerModule& SceneOutlinerModule =
                FModuleManager::LoadModuleChecked<FSceneOutlinerModule>(TEXT("SceneOutliner"));
       
        SceneOutlinerModule.RegisterDefaultColumnType<FSceneOutlinerLockColumn>(SceneOutlinerLockColumnInfo);
}

TSharedRef<ISceneOutlinerColumn> FExtendSceneOutliner::OnCreateSceneOutlinerLockColumnInfo(
        ISceneOutliner& SceneOutliner)
{
        return MakeShareable(new FSceneOutlinerLockColumn(SceneOutliner));
}

void FExtendSceneOutliner::UninitSceneOutlinerColumn()
{
        FSceneOutlinerModule& SceneOutlinerModule =
                FModuleManager::LoadModuleChecked<FSceneOutlinerModule>(TEXT("SceneOutliner"));
       
        SceneOutlinerModule.UnRegisterColumnType<FSceneOutlinerLockColumn>();
}

编译并重启编辑器,可以看到在 SceneOutliner 上添加了一个小图标。



{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

实现 ConstructRowWidget ~ ♡

先来实现一个简单的 ConstructRowWidget。我们的目标是添加一个切换开关按钮,当按钮在两个状态之间切换时打印不同的提示消息,并注明是哪一个 Actor 上的开关发生了变化。
首先,我们要使用 ConstructRowWidget() 传入的 FSceneOutlinerTreeItemRef 参数,它的类型是 ISceneOutlinerTreeItem。



它是一个树状结构节点,有以下这些子类。



我们要从输入中检索 Actor 对象,因此先将其转换为 FActorTreeItem。转换完毕之后还不能直接使用,否则在引擎初始化阶段还没有任何具体项目,会造成空指针错误,所以需要检查一下是否转换成功,否则就返回一个空 Widget。
// SceneOutlinerLockColumn.cpp

const TSharedRef<SWidget> FSceneOutlinerLockColumn::ConstructRowWidget(FSceneOutlinerTreeItemRef TreeItem,
        const STableRow<FSceneOutlinerTreeItemPtr>& Row)
{
        FActorTreeItem* ActorTreeItem = TreeItem->CastTo<FActorTreeItem>();
        // 检查是否转换成功
        if(!ActorTreeItem || !ActorTreeItem->IsValid()) return SNullWidget::NullWidget;
       
        const FCheckBoxStyle& ToggleButtonStyle =
                FExtendSceneOutlinerStyle::GetStyleSet()->GetWidgetStyle<FCheckBoxStyle>(
                        FName("SceneOutliner.LockToggle"));
       
        TSharedRef<SWidget> ConstructedRowWidget =
                SNew(SCheckBox)
                .Visibility(EVisibility::Visible)
                .Type(ESlateCheckBoxType::ToggleButton)
                .Style(&ToggleButtonStyle)
                .HAlign(HAlign_Center)
                .IsChecked(ECheckBoxState::Unchecked)
                .OnCheckStateChanged(
                        this,
                        &FSceneOutlinerLockColumn::OnLockToggleStateChanged, ActorTreeItem->Actor);
       
        return ConstructedRowWidget;
}



void FSceneOutlinerLockColumn::OnLockToggleStateChanged(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor)
{
        if(NewState == ECheckBoxState::Checked)
        {
                FString ActorName = Actor.Get()->GetActorLabel();
                GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Cyan, TEXT("Locked ") + ActorName);
               
                return;
        }

        if(NewState == ECheckBoxState::Unchecked)
        {
                FString ActorName = Actor.Get()->GetActorLabel();
                GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Cyan, TEXT("Unlock ") + ActorName);
               
                return;
        }
       
        return;
}

// SceneOutlinerLockColumn.h

#pragma once

#include "ISceneOutlinerColumn.h"

class FSceneOutlinerLockColumn : public ISceneOutlinerColumn
{
...
private:

        void OnLockToggleStateChanged(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);
};

编译并重启编辑器,可以看到相应项目类型的项目行按钮已经生成了。



{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

程序示例 ~ ♡

下面我们通过两个示例来演示 SceneOutliner 如何与场景内容互动,以及如何刷新 SceneOutliner 状态。
锁定 Actor 移动

我们希望通过大纲视图的按钮来快速锁定 Actor 移动,而不必每次通过 Transform 次级菜单来寻找 LockMovement 选项。
单件锁定移动

先仅考虑单个按钮的情况,当我们点击某个 FActorTreeItem 时,希望能够锁定该 Actor 的移动。
来到 SceneOutlinerLockColumn.h 中,添加一个新的函数。
// SceneOutlinerLockColumn.h

#pragma once

#include "ISceneOutlinerColumn.h"

class FSceneOutlinerLockColumn : public ISceneOutlinerColumn
{
...
private:

        void OnLockToggleStateChanged(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);

        // 替代上面事件的新函数
        void OnLockToggleStateChanged_LockMovement_SelectedActor(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);
};

来到 SceneOutlinerLockColumn.cpp 中实现新函数。在锁定或解锁一个 Actor 后,还需要刷新视口中 Actor 的选择状态以应用修改。
// SceneOutlinerLockColumn.cpp

#pragma once

#include "SceneOutlinerLockColumn.h"

#include "ExtendSceneOutlinerStyle.h"
#include "Styling/SlateStyle.h"
#include "ActorTreeItem.h"

#include "Subsystems/EditorActorSubsystem.h"

const TSharedRef<SWidget> FSceneOutlinerLockColumn::ConstructRowWidget(FSceneOutlinerTreeItemRef TreeItem,
        const STableRow<FSceneOutlinerTreeItemPtr>& Row)
{
        FActorTreeItem* ActorTreeItem = TreeItem->CastTo<FActorTreeItem>();

        if(!ActorTreeItem || !ActorTreeItem->IsValid()) return SNullWidget::NullWidget;
       
        // 检测 Actor 锁定移动的状态
        const bool bIsActorLocked = ActorTreeItem->Actor->IsLockLocation();
       
        const FCheckBoxStyle& ToggleButtonStyle =
                FExtendSceneOutlinerStyle::GetStyleSet()->GetWidgetStyle<FCheckBoxStyle>(
                        FName("SceneOutliner.LockToggle"));
       
        TSharedRef<SWidget> ConstructedRowWidget =
                SNew(SCheckBox)
                .Visibility(EVisibility::Visible)
                .Type(ESlateCheckBoxType::ToggleButton)
                .Style(&ToggleButtonStyle)
                .HAlign(HAlign_Center)
                .IsChecked(bIsActorLocked ? ECheckBoxState::Checked : ECheckBoxState::Unchecked)
                .OnCheckStateChanged(
                        this,
                        // 替换为新函数
                        &FSceneOutlinerLockColumn::OnLockToggleStateChanged_LockMovement_SelectedActor,
                        ActorTreeItem->Actor);
       
        return ConstructedRowWidget;
}

void FSceneOutlinerLockColumn::OnLockToggleStateChanged_LockMovement_SelectedActor(ECheckBoxState NewState,
        TWeakObjectPtr<AActor> Actor)
{
        UEditorActorSubsystem* EditorActorSubsystem = GEditor->GetEditorSubsystem<UEditorActorSubsystem>();

        if(NewState == ECheckBoxState::Checked)
        {
                // 锁定 Actor 移动
                Actor->SetLockLocation(true);

                // 刷新 Actor 选择状态
                EditorActorSubsystem->SetActorSelectionState(Actor.Get(), false);
                EditorActorSubsystem->SetActorSelectionState(Actor.Get(), true);
               
                return;
        }

        if(NewState == ECheckBoxState::Unchecked)
        {
                // 解锁 Actor 移动
                Actor->SetLockLocation(false);

                // 刷新 Actor 选择状态
                EditorActorSubsystem->SetActorSelectionState(Actor.Get(), false);
                EditorActorSubsystem->SetActorSelectionState(Actor.Get(), true);

                return;
        }
       
        return;
}

编译并重启编辑器,测试按钮效果。



{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

批量锁定移动

现在更进一步,我们希望实现批量选择的锁定移动。通过获取场景中的 ActorSelection,我们能够得到当前选中 Actor 的选择集,然后依次对每一个 Actor 进行设定操作。
一个新的问题是,此前在单件锁定中我们通过点击按钮就可以切换对应 FActorTreeItem 的锁定图标,但我们无法在同一时间逐个点击那么多个按钮,因此需要在点击操作时触发一次 SceneOutliner 刷新,并在ConstructRowWidget() 执行时自动检测 Actor 的锁定状态,并设置相应的按钮图标。
// SceneOutlinerLockColumn.h

#pragma once

#include "ISceneOutlinerColumn.h"

class FSceneOutlinerLockColumn : public ISceneOutlinerColumn
{
...
private:

        void OnLockToggleStateChanged(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);

        void OnLockToggleStateChanged_LockMovement_SelectedActor(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);

        // 替代上面事件的新函数
        void OnLockToggleStateChanged_LockMovement_SelectedActors(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);
};

// SceneOutlinerLockColumn.cpp

#pragma once

#include "SceneOutlinerLockColumn.h"

#include "ExtendSceneOutlinerStyle.h"
#include "Styling/SlateStyle.h"
#include "ActorTreeItem.h"

#include "Subsystems/EditorActorSubsystem.h"

#include "Selection.h"
#include "LevelEditor.h"
#include "ISceneOutliner.h"

const TSharedRef<SWidget> FSceneOutlinerLockColumn::ConstructRowWidget(FSceneOutlinerTreeItemRef TreeItem,
        const STableRow<FSceneOutlinerTreeItemPtr>& Row)
{
        FActorTreeItem* ActorTreeItem = TreeItem->CastTo<FActorTreeItem>();
       
        if(!ActorTreeItem || !ActorTreeItem->IsValid()) return SNullWidget::NullWidget;
       
        // 检查 Actor 的锁定状态
        const bool bIsActorLocked = ActorTreeItem->Actor->IsLockLocation();

        const FCheckBoxStyle& ToggleButtonStyle =
               FExtendSceneOutlinerStyle::GetStyleSet()->GetWidgetStyle<FCheckBoxStyle>(
                      FName("SceneOutliner.LockToggle"));
       
        TSharedRef<SWidget> ConstructedRowWidget =
                SNew(SCheckBox)
                .Visibility(EVisibility::Visible)
                .Type(ESlateCheckBoxType::ToggleButton)
                .Style(&ToggleButtonStyle)
                .HAlign(HAlign_Center)
                // 通过检查到的状态设置图标
                .IsChecked(bIsActorLocked ? ECheckBoxState::Checked : ECheckBoxState::Unchecked)
                .OnCheckStateChanged(
                      this,
                      // 替换为新函数
                      &FSceneOutlinerLockColumn::OnLockToggleStateChanged_LockMovement_SelectedActors,
                      ActorTreeItem->Actor);
       
        return ConstructedRowWidget;
}

void FSceneOutlinerLockColumn::OnLockToggleStateChanged_LockMovement_SelectedActors(ECheckBoxState NewState,
        TWeakObjectPtr<AActor> Actor)
{
        FLevelEditorModule& LevelEditorModule =
                FModuleManager::LoadModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));

        TSharedPtr<ISceneOutliner> SceneOutliner =
                LevelEditorModule.GetFirstLevelEditor()->GetSceneOutliner();

        UEditorActorSubsystem* EditorActorSubsystem =
                GEditor->GetEditorSubsystem<UEditorActorSubsystem>();

        // 获取 ActorSelection
        USelection* SelectedActors = GEditor->GetSelectedActors();

        if(NewState == ECheckBoxState::Checked)
        {
                // 如果没有选中任何 Actor,仅发生了按钮的点击事件
                if(SelectedActors->Num() == 0)
                {
                        Actor->SetLockLocation(true);
                        EditorActorSubsystem->SetActorSelectionState(Actor.Get(), false);
                        EditorActorSubsystem->SetActorSelectionState(Actor.Get(), true);
                }
                else
                {
                        for(FSelectionIterator It(*SelectedActors); It; ++It)
                        {
                                AActor* SelectedActor = Cast<AActor>( *It );

                                SelectedActor->SetLockLocation(true);
                        }

                        for(FSelectionIterator It(*SelectedActors); It; ++It)
                        {
                                AActor* SelectedActor = Cast<AActor>( *It );
                       
                                EditorActorSubsystem->SetActorSelectionState(SelectedActor, false);
                                EditorActorSubsystem->SetActorSelectionState(SelectedActor, true);
                        }
                }
               
                // 设置完毕后刷新 SceneOutliner 重新生成图标
                if(SceneOutliner.IsValid())
                {
                        SceneOutliner->FullRefresh();
                }
               
                return;
        }

        if(NewState == ECheckBoxState::Unchecked)
        {
                if(SelectedActors->Num() == 0)
                {
                        Actor->SetLockLocation(false);
                        EditorActorSubsystem->SetActorSelectionState(Actor.Get(), false);
                        EditorActorSubsystem->SetActorSelectionState(Actor.Get(), true);
                }
                else
                {
                        for(FSelectionIterator It(*SelectedActors); It; ++It)
                        {
                                AActor* SelectedActor = Cast<AActor>( *It );

                                SelectedActor->SetLockLocation(false);
                        }

                        for(FSelectionIterator It(*SelectedActors); It; ++It)
                        {
                                AActor* SelectedActor = Cast<AActor>( *It );
                       
                                EditorActorSubsystem->SetActorSelectionState(SelectedActor, false);
                                EditorActorSubsystem->SetActorSelectionState(SelectedActor, true);
                        }
                }
               
                if(SceneOutliner.IsValid())
                {
                        SceneOutliner->FullRefresh();
                }

                return;
        }
       
        return;
}

编译并重启编辑器,测试按钮效果。



{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

锁定 Actor 选择

有时候,我们希望在选择物体或者使用 Ctrl+Alt+鼠标左键框选 Actor 的时候,避免选中特定的对象。
我们来制作一个简易的锁定选择工具。
这里实现锁定选择的逻辑是通过 Actor 的 Tags 关键字。当我们希望禁用一个 Actor 的选择时,首先为这个 Actor 添加“Locked”关键字,随后,在选择事件中检查每个 Actor 的 Tags 是否存在“Locked”,如果存在则立即取消该 Actor 选择。
我们应该将这个检测的逻辑封装在一个单独的类中,但此处为了简便,就直接将逻辑写在 ExtendSceneOutliner 模块中。
来到 ExtendSceneOutliner.h 中,添加相关函数声明和变量声明。
// ExtendSceneOutliner.h

class FExtendSceneOutliner : public IModuleInterface
{
private:

        void InitCustomSelectionEvent();
        void UninitCustomSelectionEvent();
        void OnActorSelected(UObject* SelectedObject);
        bool GetEditorActorSubsystem();

public:

        bool CheckIsActorSelectionLocked(AActor* SelectedActor);
        void LockActorSelection(AActor* ActorToProcess);
        void UnlockActorSelection(AActor* ActorToProcess);
        void SetActorSelectionState(AActor* Actor, bool bShouldBeSelected);

private:
       
        TWeakObjectPtr<class UEditorActorSubsystem> WeakEditorActorSubsystem;
        FDelegateHandle SelectObjectEventHandle;
};

来到 ExtendSceneOutliner.cpp 中,实现相关函数。
这里的关键函数是 InitCustomSelectionEvent() 和 OnActorSelected(),前者负责将选择事件注册到 USelection::SelectObjectEvent 的回调中。
接着,我们就可以在其他地方利用 LockActorSelection() 和 UnlockActorSelection() 为 Actor 设置 Tags,利用 SetActorSelectionState() 来为 Actor 设置选定状态。
// ExtendSceneOutliner.cpp

#include "Selection.h"
#include "Subsystems/EditorActorSubsystem.h"

void FExtendSceneOutliner::InitCustomSelectionEvent()
{
        USelection* UserSelection = GEditor->GetSelectedActors();

        SelectObjectEventHandle =
                UserSelection->SelectObjectEvent.AddRaw(this, &FExtendSceneOutliner::OnActorSelected);
}

void FExtendSceneOutliner::UninitCustomSelectionEvent()
{
        USelection* UserSelection = GEditor->GetSelectedActors();
       
        UserSelection->SelectObjectEvent.Remove(SelectObjectEventHandle);
}

void FExtendSceneOutliner::OnActorSelected(UObject* SelectedObject)
{
        if(!GetEditorActorSubsystem()) return;
       
        if(AActor* SelectedActor = Cast<AActor>(SelectedObject))
        {
                if(CheckIsActorSelectionLocked(SelectedActor))
                {
                        WeakEditorActorSubsystem.Get()->SetActorSelectionState(SelectedActor, false);
                }
        }
}

bool FExtendSceneOutliner::GetEditorActorSubsystem()
{
        if(!WeakEditorActorSubsystem.IsValid())
        {
                WeakEditorActorSubsystem = GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
        }

        return WeakEditorActorSubsystem.IsValid();
}

bool FExtendSceneOutliner::CheckIsActorSelectionLocked(AActor* SelectedActor)
{
        if(!SelectedActor) return false;

        return SelectedActor->ActorHasTag(FName("Locked"));
}



void FExtendSceneOutliner::LockActorSelection(AActor* ActorToProcess)
{
        if(!ActorToProcess) return;

        if(!ActorToProcess->ActorHasTag(FName("Locked")))
        {
                ActorToProcess->Tags.Add(FName("Locked"));
        }
}

void FExtendSceneOutliner::UnlockActorSelection(AActor* ActorToProcess)
{
        if(!ActorToProcess) return;

        if(ActorToProcess->ActorHasTag(FName("Locked")))
        {
                ActorToProcess->Tags.Remove(FName("Locked"));
        }
}

void FExtendSceneOutliner::SetActorSelectionState(AActor* Actor, bool bShouldBeSelected)
{
        WeakEditorActorSubsystem->SetActorSelectionState(Actor, bShouldBeSelected);
}


单件锁定选择

首先依旧来考虑相对简单的情况。在 SceneOutlinerLockColumn.h 中添加函数。
// SceneOutlinerLockColumn.h

#pragma once

#include "ISceneOutlinerColumn.h"

class FSceneOutlinerLockColumn : public ISceneOutlinerColumn
{
...
private:

        void OnLockToggleStateChanged(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);

        void OnLockToggleStateChanged_LockMovement_SelectedActor(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);

        void OnLockToggleStateChanged_LockMovement_SelectedActors(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);

        // 替代上面事件的新函数
        void OnLockToggleStateChanged_LockSelection_SelectedActor(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);
};

// SceneOutlinerLockColumn.cpp

#pragma once

#include "SceneOutlinerLockColumn.h"

#include "ExtendSceneOutlinerStyle.h"
#include "Styling/SlateStyle.h"
#include "ActorTreeItem.h"

#include "Subsystems/EditorActorSubsystem.h"

#include "Selection.h"
#include "LevelEditor.h"
#include "ISceneOutliner.h"

#include "ExtendSceneOutliner.h"

const TSharedRef<SWidget> FSceneOutlinerLockColumn::ConstructRowWidget(FSceneOutlinerTreeItemRef TreeItem,
        const STableRow<FSceneOutlinerTreeItemPtr>& Row)
{
        FActorTreeItem* ActorTreeItem = TreeItem->CastTo<FActorTreeItem>();
       
        if(!ActorTreeItem || !ActorTreeItem->IsValid()) return SNullWidget::NullWidget;
       
        // 此处的检测条件发生了变化,我们检测 Actor 是否携带对应的 Tags
        FExtendSceneOutliner& ExtendSceneOutlinerModule =
                FModuleManager::LoadModuleChecked<FExtendSceneOutliner>(TEXT("ExtendSceneOutliner"));
        const bool bIsActorLocked =
                ExtendSceneOutlinerModule.CheckIsActorSelectionLocked(ActorTreeItem->Actor.Get());
       
        const FCheckBoxStyle& ToggleButtonStyle =
                FExtendSceneOutlinerStyle::GetStyleSet()->GetWidgetStyle<FCheckBoxStyle>(
                        FName("SceneOutliner.LockToggle"));
       
        TSharedRef<SWidget> ConstructedRowWidget =
                SNew(SCheckBox)
                .Visibility(EVisibility::Visible)
                .Type(ESlateCheckBoxType::ToggleButton)
                .Style(&ToggleButtonStyle)
                .HAlign(HAlign_Center)
                .IsChecked(bIsActorLocked ? ECheckBoxState::Checked : ECheckBoxState::Unchecked)
                .OnCheckStateChanged(
                        this,
                        &FSceneOutlinerLockColumn::OnLockToggleStateChanged_LockSelection_SelectedActor,
                        ActorTreeItem->Actor);
       
        return ConstructedRowWidget;
}

void FSceneOutlinerLockColumn::OnLockToggleStateChanged_LockSelection_SelectedActor(ECheckBoxState NewState,
        TWeakObjectPtr<AActor> Actor)
{
        FExtendSceneOutliner& ExtendSceneOutlinerModule =
                FModuleManager::LoadModuleChecked<FExtendSceneOutliner>(TEXT("ExtendSceneOutliner"));

        // 当开关状态发生改变时,执行对应操作
        if(NewState == ECheckBoxState::Checked)
        {
                ExtendSceneOutlinerModule.LockActorSelection(Actor.Get());
                ExtendSceneOutlinerModule.SetActorSelectionState(Actor.Get(), false);

                return;
        }

        if(NewState == ECheckBoxState::Unchecked)
        {
                ExtendSceneOutlinerModule.UnlockActorSelection(Actor.Get());

                return;
        }

        return;
}

编译并重启编辑器,测试按钮效果。



{♡☘♡☘♡\quad今天的捉弄结束了 \quad ♡☘♡☘♡}\\

批量锁定选择

与批量锁定移动类似。
// SceneOutlinerLockColumn.h

#pragma once

#include "ISceneOutlinerColumn.h"

class FSceneOutlinerLockColumn : public ISceneOutlinerColumn
{
...
private:

        void OnLockToggleStateChanged(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);

        void OnLockToggleStateChanged_LockMovement_SelectedActor(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);

        void OnLockToggleStateChanged_LockMovement_SelectedActors(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);

        void OnLockToggleStateChanged_LockSelection_SelectedActor(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);

        // 替代上面事件的新函数
        void OnLockToggleStateChanged_LockSelection_SelectedActors(ECheckBoxState NewState, TWeakObjectPtr<AActor> Actor);
};

// SceneOutlinerLockColumn.cpp

void FSceneOutlinerLockColumn::OnLockToggleStateChanged_LockSelection_SelectedActors(ECheckBoxState NewState,
        TWeakObjectPtr<AActor> Actor)
{
        FLevelEditorModule& LevelEditorModule =
                FModuleManager::LoadModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));
        TSharedPtr<ISceneOutliner> SceneOutliner =
                LevelEditorModule.GetFirstLevelEditor()->GetSceneOutliner();
       
        FExtendSceneOutliner& ExtendSceneOutlinerModule =
                FModuleManager::LoadModuleChecked<FExtendSceneOutliner>(TEXT("ExtendSceneOutliner"));
        USelection* SelectedActors = GEditor->GetSelectedActors();

        if(NewState == ECheckBoxState::Checked)
        {
                if(SelectedActors->Num() == 0)
                {
                        ExtendSceneOutlinerModule.LockActorSelection(Actor.Get());
                        ExtendSceneOutlinerModule.SetActorSelectionState(Actor.Get(), false);
                }
                else
                {
                        for(FSelectionIterator It(*SelectedActors); It; ++It)
                        {
                                AActor* SelectedActor = Cast<AActor>( *It );
                                ExtendSceneOutlinerModule.LockActorSelection(SelectedActor);
                        }
                       
                        SelectedActors->DeselectAll();
                }
               
                if(SceneOutliner.IsValid())
                {
                        SceneOutliner->FullRefresh();
                }

                return;
        }

        if(NewState == ECheckBoxState::Unchecked)
        {
                if(SelectedActors->Num() == 0)
                {
                        ExtendSceneOutlinerModule.UnlockActorSelection(Actor.Get());
                }
                else
                {
                        for(FSelectionIterator It(*SelectedActors); It; ++It)
                        {
                                AActor* SelectedActor = Cast<AActor>( *It );
                                ExtendSceneOutlinerModule.UnlockActorSelection(SelectedActor);
                        }
                }
               
                if(SceneOutliner.IsValid())
                {
                        SceneOutliner->FullRefresh();
                }
               
                return;
        }
       
        return;
}

编译并重启编辑器,测试按钮效果。



页: [1]
查看完整版本: 《调教UE5:编辑器拓展指南》自定义世界大纲