本文的目的在于学完后通过本文能及时回想起整个项目脉络,方便在面试前进行整体回顾,并不是教程文档,请先学习完腾讯课堂梁迪老师的课程
https://ke.qq.com/course/415155#term_id=100495281
菜单部分整体逻辑梳理
目前的目录结构
首先明确菜单部分当作一个Level,所以目前所有工作都针对与这个level设置的
Gameplay部分,这里定义了菜单的GameMode与controller, 具体内容:
controller部分设置了菜单关卡的鼠标属性
ASlAiMenuController::ASlAiMenuController() {
bShowMouseCursor = true;
}
void ASlAiMenuController::BeginPlay() {
FInputModeUIOnly inputMode;
inputMode.SetLockMouseToViewportBehavior(EMouseLockMode::LockAlways); //鼠标锁定对话框
SetInputMode(inputMode);
}
GameMode部分设置了菜单的显示HUD与controller
ASlAiMenuGameMode::ASlAiMenuGameMode() {
PlayerControllerClass = ASlAiMenuController::StaticClass(); //世界设置中的playerControllerClass选择为ASlAi
HUDClass = ASlAiMenuHUD::StaticClass(); //世界设置中的HUDClass选择为ASlAi
}
GameMode如何与菜单Level绑定? 进入UE里关卡的世界设置进行选定
现在重点就是HUD显示了,看看我们上面绑定的HUD类里面有什么
//SlAiMenuHUD
UCLASS()
class SLAICOURSE_API ASlAiMenuHUD : public AHUD
{
GENERATED_BODY()
public:
ASlAiMenuHUD();
TSharedPtr<class SSlAiMenuHUDWidget> MenuHUDWidget;
};
ASlAiMenuHUD::ASlAiMenuHUD() {
if (GEngine && GEngine->GameViewport) {
SAssignNew(MenuHUDWidget, SSlAiMenuHUDWidget);
GEngine->GameViewport->AddViewportWidgetContent(SNew(SWeakWidget).PossiblyNullContent(MenuHUDWidget.ToSharedRef())); //添加控件至视口
}
}
可以看到我们MenuHUD里的内容也非常简单,就是拥有一个HUDwidget的控件变量,然后
在构造函数那里new了一个这个菜单的Widget然后添加到游戏视口,所以我们菜单的具体内容便是在这
MenuHUDWidget里面了
//MenuHUDWidget
class SLAICOURSE_API SSlAiMenuHUDWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SSlAiMenuHUDWidget)
{}
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct(const FArguments& InArgs);
private:
//获取Menu样式
const struct FSlAiMenuStyle* MenuStyle;
TSharedPtr<class SSlAiMenuWidget> MenuWidget;
};
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiMenuHUDWidget::Construct(const FArguments& InArgs)
{
MenuStyle = &SlAiStyle::Get().GetWidgetStyle<FSlAiMenuStyle>("BPSlAiMenuStyle"); //把蓝图从Menuwidgetstyle继承下来的蓝图类赋值给我们的MenuStyle, 拿到蓝图类是通过单例拿到的
ChildSlot
[
SNew(SOverlay)
+SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SNew(SImage)
.Image(&MenuStyle->MenuHUDBackgroundBrush)
]
+SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SAssignNew(MenuWidget, SSlAiMenuWidget)
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
HUDwidget里便是我们真正的界面显示内容了,构造函数里的是控件的组成,一个overlay加一个MenuWidget,很合理, HUDwidget = overlay(背景色) + MenuWidget(菜单),另外提一点,slate写法太过于反人类,写界面这种事尽量不要用纯代码写, 一个是效率过低,二个是写代码对应的显示不直观,本次就纯当学习了,UE写界面还是推荐UMG,这些样式格式都不用手动用代码指定,逻辑用蓝图或者是UMG + UserWidget继承后用C++的写法都比纯slate要高效太多。
菜单界面所有的样式都是由样式蓝图指定
这个蓝图是我们在UE编辑器里创建的样式蓝图,怎么让蓝图拥有我们自定义的属性呢
我们新建一个FSlAiMenuStyle 继承 FSlateWidgetStyle类, 自动生成的代码在Style目录下面,现在我们的问题是如何让这个FSlAiMenuStyle 类 与编辑器上的样式蓝图类关联起来,比如我在FSlAiMenuStyle类中添加反射属性,怎么在蓝图中展现出来,其实就是在创建样式蓝图的时候选定它继承于我们自定义的FSlAiMenuStyle类,其实蓝图和C++代码交互,无非就是蓝图继承于C++类,包括UMG里的也是一样
我们定义一个SlAiStyle类,这个类是一个单例模式,因为我想在菜单这个关卡任意地方都读取到我蓝图中设置的样式,所以我们设置他为单例模式
class SLAICOURSE_API SlAiStyle
{
public:
static void Initialze(); //初始化
static FName GetStyleSetName(); //得到类型名称
static void ShutDown(); //关闭
static const ISlateStyle& Get();
private:
static TSharedRef<class FSlateStyleSet> Create();
static TSharedPtr<FSlateStyleSet> SlAiStyleInstance; // 实例
};
可以看到SlAiStyle的单例就是指的是FSlateStyleSet类,它拥有一个样式的集合,我们在构造它的时候指定它的Resources目录
TSharedRef<FSlateStyleSet> StyleRef = FSlateGameResources::New(SlAiStyle::GetStyleSetName(), "/Game/UI/Style", "/Game/UI/Style/");
这个/Game/UI/Style 目录就是我们样式蓝图存放的目录,可以这样理解,就是告诉FSlateStyleSet 你要在这个目录下去找我在编辑器定义的蓝图
MenuStyle = &SlAiStyle::Get().GetWidgetStyle<FSlAiMenuStyle>("BPSlAiMenuStyle");
拿到蓝图所在目录的SlateStyleSet, 通过styleSet的方法GetWidgetStyle,参数是我们编辑器蓝图的名字,所以MenuStyle就是我们拿到的样式蓝图了,因为样式蓝图是继承与我们定义的FSlAiMenuStyle的,我们在需要的控件.h文件里包含一个指针就可以了
const FSlAiMenuStyle* MenuStyle;
样式蓝图里的每一项style都需要在它的父类代码中进行设置,例如
UPROPERTY(EditAnywhere, Category=MenuHUD)
FSlateBrush MenuHUDBackgroundBrush;
UPROPERTY(EditAnywhere, Category = Menu)
FSlateBrush MenuBackgroundBrush;
UPROPERTY(EditAnywhere, Category = Menu)
FSlateBrush LeftIconBrush;
UPROPERTY(EditAnywhere, Category = Menu)
FSlateBrush RightIconBrush;
UPROPERTY(EditAnywhere, Category = Menu)
FSlateBrush TitleBorderBrush;
样式的内容讲完了,回到widget主线,我们的菜单widget内容有点多
void SSlAiMenuWidget::Construct(const FArguments& InArgs)
{
MenuStyle = &SlAiStyle::Get().GetWidgetStyle<FSlAiMenuStyle>("BPSlAiMenuStyle");
FSlateApplication::Get().PlaySound(MenuStyle->MenuBackgroundMusic);
ChildSlot
[
SAssignNew(RootSizeBox, SBox)
[
SNew(SOverlay)
+SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(FMargin(0.f, 50.f, 0.f, 0.f))
[
SNew(SImage)
.Image(&MenuStyle->MenuBackgroundBrush)
]
+ SOverlay::Slot()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
.Padding(FMargin(0.f, 25.f, 0.f, 0.f))
[
SNew(SImage)
.Image(&MenuStyle->LeftIconBrush)
]
+ SOverlay::Slot()
.HAlign(HAlign_Right)
.VAlign(VAlign_Center)
.Padding(FMargin(0.f, 25.f, 0.f, 0.f))
[
SNew(SImage)
.Image(&MenuStyle->RightIconBrush)
]
+SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Top)
[
SNew(SBox)
.WidthOverride(400.f)
.HeightOverride(100.f)
[
SNew(SBorder)
.BorderImage(&MenuStyle->TitleBorderBrush)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SAssignNew(TitleText, STextBlock)
.Font(MenuStyle->TitleFont)
.Text(FText::FromString(" zhou hang "))
]
]
]
+SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Top)
.Padding(FMargin(0.f, 130.f, 0.f, 0.f))
[
SAssignNew(ContentBox, SVerticalBox)
]
]
];
InitializedMenuList();
InitializedAnimation();
}
construct里设置子组件的内容和样式,首先是一层大壳子限定菜单大小,然后overlay下设置背景图片
以及左右边框以及菜单的标题框, 下面具体内容我们用一个垂直列表来包含
可以对照这图来看,垂直列表其实就是包含了 startGame, GamOption, 以及quitGame三个子Item
我们需要把每个Item都实现出来然后填入。
Item类,我们可以把Item控件抽离出来,因为我们可以观察到每个选项的样式是一样的,除了文本和点击事件不同之外,所以我们需要以参数的形式传入变量,以委托的形式传入方法,其实就是回调
DECLARE_DELEGATE_OneParam(FItemClicked, const EMenuItem::Type) //可以看成这句话表示 FItemClicked 是带有一个传入参数无传出参数的函数类型
/**
*
*/
class SLAICOURSE_API SSlAiMenuItemWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SSlAiMenuItemWidget)
{}
SLATE_ATTRIBUTE(FText, ItemText) //方法参数类型, 调用的名称
SLATE_EVENT(FItemClicked, OnClicked) //由委托宏解释FTtemClicked的类型
SLATE_ATTRIBUTE(EMenuItem::Type, ItemType)
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct(const FArguments& InArgs);
//重写组件的OnMouseButtonDowm方法 鼠标按下
virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
//重写组件的OnMouseButtonUp方法 鼠标抬起
virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
//重写组件的OnMouseButtonUp方法 鼠标离开
virtual void OnMouseLeave(const FPointerEvent& MouseEvent) override;
private:
FSlateColor GetTintColor() const;
private:
FItemClicked OnClicked;
EMenuItem::Type ItemType;
const struct FSlAiMenuStyle* MenuStyle;
//按钮是否已经按下
bool IsMouseButtonDown;
};
参数传入方法,首先.h的construct里申明SLATE_ATTRIBUTE
SLATE_ATTRIBUTE(FText, ItemText) //方法参数类型, 调用的名称
最后New这个控件时可以通过申明的名称来传入参数,然后.cpp的文件里实现使用外界传入的参数就用 InArgs._ItemText
(SNew(SSlAiMenuItemWidget).ItemText(FText::FromString("NewGame"))
上面是控件构造参数的使用方法,函数委托写法其实也一样
定义委托函数的类型
DECLARE_DELEGATE_OneParam(FItemClicked, const EMenuItem::Type)
//可以看成这句话表示 FItemClicked 是带有一个传入参数无传出参数的函数类型
然后在.h里进行申明
SLATE_EVENT(FItemClicked, OnClicked)
//类型是我们自己定义的FItemClicked函数,外部调用这个函数的方法名称是OnClicked
外界需要调用Onclicked方法传入一个FItemClicked类型的函数指针,即一个传入参数EMenuItem::Type,无传出参数的函数类型
SNew(SSlAiMenuItemWidget).OnClicked(this, &SSlAiMenuWidget::MenuItemOnClicked)
MenuItemOnClicked就是一个FItemClicked类型的函数,它的申明如下
void SSlAiMenuWidget::MenuItemOnClicked(EMenuItem::Type ItemType);
然后我们在控件的实现中,拿到对应的函数去执行也是通过参数拿到然后执行
OnClicked = InArgs._OnClicked; //拿到外界传入的函数
OnClicked.ExecuteIfBound(ItemType); //执行
好了,委托的使用就是上面所写,我们可以把函数,以及参数变量都可以通过控件的构造函数传入,这样就能实现在同一个Item控件类,传入不同的参数以及函数指针,从而委托执行不同的方法,在我们的具体例子中是根据传入不同的item类型,从而执行不同的控件切换
item控件其实就是一些样式加一个按钮,所以核心在于重写点击事件,我们的委托就在这里执行
FReply SSlAiMenuItemWidget::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) {
if (IsMouseButtonDown) {
IsMouseButtonDown = false;
OnClicked.ExecuteIfBound(ItemType); // 实现点击执行的逻辑, 传入的变量是绑定参数,即执行逻辑函数的传入参数
}
return FReply::Handled();
}
其中ITemtype就是不同的item类型,具体的在slaitype.h下定义了
namespace EMenuItem
{
enum Type
{
None,
StartGame,
GameOption,
QuitGame,
NewGame,
LoadRecord,
StartGameGoBack,
GameOptionGoBack,
NewGameGoBack,
ChooseRecordGoBack,
EnterGame,
EnterRecord
};
}
Item类已经写好了,我们知道在这里定义了item的样式,以及传入了一个itemtype和一个委托,它会在点击按钮的时候执行对应的委托,我们也能是猜到就是根据不同的item类型来跳转掉不同的控件,那么现在回到MenuWidget看看是如何将item组装起来的
上面已经写了MenuWidget的construct中调用InitializedMenuList()就是组装菜单
void SSlAiMenuWidget::InitializedMenuList() {
//实例化主界面
TArray<TSharedPtr<SCompoundWidget>> MainMenuList;
MainMenuList.Add(SNew(SSlAiMenuItemWidget).ItemText(FText::FromString("StartGame")).ItemType(EMenuItem::StartGame).OnClicked(this, &SSlAiMenuWidget::MenuItemOnClicked));
MainMenuList.Add(SNew(SSlAiMenuItemWidget).ItemText(FText::FromString("GameOption")).ItemType(EMenuItem::GameOption).OnClicked(this, &SSlAiMenuWidget::MenuItemOnClicked));
MainMenuList.Add(SNew(SSlAiMenuItemWidget).ItemText(FText::FromString("QuitGame")).ItemType(EMenuItem::QuitGame).OnClicked(this, &SSlAiMenuWidget::MenuItemOnClicked));
MenuMap.Add(EMenuType::MainMenu, MakeShareable(new MenuGroup(FText::FromString("Menu"), 510.f, &MainMenuList)));
//开始游戏界面
TArray<TSharedPtr<SCompoundWidget>> StartGameList;
StartGameList.Add(SNew(SSlAiMenuItemWidget).ItemText(FText::FromString("NewGame")).ItemType(EMenuItem::NewGame).OnClicked(this, &SSlAiMenuWidget::MenuItemOnClicked));
StartGameList.Add(SNew(SSlAiMenuItemWidget).ItemText(FText::FromString("LoadRecord")).ItemType(EMenuItem::LoadRecord).OnClicked(this, &SSlAiMenuWidget::MenuItemOnClicked));
StartGameList.Add(SNew(SSlAiMenuItemWidget).ItemText(FText::FromString("GoBack")).ItemType(EMenuItem::StartGameGoBack).OnClicked(this, &SSlAiMenuWidget::MenuItemOnClicked));
MenuMap.Add(EMenuType::StartGame, MakeShareable(new MenuGroup(FText::FromString("StartGame"), 510.f, &StartGameList)));
//游戏设置界面
TArray<TSharedPtr<SCompoundWidget>> GameOptionList;
//实例化游戏设置的Widget
SAssignNew(GameOptionWidget, SSlAiGameOptionWidget)
.ChangeVolume(this, &SSlAiMenuWidget::ChangeVolume);
//添加控件到数组
GameOptionList.Add(GameOptionWidget);
GameOptionList.Add(SNew(SSlAiMenuItemWidget).ItemText(FText::FromString("SlAiMenu")).ItemType(EMenuItem::GameOptionGoBack).OnClicked(this, &SSlAiMenuWidget::MenuItemOnClicked));
MenuMap.Add(EMenuType::GameOption, MakeShareable(new MenuGroup(FText::FromString("GameOption"), 610.f, &GameOptionList)));
//开始新游戏界面
TArray<TSharedPtr<SCompoundWidget>> NewGameList;
SAssignNew(NewGameWidget, SSlAiNewGameWidget);
NewGameList.Add(NewGameWidget);
NewGameList.Add(SNew(SSlAiMenuItemWidget).ItemText(FText::FromString("EnterGame")).ItemType(EMenuItem::EnterGame).OnClicked(this, &SSlAiMenuWidget::MenuItemOnClicked));
NewGameList.Add(SNew(SSlAiMenuItemWidget).ItemText(FText::FromString("GoBack")).ItemType(EMenuItem::NewGameGoBack).OnClicked(this, &SSlAiMenuWidget::MenuItemOnClicked));
MenuMap.Add(EMenuType::NewGame, MakeShareable(new MenuGroup(FText::FromString("NewGame"), 510.f, &NewGameList)));
//选择存档界面
TArray<TSharedPtr<SCompoundWidget>> ChooseRecordList;
SAssignNew(ChooseRecordWidget, SSlAiChooseRecordWidget);
ChooseRecordList.Add(ChooseRecordWidget);
ChooseRecordList.Add(SNew(SSlAiMenuItemWidget).ItemText(FText::FromString("EnterRecord")).ItemType(EMenuItem::EnterRecord).OnClicked(this, &SSlAiMenuWidget::MenuItemOnClicked));
ChooseRecordList.Add(SNew(SSlAiMenuItemWidget).ItemText(FText::FromString("GoBack")).ItemType(EMenuItem::ChooseRecordGoBack).OnClicked(this, &SSlAiMenuWidget::MenuItemOnClicked));
MenuMap.Add(EMenuType::ChooseRecord, MakeShareable(new MenuGroup(FText::FromString("LoadRecord"), 510.f, &ChooseRecordList)));
}
其中MenuType与MenuItem不一样,每三个item组成一个MenuType
//Menu界面类型
namespace EMenuType {
enum Type {
None,
MainMenu,
StartGame,
GameOption,
NewGame,
ChooseRecord
};
}
把所有的界面都组装好存入到Map中,然后初始化动画InitializedAnimation();
void SSlAiMenuWidget::InitializedAnimation() {
//开始延时
const float StartDelay = 0.3f;
//持续时间
const float AnimDuration = 0.6f;
MenuAnimation = FCurveSequence();
MenuCurve = MenuAnimation.AddCurve(StartDelay, AnimDuration, ECurveEaseFunction::QuadInOut);
//初始设置Menu大小
ResetWidgetSize(600.f, 510.f);
//初始显示主界面
ChooseWidget(EMenuType::MainMenu);
//允许点击按钮
ControlLocked = false;
//设置动画状态为停止
AnimState = EMenuAnim::Stop;
//设置动画播放器跳到结尾,也就是1
MenuAnimation.JumpToEnd();
}
其中ChooseWidget选择主界面展示,其实就是把Map中组装好的控件填入MenuWidget垂直框的插槽
void SSlAiMenuWidget::ChooseWidget(EMenuType::Type WidgetType)
{
//定义是否已经显示菜单
IsMenuShow = WidgetType != EMenuType::None;
//移出所有组件
ContentBox->ClearChildren();
//如果Menutype是None
if (WidgetType == EMenuType::None) return;
//循环添加组件
for (TArray<TSharedPtr<SCompoundWidget>>::TIterator It((*MenuMap.Find(WidgetType))->ChildWidget); It; ++It) {
ContentBox->AddSlot().AutoHeight()[(*It)->AsShared()];
}
//更改标题
TitleText->SetText((*MenuMap.Find(WidgetType))->MenuName);
}
整个主页面就构造完毕了。当然这只是构造结束了,页面呈现已经写完了,接下来看看传入item的委托具体方法是什么
void SSlAiMenuWidget::MenuItemOnClicked(EMenuItem::Type ItemType) {
//如果锁住了,直接return
if (ControlLocked) return;
//设置锁住了按钮
ControlLocked = true;
switch (ItemType)
{
case EMenuItem::StartGame:
PlayClose(EMenuType::StartGame);
break;
case EMenuItem::GameOption:
PlayClose(EMenuType::GameOption);
break;
case EMenuItem::QuitGame:
SlAiHelper::PlayerSoundAndCall(UGameplayStatics::GetPlayerController(GWorld, 0)->GetWorld(), MenuStyle->ExitGameSound, this, &SSlAiMenuWidget::QuitGame);
break;
case EMenuItem::NewGame:
PlayClose(EMenuType::NewGame);
break;
case EMenuItem::LoadRecord:
PlayClose(EMenuType::ChooseRecord);
break;
case EMenuItem::StartGameGoBack:
PlayClose(EMenuType::MainMenu);
break;
case EMenuItem::GameOptionGoBack:
PlayClose(EMenuType::MainMenu);
break;
case EMenuItem::NewGameGoBack:
PlayClose(EMenuType::StartGame);
break;
case EMenuItem::ChooseRecordGoBack:
PlayClose(EMenuType::StartGame);
break;
case EMenuItem::EnterGame:
//检测是否可以进入游戏
if (NewGameWidget->AllowEnterGame())
{
SlAiHelper::PlayerSoundAndCall(UGameplayStatics::GetPlayerController(GWorld, 0)->GetWorld(), MenuStyle->StartGameSound, this, &SSlAiMenuWidget::EnterGame);
}
else
{
//解锁按钮
ControlLocked = false;
}
break;
case EMenuItem::EnterRecord:
ChooseRecordWidget->UpdateRecordName();
SlAiHelper::PlayerSoundAndCall(UGameplayStatics::GetPlayerController(GWorld, 0)->GetWorld(), MenuStyle->StartGameSound, this, &SSlAiMenuWidget::EnterGame);
break;
}
}
可以看到,主要就是对对应的itemType执行相应的PlayClose方法
void SSlAiMenuWidget::PlayClose(EMenuType::Type NewMenu) {
//设置新的界面
CurrentMenu = NewMenu;
//设置新高度
CurrentHeight = (*MenuMap.Find(NewMenu))->MenuHeight;
//设置播放状态是Close
AnimState = EMenuAnim::Close;
//播放反向动画
MenuAnimation.PlayReverse(this->AsShared());
//播放切换菜单音乐
FSlateApplication::Get().PlaySound(MenuStyle->MenuItemChangeSound);
}
可以看到playclose里就是做了一些动画和音乐的设置,等等,按理说我们的点击事件应该是要切换控件的啊,这里为什么没有调用choosewidget?
这是因为函数中设置了当前CurrentMenu,我们加入了动画机制,所以我们会在每一帧中来选择菜单,所以重写了Tick函数
void SSlAiMenuWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) {
switch (AnimState)
{
case EMenuAnim::Stop:
break;
case EMenuAnim::Close:
//如果正在播放
if (MenuAnimation.IsPlaying()) {
//实时修改Menu的大小
ResetWidgetSize(MenuCurve.GetLerp() * 600.f, -1.f);
//在关闭了40%的时候设置不显示组件
if (MenuCurve.GetLerp() < 0.6f && IsMenuShow) ChooseWidget(EMenuType::None);
}
else {
//关闭动画完了,设置状态为打开
AnimState = EMenuAnim::Open;
//开始播放打开动画
MenuAnimation.Play(this->AsShared());
}
break;
case EMenuAnim::Open:
//如果正在播放
if (MenuAnimation.IsPlaying())
{
//实时修改Menu大小
ResetWidgetSize(MenuCurve.GetLerp() * 600.f, CurrentHeight);
//打开60%之后显示组件
if (MenuCurve.GetLerp() > 0.6f && !IsMenuShow) ChooseWidget(CurrentMenu);
}
//如果已经播放完毕
if (MenuAnimation.IsAtEnd())
{
//修改状态为Stop
AnimState = EMenuAnim::Stop;
//解锁按钮
ControlLocked = false;
}
break;
}
}
我们确实再Tick函数中找到了ChooseWidget(CurrentMenu),这样整个MenuWidget的流程就写完了,但是还有一些子控件我们没有讨论,
我们来看一下SSlAiGameOptionWidget这个控件,游戏设置控件,效果是下面这样
.h文件是这样
DECLARE_DELEGATE_TwoParams (FChangeVolume, const float, const float)
class SLAICOURSE_API SSlAiGameOptionWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SSlAiGameOptionWidget)
{}
SLATE_EVENT(FChangeVolume, ChangeVolume)
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct(const FArguments& InArgs);
private:
//统一设置样式
void StyleInitialized();
//checkbox事件
void ZhCheckBoxStateChanged(ECheckBoxState NewState);
void EnCheckBoxStateChanged(ECheckBoxState NewState);
//音量变化事件
void MusicSliderChanged(float value);
void SoundSliderChanged(float value);
private:
const struct FSlAiMenuStyle* MenuStyle;
TSharedPtr<SCheckBox> EnCheckBox;
TSharedPtr<SCheckBox> ZhCheckBox;
//进度条
TSharedPtr<SSlider> Muslider; //背景音乐
TSharedPtr<SSlider> Soslider; //音效
//进度条百分比
TSharedPtr<STextBlock> MuTextBlock;
TSharedPtr<STextBlock> SoTextBlock;
//委托变量
FChangeVolume ChangeVolume;
};
我们重点关注两个滑动条的值如何与存档里设置关联起来,达到保存设置的作用
还是一个委托函数,改变音量与音效,在滑动条被滑动的时候被执行
SAssignNew(Muslider, SSlider)
.Style(&MenuStyle->SliderStyle)
.OnValueChanged(this, &SSlAiGameOptionWidget::MusicSliderChanged)
SAssignNew(Soslider, SSlider)
.Style(&MenuStyle->SliderStyle)
.OnValueChanged(this, &SSlAiGameOptionWidget::SoundSliderChanged)
void SSlAiGameOptionWidget::MusicSliderChanged(float value) {
// 修改显示百分比
MuTextBlock->SetText(FText::FromString(FString::FromInt(FMath::RoundToInt(value * 100)) + FString("%")));
//修改音量
//SlAiDataHandle::Get()->ResetMenuVolume(value, -1.f);
ChangeVolume.ExecuteIfBound(value, -1.f);
}
void SSlAiGameOptionWidget::SoundSliderChanged(float value) {
SoTextBlock->SetText(FText::FromString(FString::FromInt(FMath::RoundToInt(value * 100)) + FString("%")));
//SlAiDataHandle::Get()->ResetMenuVolume(-1.f, value);
ChangeVolume.ExecuteIfBound(-1.f, value);
}
我们回到主页面,看看传入的委托函数是什么
//游戏设置界面
TArray<TSharedPtr<SCompoundWidget>> GameOptionList;
//实例化游戏设置的Widget
SAssignNew(GameOptionWidget, SSlAiGameOptionWidget)
.ChangeVolume(this, &SSlAiMenuWidget::ChangeVolume);
void SSlAiMenuWidget::ChangeVolume(const float MusicVolume, const float SoundVolume) {
SlAiDataHandle::Get()->ResetMenuVolume(MusicVolume, SoundVolume);
}
SlAiDataHandle也是一个单例模式,委托函数其实就是设置SlAiDataHandle里保存的音效音量的值,这样我们在滑动滑条的时候,设置的对应的值就被SlAiDataHandle拿到,拿到后就去更新存档文件里对应的值
void SlAiDataHandle::ResetMenuVolume(float MusicVol, float SoundVol) {
if (MusicVol > 0) {
MusicVolume = MusicVol;
}
if (SoundVol > 0) {
SoundVolume = SoundVol;
}
SlAiSingleton<SlAiJsonHandle>::Get()->UpdateRecordData("En", MusicVolume, MusicVolume, &RecordDataList);
}
gameoptionwidget到存档这条链路明确了,那么从存档到gameoptionwidget显示呢,怎么样让我游戏重启时就是上一次设置的值呢,请看gameoptionwidget的构造函数,构造函数中执行了StyleInitialized();
void SSlAiGameOptionWidget::StyleInitialized() {
//设置zhcheckbox样式
ZhCheckBox->SetUncheckedImage(&MenuStyle->UnCheckedBoxBrush);
ZhCheckBox->SetUncheckedHoveredImage(&MenuStyle->UnCheckedBoxBrush);
ZhCheckBox->SetUncheckedPressedImage(&MenuStyle->UnCheckedBoxBrush);
ZhCheckBox->SetCheckedImage(&MenuStyle->CheckedBoxBrush);
ZhCheckBox->SetCheckedHoveredImage(&MenuStyle->CheckedBoxBrush);
ZhCheckBox->SetCheckedPressedImage(&MenuStyle->CheckedBoxBrush);
//设置encheckbox样式
EnCheckBox->SetUncheckedImage(&MenuStyle->UnCheckedBoxBrush);
EnCheckBox->SetUncheckedHoveredImage(&MenuStyle->UnCheckedBoxBrush);
EnCheckBox->SetUncheckedPressedImage(&MenuStyle->UnCheckedBoxBrush);
EnCheckBox->SetCheckedImage(&MenuStyle->CheckedBoxBrush);
EnCheckBox->SetCheckedHoveredImage(&MenuStyle->CheckedBoxBrush);
EnCheckBox->SetCheckedPressedImage(&MenuStyle->CheckedBoxBrush);
ZhCheckBox->SetIsChecked(ECheckBoxState::Unchecked);
EnCheckBox->SetIsChecked(ECheckBoxState::Checked);
Muslider->SetValue(SlAiDataHandle::Get()->MusicVolume);
Soslider->SetValue(SlAiDataHandle::Get()->SoundVolume);
//MuTextBlock->SetText(FText::FromString(FString("100%")));
//SoTextBlock->SetText(FText::FromString(FString("100%")));
MusicSliderChanged(SlAiDataHandle::Get()->MusicVolume);
SoundSliderChanged(SlAiDataHandle::Get()->SoundVolume);
}
直接看最后四行,这里直接获得了slaidatahandle里存放的对应变量,然后set到slider控件,所以我们肯定能推断 ,datahandle里一定是构造的时候就读取到存档的内容然后进行其内所有变量的初始化,进入datahandle里看一看
SlAiDataHandle::SlAiDataHandle()
{
//初始化
InitRecordData();
InitializedMenuAudio();
}
void SlAiDataHandle::InitRecordData() {
RecordName = FString("");
//读取存档数据
FString Culture;
SlAiSingleton<SlAiJsonHandle>::Get()->RecordDataJsonRead(Culture, MusicVolume, SoundVolume, RecordDataList);
//输出
SlAiHelper::Debug(FString::SanitizeFloat(MusicVolume) + FString("--") + FString::SanitizeFloat(SoundVolume));
//循环读取RecordDataList
for (TArray<FString>::TIterator It(RecordDataList); It; ++It) {
SlAiHelper::Debug(*It, 20.f);
}
}
果不其然,我们在datahandle构造的时候,就已经把存档的内容全部读入到自己的变量中了,这样通过get拿到实例肯定是存档里的数据了,非常合理,这样从存档到界面设置的链路也通了,gameoption这个控件也大致结束了
总结
我们大致理了一下菜单这个Level的逻辑脉络,还有一些没讲到的比如模块注册,存档组件以及具体的存档读入的json转换这些,我们尽量着眼于整体来熟悉整个项目流程,日后有时间再填