前面把性能显示面板做出来了。
要开始设置界面了。
W_SettingsPanel
/Game/UI/Settings/W_SettingsPanel.W_SettingsPanel
这是设置界面一个分页的Panel。这个蓝图的父类是插件里面的 GameSettingPanel。我们先暂时当黑盒。
这个界面有一个进入UI的动画,我们回头做。
左侧的设置项目,是一个 GameSettingListView,他是 UListView的子类。
右侧是一个设置的需求描述,很普通的一个描述面板
似乎所有东西Settings插件都准备好了,只需要拼出UI来。
W_LyraSettingScreen
这是设置界面的总界面,上面若干个TAB,下面是设置面板具体。
/Game/UI/Settings/W_LyraSettingScreen.W_LyraSettingScreen
把C++父类弄出来 class ULyraSettingScreen : public UGameSettingScreen
这个类只有三个侧键,确认返回,取消
ULyraGameSettingRegistry
设置界面有一个 树形状的数据结构(ULyraGameSettingRegistry)
里面有很多个根节点 UGameSettingCollection 音频设置,视频设置,游戏设置,鼠标键盘设置。。。
下面每个节点是一个UGameSettings。这是一个无限嵌套的map。形成了整个设置的数据结构
回过来看这个文件。
头文件这里的IWYU。
// 针对 IWYU(Include What You Use)工具 的专用注释指令(编译期无实际功能,仅给工具看)
// IWYU 是一款自动化工具,用于检测 C/C++ 代码中 “冗余的 #include” 或 “缺失的 #include”,目的是精简头文件依赖、提升编译效率。
// pragma: keep 强制告诉 IWYU:不要删除这行 #include 指令,即使工具检测到当前文件看似没有直接使用该头文件的内容,也必须保留。
#include "DataSource/GameSettingDataSourceDynamic.h" // IWYU pragma: keep
#include "GameSettingRegistry.h"
#include "Settings/EqZeroSettingsLocal.h" // IWYU pragma: keep
接下来是这两个宏定义
#define GET_SHARED_SETTINGS_FUNCTION_PATH(FunctionOrPropertyName) \
MakeShared<FGameSettingDataSourceDynamic>(TArray<FString>({ \
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroLocalPlayer, GetSharedSettings), \
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroSettingsShared, FunctionOrPropertyName) \
}))
#define GET_LOCAL_SETTINGS_FUNCTION_PATH(FunctionOrPropertyName) \
MakeShared<FGameSettingDataSourceDynamic>(TArray<FString>({ \
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroLocalPlayer, GetLocalSettings), \
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroSettingsLocal, FunctionOrPropertyName) \
}))
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroLocalPlayer, GetLocalSettings) 会检查UEqZeroLocalPlayer上有没有GetLocalSettings
#define GET_FUNCTION_NAME_STRING_CHECKED(ClassName, FunctionName) \
((void)sizeof(&ClassName::FunctionName), TEXT(#FunctionName))
如果展开后,逗号表达式返回值是第二个字符串,如果函数不在编译救错误了。
((void)sizeof(&ULyraLocalPlayer::GetSharedSettings), TEXT("GetSharedSettings"))
成员函数指针
#include <bits/stdc++.h>
class A
{
public:
void Func() { printf("Hello World!"); }
};
int main()
{
printf("%d\n", sizeof(&A::Func)); // 16
A obj;
void (A::*fp)() = &A::Func;
(obj.*fp)();
return 0;
}
回来
#define GET_LOCAL_SETTINGS_FUNCTION_PATH(FunctionOrPropertyName) \
MakeShared<FGameSettingDataSourceDynamic>(TArray<FString>({ \
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroLocalPlayer, GetLocalSettings), \
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroSettingsLocal, FunctionOrPropertyName) \
}))
我还想知道这个MakeShared的参数是什么来着,这是 FGameSettingDataSourceDynamic 的构造函数
他相当于 MakeShared
UCLASS()
class UEqZeroGameSettingRegistry : public UGameSettingRegistry
{
GENERATED_BODY()
protected:
UPROPERTY()
TObjectPtr<UGameSettingCollection> VideoSettings;
UPROPERTY()
TObjectPtr<UGameSettingCollection> AudioSettings;
UPROPERTY()
TObjectPtr<UGameSettingCollection> GameplaySettings;
UPROPERTY()
TObjectPtr<UGameSettingCollection> MouseAndKeyboardSettings;
UPROPERTY()
TObjectPtr<UGameSettingCollection> GamepadSettings;
};
这个类目前也就这样了,初始化逻辑没写。还有很多
我们回到 ULyraSettingScreen 这个类也很空,逻辑主要是三个ActionBar
拼UI
这是设置主界面底层的C++类
UCLASS(Abstract, meta = (Category = "Settings", DisableNativeTick))
class UEqZeroSettingScreen : public UGameSettingScreen
{
GENERATED_BODY()
protected:
UPROPERTY(BlueprintReadOnly, Category = Input, meta = (BindWidget, OptionalWidget = true, AllowPrivateAccess = true))
TObjectPtr<UEqZeroTabListWidgetBase> TopSettingsTabs;
UPROPERTY(EditDefaultsOnly)
FDataTableRowHandle BackInputActionData;
UPROPERTY(EditDefaultsOnly)
FDataTableRowHandle ApplyInputActionData;
UPROPERTY(EditDefaultsOnly)
FDataTableRowHandle CancelChangesInputActionData;
FUIActionBindingHandle BackHandle;
FUIActionBindingHandle ApplyHandle;
FUIActionBindingHandle CancelChangesHandle;
};
这个类也没啥太多逻辑,顶部的Tab栏,还有侧边按键
BackInputActionData,ApplyInputActionData,CancelChangesInputActionData 是侧键的按钮配置,是CommonUI定义的
BackHandle,ApplyHandle,CancelChangesHandle 是 RegisterUIActionBinding 接口注册TAG=>按键回调后的一个句柄
这几个Data要到蓝图里面的配置一下
/Game/UI/DT_UniversalActions.DT_UniversalActions
这是一个DataTable,类型是 CommonInputActionDataBase。需要的配置是里面的某一行。
然后我们把这个UI拼出来。
===
先拼接上面的设置切换TAB
/Game/UI/Foundation/TabbedView/W_HorizontalTabList.W_HorizontalTabList
这个UI长这样,中间是TAB,前后各一个 Action Widget,怎么用还不知道。
【Common Action Widget】【HorizontalBox】【Common Action Widget】
在设置界面配置这个TAB的数据。
- 配置这个 Pre/Next Tab Input Action Data
在CommonUI里面的 UCommonTabListWidgetBase
/** The input action to listen for causing the next tab to be selected */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = TabList, meta = (RowType = "/Script/CommonUI.CommonInputActionDataBase"))
FDataTableRowHandle NextTabInputActionData;
/** The input action to listen for causing the previous tab to be selected */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = TabList, meta = (RowType = "/Script/CommonUI.CommonInputActionDataBase"))
FDataTableRowHandle PreviousTabInputActionData;
配置的是 /CommonUI/GenericInputActionDataTable.GenericInputActionDataTable
GenericLeftShoulder 和 GenericRightShoulder
勾选 auto listen for input
======= 完成【W_LyraButtonTab】
Debug的按钮类型选择 /Game/UI/Foundation/Buttons/W_LyraButtonTab.W_LyraButtonTab 如果不做的话,在编辑器看不到效果吧。
W_LyraButtonTab=> ULyraTabButtonBase (C++) => ULyraButtonBase(C++) => UCommonButtonBase (C++)
UCommonButtonBase 头顶的注释是,非激活情况下自动禁用。
ULyraButtonBase 看起来多了一个可重写按钮Text的功能
ULyraTabButtonBase 多了一个 UCommonLazyImage,多显示了一张图片。
【让AI总结一下这么多按钮干嘛的 TODO】
【关于按钮的Style】
创建一个蓝图,父类是 Common Button Style, 里面可以配置一些比如 Base, Hovered, Pressed 状态下的按钮状态。
======== 在 SettingScreen 设置主界面设置这个Tab,在设计模式显示三个Tab
《【TAB】【TAB】【TAB】》
SettingScreen的蓝图
先完成ESC出界面,点击设置出设置界面的流程。
/Game/UI/Hud/W_EqGameMenu.W_EqGameMenu 蓝图,按钮 On Clicked
Event: OnClicked(OptionsButtons)
→ DeactivateWidget(Self)
→ GetOwningPlayer(Self)
→ PushContentToLayerForPlayer(
Player,
Widget=W_EQSettingScre,
Layer=UI.Layer.Menu
)
ESC界面点击按钮,销毁自己,然后打开设置界面
Event: Construct
-> Sequence
-> RegisterTopLevelTab(self, "GameplayCollection")
修BUG:找不到配置
运行,炸了!!!
这里拿到了最后的nullptr
UGameSettingCollection* UGameSettingScreen::GetSettingCollection(FName SettingDevName, bool& HasAnySettings)
{
HasAnySettings = false;
if (UGameSettingCollection* Collection = GetRegistry()->FindSettingByDevNameChecked<UGameSettingCollection>(SettingDevName))
{
TArray<UGameSetting*> InOutSettings;
FGameSettingFilterState FilterState;
Collection->GetSettingsForFilter(FilterState, InOutSettings);
HasAnySettings = InOutSettings.Num() > 0;
return Collection;
}
return nullptr;
}
看一下Class Default的配置先
Input -> Display in Action Bar 打勾
三个
BackInputActionData、ApplyInputActionData、CancelChangeInputActionData 配置一下,这里提供了按键的配置。
勾选IsBackHandler,勾选IsBackActionDisplayedInActionBar。
但是各个炸的是SettingPanel,所以过来看一下,分析下代码。CreateRegistry 需要子类重写。
断一下调用
UGameSettingCollection* UGameSettingScreen::GetSettingCollection(FName SettingDevName, bool& HasAnySettings)
{
HasAnySettings = false;
if (UGameSettingCollection* Collection = GetRegistry()->FindSettingByDevNameChecked<UGameSettingCollection>(SettingDevName))
{
// ...
}
}
template <typename GameSettingRegistryT = UGameSettingRegistry>
GameSettingRegistryT* GetRegistry() const { return Cast<GameSettingRegistryT>(const_cast<UGameSettingScreen*>(this)->GetOrCreateRegistry()); }
看起来很正常的创建,并且设置给了panel呀。
UGameSettingRegistry* UGameSettingScreen::GetOrCreateRegistry()
{
if (Registry == nullptr)
{
UGameSettingRegistry* NewRegistry = this->CreateRegistry();
NewRegistry->OnSettingChangedEvent.AddUObject(this, &ThisClass::HandleSettingChanged);
Settings_Panel->SetRegistry(NewRegistry);
Registry = NewRegistry;
}
return Registry;
}
FindSettingByDevNameChecked 的时候找不到。
问题应该是
void UEqZeroGameSettingRegistry::OnInitialize(ULocalPlayer* InLocalPlayer)
{
UEqZeroLocalPlayer* EqZeroLocalPlayer = Cast<UEqZeroLocalPlayer>(InLocalPlayer);
VideoSettings = InitializeVideoSettings(EqZeroLocalPlayer);
InitializeVideoSettings_FrameRates(VideoSettings, EqZeroLocalPlayer);
RegisterSetting(VideoSettings);
AudioSettings = InitializeAudioSettings(EqZeroLocalPlayer);
RegisterSetting(AudioSettings);
GameplaySettings = InitializeGameplaySettings(EqZeroLocalPlayer);
RegisterSetting(GameplaySettings);
MouseAndKeyboardSettings = InitializeMouseAndKeyboardSettings(EqZeroLocalPlayer);
RegisterSetting(MouseAndKeyboardSettings);
GamepadSettings = InitializeGamepadSettings(EqZeroLocalPlayer);
RegisterSetting(GamepadSettings);
}
这里面的函数都是空的,我们先完成 GameplaySettings 吧
完成 GameplaySettings
UGameSettingCollection* UEqZeroGameSettingRegistry::InitializeGameplaySettings(UEqZeroLocalPlayer* InLocalPlayer)
{
UGameSettingCollection* Screen = NewObject<UGameSettingCollection>();
Screen->SetDevName(TEXT("GameplayCollection"));
Screen->SetDisplayName(LOCTEXT("GameplayCollection_Name", "Gameplay"));
Screen->Initialize(InLocalPlayer);
return Screen;
}
很多关联文件没有,只能这样。
啥也没有!!!
bool UEqZeroTabListWidgetBase::RegisterDynamicTab(const FEqZeroTabDescriptor& TabDescriptor)
{
// 隐藏的标签在运行时注册期间会被有意跳过
if (TabDescriptor.bHidden)
{
return true;
}
PendingTabLabelInfoMap.Add(TabDescriptor.TabId, TabDescriptor);
return RegisterTab(TabDescriptor.TabId, TabDescriptor.TabButtonType, TabDescriptor.CreatedTabContentWidget);
}
这里是hidden,
UGameSettingCollection* UGameSettingScreen::GetSettingCollection(FName SettingDevName, bool& HasAnySettings)
{
HasAnySettings = false;
if (UGameSettingCollection* Collection = GetRegistry()->FindSettingByDevNameChecked<UGameSettingCollection>(SettingDevName))
{
TArray<UGameSetting*> InOutSettings;
FGameSettingFilterState FilterState;
Collection->GetSettingsForFilter(FilterState, InOutSettings);
HasAnySettings = InOutSettings.Num() > 0;
return Collection;
}
return nullptr;
}
这里筛选的设置项目是0啊。
他会遍历 UGameSettingCollection 里面的 UGameSetting 数组,我们确实为了测试没加进来。
语言设置
UGameSettingCollection* UEqZeroGameSettingRegistry::InitializeGameplaySettings(UEqZeroLocalPlayer* InLocalPlayer)
{
UGameSettingCollection* Screen = NewObject<UGameSettingCollection>();
Screen->SetDevName(TEXT("GameplayCollection"));
Screen->SetDisplayName(LOCTEXT("GameplayCollection_Name", "Gameplay"));
Screen->Initialize(InLocalPlayer);
{
UGameSettingCollection* LanguageSubsection = NewObject<UGameSettingCollection>();
LanguageSubsection->SetDevName(TEXT("LanguageCollection"));
LanguageSubsection->SetDisplayName(LOCTEXT("LanguageCollection_Name", "Language"));
Screen->AddSetting(LanguageSubsection);
//----------------------------------------------------------------------------------
{
UEqZeroSettingValueDiscrete_Language* Setting = NewObject<UEqZeroSettingValueDiscrete_Language>();
Setting->SetDevName(TEXT("Language"));
Setting->SetDisplayName(LOCTEXT("LanguageSetting_Name", "Language"));
Setting->SetDescriptionRichText(LOCTEXT("LanguageSetting_Description", "The language of the game."));
#if WITH_EDITOR
if (GIsEditor)
{
Setting->SetDescriptionRichText(LOCTEXT("LanguageSetting_WithEditor_Description", "The language of the game.\n\n<text color=\"#ffff00\">WARNING: Language changes will not affect PIE, you'll need to run with -game to test this, or change your PIE language options in the editor preferences.</>"));
}
#endif
Setting->AddEditCondition(FWhenPlayingAsPrimaryPlayer::Get());
LanguageSubsection->AddSetting(Setting);
}
//----------------------------------------------------------------------------------
}
return Screen;
}
就这样,设置项目就显示到界面上去了。
分析Settings的流程初步
分析一下
继承关系是这样的,设置界面最上一层是几个 UGameSettingCollection 代表音频,视频,游戏设置的几个分页。
UObject
└── UGameSetting // 所有设置项的抽象基类
├── UGameSettingCollection // 设置项容器(设置分组/分区)
│ └── UGameSettingCollectionPage // 可导航的子页面(选中后跳入独立页面)
├── UGameSettingAction // 动作按钮(如"自动设置画质")— 没有值
└── UGameSettingValue // 有值的设置项抽象基类
├── UGameSettingValueScalar // 连续值(滑块)抽象
│ └── UGameSettingValueScalarDynamic // 【具体类】通过属性路径动态读写
└── UGameSettingValueDiscrete // 离散选项(旋钮)抽象
└── UGameSettingValueDiscreteDynamic // 【具体类】通过属性路径动态读写
├── UGameSettingValueDiscreteDynamic_Bool // 布尔开关 (ON/OFF)
├── UGameSettingValueDiscreteDynamic_Number // 数值列表 (0,1,2,3...)
├── UGameSettingValueDiscreteDynamic_Enum // 枚举选择
├── UGameSettingValueDiscreteDynamic_Color // 颜色
└── UGameSettingValueDiscreteDynamic_Vector2D // 二维向量
语言设置继承 UGameSettingValueDiscrete
UGameSettingValueDiscrete
└── UEqZeroSettingValueDiscrete_Language // 语言选择器,自行管理选项列表
| 类 | 职责 |
|---|---|
| UGameSetting | 所有设置的根基类。拥有 DevName(程序名)、DisplayName(显示名)、Description(描述)、EditConditions(编辑条件)、Tags(标签)。管理生命周期:Initialize → Startup → OnInitialized。缓存可编辑状态 FGameSettingEditableState。 |
| UGameSettingCollection | 纯容器,不可选中。AddSetting() 添加子项并自动初始化。GetSettingsForFilter() 递归展平可见设置列表。 |
| UGameSettingCollectionPage | 可选中的集合,选中时触发 OnExecuteNavigationEvent,Panel 将跳转到新页面。 |
| UGameSettingAction | 动作按钮。触发 FGameplayTag 命名的事件,或执行自定义 TFunction<void(ULocalPlayer*)>。没有值需要保存/恢复。 |
| UGameSettingValue | 有值的抽象基类。新增 StoreInitial()(记录初始值)、ResetToDefault()(重置为默认)、RestoreToInitial()(恢复到打开设置时的值)。 |
| UGameSettingValueScalar | 连续值(滑块)。定义 GetValue/SetValue、GetSourceRange(值域)、GetSourceStep(步长)、归一化接口。 |
| UGameSettingValueScalarDynamic | 具体实现。通过 FGameSettingDataSource getter/setter 读写。支持 SetSourceRangeAndStep、SetDisplayFormat 等配置。 |
| UGameSettingValueDiscrete | 离散值(旋钮/下拉)。定义 GetDiscreteOptions()、GetDiscreteOptionIndex()、SetDiscreteOptionByIndex()。 |
| UGameSettingValueDiscreteDynamic | 具体实现。内部维护 OptionValues[](字符串值)和 OptionDisplayTexts[](显示文本)两个并行数组。通过 FGameSettingDataSource 读写当前字符串值。 |
这里我们给 Game 的 UGameSettingCollection 挂了一个 语言设置的 UGameSettingCollection。
语言设置下面是一个 UEqZeroSettingValueDiscrete_Language 的 Settings。
他的父类是 UGameSettingValueDiscrete,代表那个选语言【<简体中文>】的那个按钮和两边的箭头
读取数据的流程
Setting->GetValueAsString()
→ Getter->GetValueAsString(LocalPlayer)
→ FGameSettingDataSourceDynamic::GetValueAsString()
→ PropertyPathHelpers::GetPropertyValueAsString(LocalPlayer, CachedPath)
→ 引擎反射: LocalPlayer->GetLocalSettings()->GetShadowQuality()
→ 返回 "2" (字符串)
Setting->SetDiscreteOptionByIndex(3)
→ Setter->SetValue(LocalPlayer, "3")
→ FGameSettingDataSourceDynamic::SetValue()
→ PropertyPathHelpers::SetPropertyValueFromString(LocalPlayer, CachedPath, "3")
→ 引擎反射: LocalPlayer->GetLocalSettings()->SetShadowQuality(3)
如果要断点体会这个流程的话点这里。
FString FGameSettingDataSourceDynamic::GetValueAsString(ULocalPlayer* InLocalPlayer) const
{
FString OutStringValue;
const bool bSuccess = PropertyPathHelpers::GetPropertyValueAsString(InLocalPlayer, DynamicPath, OutStringValue);
ensure(bSuccess);
return OutStringValue;
}
void FGameSettingDataSourceDynamic::SetValue(ULocalPlayer* InLocalPlayer, const FString& InStringValue)
{
const bool bSuccess = PropertyPathHelpers::SetPropertyValueFromString(InLocalPlayer, DynamicPath, InStringValue);
ensure(bSuccess);
}
FGameSettingDataSourceDynamic 是如何被使用的,
#define GET_SHARED_SETTINGS_FUNCTION_PATH(FunctionOrPropertyName) \
MakeShared<FGameSettingDataSourceDynamic>(TArray<FString>({ \
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroLocalPlayer, GetSharedSettings), \
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroSettingsShared, FunctionOrPropertyName) \
}))
#define GET_LOCAL_SETTINGS_FUNCTION_PATH(FunctionOrPropertyName) \
MakeShared<FGameSettingDataSourceDynamic>(TArray<FString>({ \
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroLocalPlayer, GetLocalSettings), \
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroSettingsLocal, FunctionOrPropertyName) \
}))
这里,但是语言设置没用到。。。。
我们看看修改后点击确认的调用
void UGameSettingScreen::ApplyChanges()
{
if (ChangeTracker.HaveSettingsBeenChanged())
{
ChangeTracker.ApplyChanges();
ClearDirtyState();
Registry->SaveChanges();
}
}
这个 Tracker 会遍历所有的 DirtySettings,然后调用Apply 和 StoreInitial
ChangeTracker 是设置主界面的一个成员
UCLASS(MinimalAPI, Abstract, meta = (Category = "Settings", DisableNativeTick))
class UGameSettingScreen : public UCommonActivatableWidget
{
// 。。。
FGameSettingRegistryChangeTracker ChangeTracker;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
TObjectPtr<UGameSettingPanel> Settings_Panel;
UPROPERTY(Transient)
mutable TObjectPtr<UGameSettingRegistry> Registry;
}
他可以 watch一个 UGameSettingRegistry
ChangeTracker.WatchRegistry(Registry);
核心应该是UGameSettingRegistry的变更回调绑到了 Tracker上。
void FGameSettingRegistryChangeTracker::WatchRegistry(UGameSettingRegistry* InRegistry)
{
ClearDirtyState();
StopWatchingRegistry();
if (Registry.Get() != InRegistry)
{
Registry = InRegistry;
InRegistry->OnSettingChangedEvent.AddRaw(this, &FGameSettingRegistryChangeTracker::HandleSettingChanged);
}
}
每一个setting,都以change, apply的代理。
class UGameSetting : public UObject
{
DECLARE_EVENT_TwoParams(UGameSetting, FOnSettingChanged, UGameSetting* /*InSetting*/, EGameSettingChangeReason /*InChangeReason*/);
DECLARE_EVENT_OneParam(UGameSetting, FOnSettingApplied, UGameSetting* /*InSetting*/);
DECLARE_EVENT_OneParam(UGameSetting, FOnSettingEditConditionChanged, UGameSetting* /*InSetting*/);
FOnSettingChanged OnSettingChangedEvent;
FOnSettingApplied OnSettingAppliedEvent;
FOnSettingEditConditionChanged OnSettingEditConditionChangedEvent;
};
UGameSettingRegistry 注册的时候会把这个代理接过来。
void UGameSettingRegistry::RegisterInnerSettings(UGameSetting* InSetting)
{
InSetting->OnSettingChangedEvent.AddUObject(this, &ThisClass::HandleSettingChanged);
InSetting->OnSettingAppliedEvent.AddUObject(this, &ThisClass::HandleSettingApplied);
InSetting->OnSettingEditConditionChangedEvent.AddUObject(this, &ThisClass::HandleSettingEditConditionsChanged);
// ...
}
到这里我们明白了什么。GameSetting 的数据变更会一路广播到 主界面的SettingScreen上。那么Apply按钮是如何出现的呢?
UGameSetting -> UGameSettingRegistry ->
void UGameSettingScreen::HandleSettingChanged(UGameSetting* Setting, EGameSettingChangeReason Reason)
{
OnSettingsDirtyStateChanged(true);
}
这样的一路上来,这里面包括apply和setting
void UEqZeroSettingScreen::OnSettingsDirtyStateChanged_Implementation(bool bSettingsDirty)
{
if (bSettingsDirty)
{
if (!GetActionBindings().Contains(ApplyHandle))
{
AddActionBinding(ApplyHandle);
}
if (!GetActionBindings().Contains(CancelChangesHandle))
{
AddActionBinding(CancelChangesHandle);
}
}
else
{
RemoveActionBinding(ApplyHandle);
RemoveActionBinding(CancelChangesHandle);
}
}
整理一下,语言设置,读取配置,修改触发apply按钮,点击然后保存流程通了。大概。
保存到底层的流程没找到(local settings, shared setting)或者不是走的这里?
GameSettings保存的数据结构是如何维护的?这条继承路完全没有数据段。我们下一个先。
还有一个问题,他为什么会创建这个UI呢?
/Game/UI/Settings/Editors/W_SettingsListEntry_Discrete.W_SettingsListEntry_Discrete
引用查找 /Game/UI/Settings/GameSettingRegistryVisuals.GameSettingRegistryVisuals
恰好没崩。
这个东西配置在,设置主界面->【SettingPanel】->【ListView_Settings】这是一个 GameSettingListView
LyraButtonBase
【拼UI修 LyraButtonBase】....
====
主界面,设置顶上的TAB没有字?
bool ULyraTabListWidgetBase::RegisterDynamicTab(const FLyraTabDescriptor& TabDescriptor)
字是FLyraTabDescriptor 里面的 FText TabText; 决定的。
这个是 Setting Collection 的 GetDisplayName
全是对的???
应该是也跑到了。
void ULyraTabButtonBase::SetTabLabelInfo_Implementation(const FLyraTabDescriptor& TabLabelInfo)
{
SetButtonText(TabLabelInfo.TabText);
SetIconBrush(TabLabelInfo.IconBrush);
}
。。。
问题是 /Game/UI/Foundation/Buttons/W_EqButtonTab.W_EqButtonTab 这里的 UpdateButtonText 的蓝图事件到Text的修改断了。
切换语言的时候右边的面板更新是为什么
我啥也没干呀。
这个是SettingsPanel里面的
/Game/UI/Settings/W_GameSettingsDetailView.W_GameSettingsDetailView 这个控件。
从 SettingsPanel 开始就是Settings插件里面的类了。
UCLASS(MinimalAPI, Abstract)
class UGameSettingPanel : public UCommonUserWidget
{
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional, BlueprintProtected = true, AllowPrivateAccess = true))
TObjectPtr<UGameSettingDetailView> Details_Settings;
};
设置变化的时候从UGameSetting 取内容
void UGameSettingPanel::FillSettingDetails(UGameSetting* InSetting)
{
if (Details_Settings)
{
Details_Settings->FillSettingDetails(InSetting);
}
OnFocusedSettingChanged.Broadcast(InSetting);
}
里面无非是一些 GetDisplayName,GetDescriptionRichText 然后 SetText 上去。
===
里面的语言列表 Box_DetailsExtension
我们断点一下,从设置下到vertical box 的添加是怎么样的呢?UGameSettingDetailView::CreateDetailsExtension
先等等,我们多加几个设置项后再来对比一下。
其他:Gameplay的回放系统我们还没做,这里是设置也先跳过吧。
Vedio 设置
我们的 GameSettingRegistry 放出来
void UEqZeroGameSettingRegistry::OnInitialize(ULocalPlayer* InLocalPlayer)
{
UEqZeroLocalPlayer* EqZeroLocalPlayer = Cast<UEqZeroLocalPlayer>(InLocalPlayer);
VideoSettings = InitializeVideoSettings(EqZeroLocalPlayer);
if (VideoSettings)
{
// InitializeVideoSettings_FrameRates(VideoSettings, EqZeroLocalPlayer);
RegisterSetting(VideoSettings);
}
}
窗口模式设置
看看 ULyraGameSettingRegistry::InitializeVideoSettings
里面又包括了几个大类。
- Display
- 全屏、窗口
- 分辨率
- 性能检测面板开哪些,上一段的帧数显示哪些的。
- Graphics
- 色盲模式类型
- 色盲模式强度
- 亮度
- Graphics Quality
- 自动设置质量
- 质量预设,low, mid, high, epic, custom
- 下面是很多的细分预设,例如全局光,阴影,贴图,例子的级别等,都分low, mid, high, epic
- Advanced Graphics
- 帧数限制和垂直同步开关。
UGameSettingCollection* UEqZeroGameSettingRegistry::InitializeVideoSettings(UEqZeroLocalPlayer* InLocalPlayer)
{
UGameSettingCollection* Screen = NewObject<UGameSettingCollection>();
Screen->SetDevName(TEXT("VideoCollection"));
Screen->SetDisplayName(LOCTEXT("VideoCollection_Name", "Video"));
Screen->Initialize(InLocalPlayer);
UGameSettingValueDiscreteDynamic_Enum* WindowModeSetting = nullptr;
UGameSetting* MobileFPSType = nullptr;
// Display
////////////////////////////////////////////////////////////////////////////////////
{
UGameSettingCollection* Display = NewObject<UGameSettingCollection>();
Display->SetDevName(TEXT("DisplayCollection"));
Display->SetDisplayName(LOCTEXT("DisplayCollection_Name", "Display"));
Screen->AddSetting(Display);
//----------------------------------------------------------------------------------
{
// 窗口模式设置:窗口、窗口全屏、全屏
UGameSettingValueDiscreteDynamic_Enum* Setting = NewObject<UGameSettingValueDiscreteDynamic_Enum>();
Setting->SetDevName(TEXT("WindowMode"));
Setting->SetDisplayName(LOCTEXT("WindowMode_Name", "Window Mode"));
Setting->SetDescriptionRichText(LOCTEXT("WindowMode_Description", "In Windowed mode you can interact with other windows more easily, and drag the edges of the window to set the size. In Windowed Fullscreen mode you can easily switch between applications. In Fullscreen mode you cannot interact with other windows as easily, but the game will run slightly faster."));
Setting->SetDynamicGetter(GET_LOCAL_SETTINGS_FUNCTION_PATH(GetFullscreenMode));
Setting->SetDynamicSetter(GET_LOCAL_SETTINGS_FUNCTION_PATH(SetFullscreenMode));
Setting->AddEnumOption(EWindowMode::Fullscreen, LOCTEXT("WindowModeFullscreen", "Fullscreen"));
Setting->AddEnumOption(EWindowMode::WindowedFullscreen, LOCTEXT("WindowModeWindowedFullscreen", "Windowed Fullscreen"));
Setting->AddEnumOption(EWindowMode::Windowed, LOCTEXT("WindowModeWindowed", "Windowed"));
Setting->AddEditCondition(FWhenPlatformHasTrait::KillIfMissing(TAG_Platform_Trait_SupportsWindowedMode, TEXT("Platform does not support window mode")));
WindowModeSetting = Setting;
Display->AddSetting(Setting);
}
给 Vedio这一分页(UGameSettingCollection)加了 (display这一小分类 UGameSettingCollection)
添加了一个 UGameSettingValueDiscreteDynamic_Enum 加到 display 上面。
第一块设置,窗口模式设置:窗口、窗口全屏、全屏。
这里面有很多细节。
SetDevName、SetDisplayName、SetDescriptionRichText 只是设置一下信息,用于右侧面板的显示。
Setting->SetDynamicGetter(GET_LOCAL_SETTINGS_FUNCTION_PATH(GetFullscreenMode));
Setting->SetDynamicSetter(GET_LOCAL_SETTINGS_FUNCTION_PATH(SetFullscreenMode));
这是把参数赋值给这两个变量
UCLASS(MinimalAPI)
class UGameSettingValueDiscreteDynamic : public UGameSettingValueDiscrete
{
protected:
TSharedPtr<FGameSettingDataSource> Getter;
TSharedPtr<FGameSettingDataSource> Setter;
}
前面我们看过他最终是一个shared_ptr的 FGameSettingDataSourceDynamic,构造参数是("GetLocalSettings", "GetFullscreenMode")
#define GET_LOCAL_SETTINGS_FUNCTION_PATH(FunctionOrPropertyName) \
MakeShared<FGameSettingDataSourceDynamic>(TArray<FString>({ \
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroLocalPlayer, GetLocalSettings), \
GET_FUNCTION_NAME_STRING_CHECKED(UEqZeroSettingsLocal, FunctionOrPropertyName) \
}))
他最后会 PropertyPathHelpers,通过反射调用到对应接口。
FString FGameSettingDataSourceDynamic::GetValueAsString(ULocalPlayer* InLocalPlayer) const
{
FString OutStringValue;
const bool bSuccess = PropertyPathHelpers::GetPropertyValueAsString(InLocalPlayer, DynamicPath, OutStringValue);
ensure(bSuccess);
return OutStringValue;
}
void FGameSettingDataSourceDynamic::SetValue(ULocalPlayer* InLocalPlayer, const FString& InStringValue)
{
const bool bSuccess = PropertyPathHelpers::SetPropertyValueFromString(InLocalPlayer, DynamicPath, InStringValue);
ensure(bSuccess);
}
然后是
Setting->AddEnumOption(EWindowMode::Fullscreen, LOCTEXT("WindowModeFullscreen", "Fullscreen"));
Setting->AddEnumOption(EWindowMode::WindowedFullscreen, LOCTEXT("WindowModeWindowedFullscreen", "Windowed Fullscreen"));
Setting->AddEnumOption(EWindowMode::Windowed, LOCTEXT("WindowModeWindowed", "Windowed"));
UCLASS(MinimalAPI)
class UGameSettingValueDiscreteDynamic : public UGameSettingValueDiscrete
{
TArray<FString> OptionValues;
TArray<FText> OptionDisplayTexts;
};
value和text会被AddDynamicOption接口加到 TArray里面。
template<typename EnumType>
void AddEnumOption(EnumType InEnumValue, const FText& InOptionText)
{
const FString StringValue = StaticEnum<EnumType>()->GetNameStringByValue((int64)InEnumValue);
AddDynamicOption(StringValue, InOptionText);
}
例如
Setting->AddEnumOption(EWindowMode::Fullscreen, LOCTEXT("WindowModeFullscreen", "Fullscreen"));
文本本地化
用 namespace 和 key 作为键值,查表替换。最后没有就拿 Fullscreen 去给 Text。
#define LOCTEXT_NAMESPACE "EqZero"
Setting->AddEnumOption(EWindowMode::Fullscreen, LOCTEXT("WindowModeFullscreen", "Fullscreen"));
最后两个Array里面是【枚举1,枚举2,枚举3】【枚举1Text,枚举2Text,枚举3Text】
Setting->AddEditCondition(FWhenPlatformHasTrait::KillIfMissing(TAG_Platform_Trait_SupportsWindowedMode, TEXT("Platform does not support window mode")));
如果当前平台没有 Platform.Trait.SupportsWindowedMode 这个 Gameplay Tag,就把这个设置项直接 Kill(隐藏/移除),让玩家在设置界面中看不到它。
Platform.Trai.SupportsWindowedMode 这个加到 Project Settings -> Plugins -> Common UI Framework 这里已经有了。
======
炸了 !!!
void UGameSettingListEntrySetting_Navigation::NativeOnInitialized()
{
Super::NativeOnInitialized();
Button_Navigate->OnClicked().AddUObject(this, &ThisClass::HandleNavigationButtonClicked);
}
Button_Navigate 这里是nullptr
/Game/UI/Settings/Editors/W_SettingsListEntry_SubCollection.W_SettingsListEntry_SubCollection 这个蓝图有编译错误。
因为我们 LyraMenuButton 重做了,这里引用关系丢了,找不到这个控件。重新加上。
====
多设置分页切换BUG
两个分页的设置加到一页取了,头顶TAB也没有切换功能。
检查 UGameSettingPanel 的 ListView_Settings 怎么加进去的
默认流程
Registry::OnInitialize()
├── RegisterSetting(VideoSettings) // DevName: "VideoCollection"
└── RegisterSetting(GameplaySettings) // DevName: "GameplayCollection" 之类
│
▼
GetOrCreateRegistry()
└── Settings_Panel->SetRegistry(Registry)
└── RefreshSettingsList() // ← 用的是默认空 FilterState
└── Registry->GetSettingsForFilter(空Filter, ...)
└── 返回所有 Setting,不区分分页
emmmm,在 TopSettingTabs 添加一个 On Tab Selected 事件。这是 UCommonTabListWidgetBase 这里 Common UI 的内容。
然后去调用
void UGameSettingScreen::NavigateToSetting(FName SettingDevName)
{
NavigateToSettings({SettingDevName});
}
这个在一开UI就会调用一次
从这里注册的时候就会 RegisterTab
bool UEqZeroTabListWidgetBase::RegisterDynamicTab(const FEqZeroTabDescriptor& TabDescriptor)
{
// 隐藏的标签在运行时注册期间会被有意跳过
if (TabDescriptor.bHidden)
{
return true;
}
PendingTabLabelInfoMap.Add(TabDescriptor.TabId, TabDescriptor);
return RegisterTab(TabDescriptor.TabId, TabDescriptor.TabButtonType, TabDescriptor.CreatedTabContentWidget);
}
CommonButtonGroupBase:OnSelectionStateChangedBase
CommonTabListWidgetBase:HandleTabButtonSelected
W_EqSettingScreen:On Tab Selected
从底层上来的。而且只会出现一次。
TAB的先后顺序和 Register Dynamic Tab 的顺序有关。
===
设置界面的详情
选中一个设置项的时候,右边会有详细说明。文本内容的修改都比较简单。
这里有个 Box_DetailsExtension。会加出好多个项,并且还能当前选中的高亮。
详细看看这个数据怎么来的。
void UGameSettingPanel::FillSettingDetails(UGameSetting* InSetting)
{
if (Details_Settings)
{
Details_Settings->FillSettingDetails(InSetting);
}
OnFocusedSettingChanged.Broadcast(InSetting);
}
从入口开始
HandleSettingItemHoveredChanged / HandleSettingItemSelectionChanged
→ FillSettingDetails(Setting)
→ Details_Settings->FillSettingDetails(InSetting) // 转发给 DetailView
代码位于 // void UGameSettingDetailView::FillSettingDetails(UGameSetting* InSetting)
先释放资源
// 把现有子 Widget 归还到对象池(不销毁,留着复用)
for (UWidget* ChildExtension : Box_DetailsExtension->GetAllChildren())
ExtensionWidgetPool.Release(Cast<UUserWidget>(ChildExtension));
// 从 VerticalBox 容器中移除
Box_DetailsExtension->ClearChildren();
这里用了 FUserWidgetPool(UMG 自带的 Widget 对象池),避免每次都 NewObject + GC。
【FUserWidgetPool ExtensionWidgetPool】内存池可以看看。
通过 VisualData 查找该设置需要哪些扩展 Widget 类
TArray<TSoftClassPtr<UGameSettingDetailExtension>> ExtensionClassPtrs;
if (VisualData)
ExtensionClassPtrs = VisualData->GatherDetailExtensions(InSetting);
VisualData 是一个 UGameSettingVisualData DataAsset(在编辑器里配置),查找逻辑分两级:
- 按名称查 — 在 ExtensionsForName 这个 TMap<FName, FGameSettingNameExtensions> 中,用 InSetting->GetDevName()(如
"Brightness")查找。如果找到且bIncludeClassDefaultExtensions == false,直接返回,不再继续。 - 按类型继承链查 — 遍历 Setting 的 UClass 继承链(
GetClass() → GetSuperClass() → ...),在 ExtensionsForClasses 这个 TMap<TSubclassOf, FGameSettingClassExtensions> 中逐级查找。这意味着你可以为 UGameSettingValueScalarDynamic配一个滑块预览扩展,所有标量设置都会自动获得。
断点进 UGameSettingVisualData::GatherDetailExtensions 这里看看
这个VisualData是配置的DA。
/Game/UI/Settings/GameSettingRegistryVisuals.GameSettingRegistryVisuals 这个东西。
// Find extensions for it using the super chain of the setting so that we get any
// class based extensions for this setting.
for (UClass* Class = InSetting->GetClass(); Class; Class = Class->GetSuperClass())
{
if (TSubclassOf<UGameSetting> SettingClass = TSubclassOf<UGameSetting>(Class))
{
FGameSettingClassExtensions* ExtensionForClass = ExtensionsForClasses.Find(SettingClass);
if (ExtensionForClass)
{
Extensions.Append(ExtensionForClass->Extensions);
}
}
}
这里 Extensions 的Array里面只有这一个UI控件
/Game/UI/Settings/Extensions/Enum/W_EnumOptionExtension.W_EnumOptionExtension
他会拿到一个GameSettings调用蓝图函数 RebuildOptions
他的数据来源是 GetDiscreteOptions 函数
比如语言这样的重写
TArray<FText> UEqZeroSettingValueDiscrete_Language::GetDiscreteOptions() const
{
TArray<FText> Options;
for (const FString& CultureName : AvailableCultureNames)
{
if (CultureName == TEXT(""))
{
// 空字符串代表 "系统默认"
const FCulturePtr SystemDefaultCulture = FInternationalization::Get().GetDefaultCulture();
if (ensure(SystemDefaultCulture))
{
const FString& DefaultCultureDisplayName = SystemDefaultCulture->GetDisplayName();
FText LocalizedSystemDefault = FText::Format(LOCTEXT("SystemDefaultLanguage", "系统默认 ({0})"), FText::FromString(DefaultCultureDisplayName));
Options.Add(MoveTemp(LocalizedSystemDefault));
}
}
else
{
FCulturePtr Culture = FInternationalization::Get().GetCulture(CultureName);
if (ensureMsgf(Culture, TEXT("Unable to find Culture '%s'!"), *CultureName))
{
const FString CultureDisplayName = Culture->GetDisplayName();
const FString CultureNativeName = Culture->GetNativeName();
// 仅在本地名称和显示名称不同时同时展示二者,避免重复
FString Entry = (!CultureNativeName.Equals(CultureDisplayName, ESearchCase::CaseSensitive))
? FString::Printf(TEXT("%s (%s)"), *CultureNativeName, *CultureDisplayName)
: CultureNativeName;
Options.Add(FText::FromString(Entry));
}
}
}
return Options;
}
这个是比较通用的重写。
TArray<FText> UGameSettingValueDiscreteDynamic::GetDiscreteOptions() const
{
const TArray<FString>& DisabledOptions = GetEditState().GetDisabledOptions();
if (DisabledOptions.Num() > 0)
{
TArray<FText> AllowedOptions;
for (int32 OptionIndex = 0; OptionIndex < OptionValues.Num(); ++OptionIndex)
{
if (!DisabledOptions.Contains(OptionValues[OptionIndex]))
{
AllowedOptions.Add(OptionDisplayTexts[OptionIndex]);
}
}
return AllowedOptions;
}
return OptionDisplayTexts;
}
前面我们分析过添加枚举的时候那两个Array。
UCLASS(MinimalAPI)
class UGameSettingValueDiscreteDynamic : public UGameSettingValueDiscrete
{
TArray<FString> OptionValues;
TArray<FText> OptionDisplayTexts;
};
这个是通过,这个枚举写进去的
Setting->AddEnumOption(EWindowMode::Fullscreen, LOCTEXT("WindowModeFullscreen", "Fullscreen"));
Setting->AddEnumOption(EWindowMode::WindowedFullscreen, LOCTEXT("WindowModeWindowedFullscreen", "Windowed Fullscreen"));
Setting->AddEnumOption(EWindowMode::Windowed, LOCTEXT("WindowModeWindowed", "Windowed"));
OK