《调教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 &#34;Modules/ModuleInterface.h&#34;
class FExtendSceneOutliner : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
virtual ~FExtendSceneOutliner() {}
};
来到 ExtendSceneOutliner.cpp 中实现模块。
// ExtendSceneOutliner.cpp
#pragma once
#include &#34;ExtendSceneOutliner.h&#34;
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[]
{
&#34;Core&#34;,
&#34;CoreUObject&#34;,
&#34;Engine&#34;,
&#34;InputCore&#34;,
&#34;SceneOutliner&#34;,
&#34;Slate&#34;,
&#34;SlateCore&#34;,
&#34;UnrealEd&#34;
});
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[]
{
&#34;ExtendEditor&#34;,
&#34;ExtendSceneOutliner&#34;
} );
}
}
来到 .uproject 文件中,配置模块的启动方式。
// ExtendEditor.uproject
{
&#34;FileVersion&#34;: 3,
&#34;EngineAssociation&#34;: &#34;5.0&#34;,
&#34;Category&#34;: &#34;&#34;,
&#34;Description&#34;: &#34;&#34;,
&#34;Modules&#34;: [
{
&#34;Name&#34;: &#34;ExtendEditor&#34;,
&#34;Type&#34;: &#34;Runtime&#34;,
&#34;LoadingPhase&#34;: &#34;Default&#34;
},
{
&#34;Name&#34;: &#34;ExtendSceneOutliner&#34;,
&#34;Type&#34;: &#34;Editor&#34;,
&#34;LoadingPhase&#34;: &#34;Default&#34;
}
],
&#34;Plugins&#34;: [
{
&#34;Name&#34;: &#34;ModelingToolsEditorMode&#34;,
&#34;Enabled&#34;: true,
&#34;TargetAllowList&#34;: [
&#34;Editor&#34;
]
}
]
}
重启代码编辑器并编译,完成“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(&#34;ExtendSceneOutlinerStyle&#34;);
};
来到 ExtendSceneOutlinerStyle.cpp 中实现 FExtendSceneOutlinerStyle 类。我们将图标资源放在项目文件夹下的“Resource”文件夹下。该文件夹需要自己创建。
// ExtendSceneOutlinerStyle.cpp
#pragma once
#include &#34;ExtendSceneOutlinerStyle.h&#34;
#include &#34;Styling/SlateStyle.h&#34;
#include &#34;Styling/SlateStyleRegistry.h&#34;
#include &#34;Styling/StyleColors.h&#34;
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(&#34;/Resource/&#34;);
SlateStyleSet->SetContentRoot(RootPath);
{
const FVector2D IconeSize(16.f, 16.f);
FSlateImageBrush* SlateImageBrush = new FSlateImageBrush(RootPath + TEXT(&#34;Lock.png&#34;), IconeSize);
SlateStyleSet->Set(&#34;SceneOutliner.Lock&#34;, SlateImageBrush);
}
{
const FVector2D IconeSize(16.f, 16.f);
FSlateImageBrush* SlateImageBrush = new FSlateImageBrush(RootPath + TEXT(&#34;Unlock.png&#34;), IconeSize);
SlateStyleSet->Set(&#34;SceneOutliner.Unlock&#34;, SlateImageBrush);
}
{
const FVector2D IconeSize(16.f, 16.f);
const FCheckBoxStyle SelectionLockToggleButtonStyle =
FCheckBoxStyle()
.SetCheckBoxType(ESlateCheckBoxType::ToggleButton)
.SetPadding(FMargin(10.f))
.SetUncheckedImage(FSlateImageBrush(RootPath + TEXT(&#34;/Unlock.png&#34;), IconeSize, FStyleColors::White25))
.SetUncheckedHoveredImage(FSlateImageBrush(RootPath + TEXT(&#34;/Unlock.png&#34;), IconeSize, FStyleColors::AccentBlue))
.SetUncheckedPressedImage(FSlateImageBrush(RootPath + TEXT(&#34;/Unlock.png&#34;), IconeSize, FStyleColors::Foreground))
.SetCheckedImage(FSlateImageBrush(RootPath + TEXT(&#34;/Lock.png&#34;), IconeSize, FStyleColors::Foreground))
.SetCheckedHoveredImage(FSlateImageBrush(RootPath + TEXT(&#34;/Lock.png&#34;), IconeSize, FStyleColors::AccentBlack))
.SetCheckedPressedImage(FSlateImageBrush(RootPath + TEXT(&#34;/Lock.png&#34;), IconeSize, FStyleColors::AccentGray));
SlateStyleSet->Set(&#34;SceneOutliner.LockToggle&#34;, SelectionLockToggleButtonStyle);
}
return SlateStyleSet;
}
来到 ExtendSceneOutliner.cpp 的模块启动函数,将样式集初始化函数添加到其中。
// ExtendSceneOutliner.cpp
#include &#34;ExtendSceneOutlinerStyle.h&#34;
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 &#34;ISceneOutlinerColumn.h&#34;
class FSceneOutlinerLockColumn : public ISceneOutlinerColumn
{
public:
FSceneOutlinerLockColumn(ISceneOutliner& SceneOutliner) {}
static FName GetID() {return FName(&#34;SceneOutlinerExtendColumn&#34;);}
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 &#34;SceneOutlinerLockColumn.h&#34;
#include &#34;ExtendSceneOutlinerStyle.h&#34;
#include &#34;Styling/SlateStyle.h&#34;
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(&#34;Lock the transformation of actor&#34;)))
[
SNew(SImage)
.ColorAndOpacity(FSlateColor::UseForeground())
.Image(FExtendSceneOutlinerStyle::GetStyleSet().Get()->GetBrush(&#34;SceneOutliner.Lock&#34;))
];
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 &#34;Modules/ModuleInterface.h&#34;
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 &#34;Modules/ModuleManager.h&#34;
#include &#34;SceneOutlinerModule.h&#34;
#include &#34;SceneOutlinerLockColumn.h&#34;
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(&#34;SceneOutliner&#34;));
SceneOutlinerModule.RegisterDefaultColumnType<FSceneOutlinerLockColumn>(SceneOutlinerLockColumnInfo);
}
TSharedRef<ISceneOutlinerColumn> FExtendSceneOutliner::OnCreateSceneOutlinerLockColumnInfo(
ISceneOutliner& SceneOutliner)
{
return MakeShareable(new FSceneOutlinerLockColumn(SceneOutliner));
}
void FExtendSceneOutliner::UninitSceneOutlinerColumn()
{
FSceneOutlinerModule& SceneOutlinerModule =
FModuleManager::LoadModuleChecked<FSceneOutlinerModule>(TEXT(&#34;SceneOutliner&#34;));
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(&#34;SceneOutliner.LockToggle&#34;));
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(&#34;Locked &#34;) + ActorName);
return;
}
if(NewState == ECheckBoxState::Unchecked)
{
FString ActorName = Actor.Get()->GetActorLabel();
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Cyan, TEXT(&#34;Unlock &#34;) + ActorName);
return;
}
return;
}
// SceneOutlinerLockColumn.h
#pragma once
#include &#34;ISceneOutlinerColumn.h&#34;
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 &#34;ISceneOutlinerColumn.h&#34;
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 &#34;SceneOutlinerLockColumn.h&#34;
#include &#34;ExtendSceneOutlinerStyle.h&#34;
#include &#34;Styling/SlateStyle.h&#34;
#include &#34;ActorTreeItem.h&#34;
#include &#34;Subsystems/EditorActorSubsystem.h&#34;
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(&#34;SceneOutliner.LockToggle&#34;));
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 &#34;ISceneOutlinerColumn.h&#34;
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 &#34;SceneOutlinerLockColumn.h&#34;
#include &#34;ExtendSceneOutlinerStyle.h&#34;
#include &#34;Styling/SlateStyle.h&#34;
#include &#34;ActorTreeItem.h&#34;
#include &#34;Subsystems/EditorActorSubsystem.h&#34;
#include &#34;Selection.h&#34;
#include &#34;LevelEditor.h&#34;
#include &#34;ISceneOutliner.h&#34;
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(&#34;SceneOutliner.LockToggle&#34;));
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(&#34;LevelEditor&#34;));
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 &#34;Selection.h&#34;
#include &#34;Subsystems/EditorActorSubsystem.h&#34;
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(&#34;Locked&#34;));
}
void FExtendSceneOutliner::LockActorSelection(AActor* ActorToProcess)
{
if(!ActorToProcess) return;
if(!ActorToProcess->ActorHasTag(FName(&#34;Locked&#34;)))
{
ActorToProcess->Tags.Add(FName(&#34;Locked&#34;));
}
}
void FExtendSceneOutliner::UnlockActorSelection(AActor* ActorToProcess)
{
if(!ActorToProcess) return;
if(ActorToProcess->ActorHasTag(FName(&#34;Locked&#34;)))
{
ActorToProcess->Tags.Remove(FName(&#34;Locked&#34;));
}
}
void FExtendSceneOutliner::SetActorSelectionState(AActor* Actor, bool bShouldBeSelected)
{
WeakEditorActorSubsystem->SetActorSelectionState(Actor, bShouldBeSelected);
}
单件锁定选择
首先依旧来考虑相对简单的情况。在 SceneOutlinerLockColumn.h 中添加函数。
// SceneOutlinerLockColumn.h
#pragma once
#include &#34;ISceneOutlinerColumn.h&#34;
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 &#34;SceneOutlinerLockColumn.h&#34;
#include &#34;ExtendSceneOutlinerStyle.h&#34;
#include &#34;Styling/SlateStyle.h&#34;
#include &#34;ActorTreeItem.h&#34;
#include &#34;Subsystems/EditorActorSubsystem.h&#34;
#include &#34;Selection.h&#34;
#include &#34;LevelEditor.h&#34;
#include &#34;ISceneOutliner.h&#34;
#include &#34;ExtendSceneOutliner.h&#34;
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(&#34;ExtendSceneOutliner&#34;));
const bool bIsActorLocked =
ExtendSceneOutlinerModule.CheckIsActorSelectionLocked(ActorTreeItem->Actor.Get());
const FCheckBoxStyle& ToggleButtonStyle =
FExtendSceneOutlinerStyle::GetStyleSet()->GetWidgetStyle<FCheckBoxStyle>(
FName(&#34;SceneOutliner.LockToggle&#34;));
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(&#34;ExtendSceneOutliner&#34;));
// 当开关状态发生改变时,执行对应操作
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 &#34;ISceneOutlinerColumn.h&#34;
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(&#34;LevelEditor&#34;));
TSharedPtr<ISceneOutliner> SceneOutliner =
LevelEditorModule.GetFirstLevelEditor()->GetSceneOutliner();
FExtendSceneOutliner& ExtendSceneOutlinerModule =
FModuleManager::LoadModuleChecked<FExtendSceneOutliner>(TEXT(&#34;ExtendSceneOutliner&#34;));
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]