本文的目的在于学完后通过本文能及时回想起整个项目脉络,方便在面试前进行整体回顾,并不是教程文档,请先学习完腾讯课堂梁迪老师的课程

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选择为ASlAiHUDClass = ASlAiMenuHUD::StaticClass();  //世界设置中的HUDClass选择为ASlAi
}

GameMode如何与菜单Level绑定? 进入UE里关卡的世界设置进行选定

现在重点就是HUD显示了,看看我们上面绑定的HUD类里面有什么

//SlAiMenuHUDUCLASS()
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里面了

//MenuHUDWidgetclass 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;//实例化游戏设置的WidgetSAssignNew(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;//设置动画播放器跳到结尾,也就是1MenuAnimation.JumpToEnd();
}

其中ChooseWidget选择主界面展示,其实就是把Map中组装好的控件填入MenuWidget垂直框的插槽

void SSlAiMenuWidget::ChooseWidget(EMenuType::Type WidgetType)
{//定义是否已经显示菜单IsMenuShow = WidgetType != EMenuType::None;//移出所有组件ContentBox->ClearChildren();//如果Menutype是Noneif (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) {//如果锁住了,直接returnif (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;//设置播放状态是CloseAnimState = 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()){//修改状态为StopAnimState = 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;//实例化游戏设置的WidgetSAssignNew(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));//循环读取RecordDataListfor (TArray<FString>::TIterator It(RecordDataList); It; ++It) {SlAiHelper::Debug(*It, 20.f);}}

果不其然,我们在datahandle构造的时候,就已经把存档的内容全部读入到自己的变量中了,这样通过get拿到实例肯定是存档里的数据了,非常合理,这样从存档到界面设置的链路也通了,gameoption这个控件也大致结束了

总结

我们大致理了一下菜单这个Level的逻辑脉络,还有一些没讲到的比如模块注册,存档组件以及具体的存档读入的json转换这些,我们尽量着眼于整体来熟悉整个项目流程,日后有时间再填

UE4 C++纯slate开发沙盒游戏(一) 菜单部分相关推荐

  1. [UE4入门笔记(13)] 40.准星 41.射线检测(续第12篇) 42.行为状态机 --梁迪老师UE4纯C++Slate开发沙盒游戏

    目录 前言: 本篇学习内容: 40.准星 41.射线检测(续第12篇) 42.行为状态机 前言: 笔者目前在校本科大三,目标方向是人工智能.计算机视觉.上一个OpenCV学习笔记专栏已完结,在学习完O ...

  2. 这个沙盒游戏建立在数字时代,你能通关吗?

    从没有一个时代像数字时代一样拥有如此多的"连接".在沙盒游戏中,玩家用"连接"创造和改变世界:而身处数字时代就如同在一个巨大的沙盒游戏中,组织用"连接 ...

  3. 阐述沙盒游戏的历史和理论

    来自 GameRes:http://www.gameres.com/msg_233466.html "沙盒"现在是游戏圈中的热词.与"自由"或"爱&q ...

  4. linux沙盒游戏,沙盒游戏_PE沙盒游戏合集,欢迎⊙ω⊙_安卓应用游戏下载- AppChina应用汇...

    中文名:我的世界 原版名称:Minecraft 其他名称:麦块.MC.当个创世神 游戏类型:沙盒.生存.冒险 游戏平台:Windows.Linux.OS X,Android(Pocket Editio ...

  5. 基于Unity3D的体素沙盒游戏设计与实现(上)

    基于Unity3D的体素沙盒游戏设计与实现 摘    要 随着计算机硬件和软件技术的逐步发展,世界游戏开发行业也在日益壮大,涌现出不少优秀的作品,逐渐成为各国文化创意领域一张闪亮的名片.本文以全球知名 ...

  6. 不仅仅是建模 6个步骤解析沙盒游戏场景设计

    游戏场景是怎么制作的?尤其是沙盒游戏,开放的世界对场景设计的要求更为严苛.场景制作当然需要美术做模型,但这不是问题的根本.从以下几个方面来说: 游戏标准尺寸 地形 路网和区域划分 游戏内容密度 工作量 ...

  7. 游戏场景建模需要会美术画画吗?来看沙盒游戏是怎么做出来的

    做游戏场景设计可以不用画画吗? 游戏场景是怎么制作的?尤其是沙盒游戏,开放的世界对场景设计的要求更为严苛.场景制作当然需要美术做模型,但这不是问题的根本.从以下几个方面来说: 游戏标准尺寸 地形 路网 ...

  8. 20年,1人写出70万行代码!沙盒游戏「鼻祖」13年靠玩家捐赠维生

    本文转载自 新智元 一款游戏,让一位玩家皈依佛教. 这位玩家曾寄信开发者,因其开发的游戏理解了「众生皆苦,世事无常」. 究竟是什么游戏还能让人看破红尘? Dwarf Fortress (矮人堡垒)! ...

  9. 教程:如何在优麒麟上畅玩沙盒游戏--《我的世界》

    技术文章看累了?接下来由小优带领大家轻松一下,教大家如何在优麒麟上畅玩沙盒游戏–<我的世界>.本篇教程来自优麒麟核心爱好者陌生人的投稿,也欢迎大家与我们一起分享你喜爱的游戏哦! 01 安装 ...

最新文章

  1. 宝塔mysql优化_宝塔面板下实现MySQL性能优化处理
  2. 6、android传递数据之剪切板传递数据
  3. 函数 php_PHP函数缺陷详解
  4. 【大盛】全网首发HTC One/M7 最新本地化TrickDroid9.0/固件升级/永久root/高级,快速设置/稳定,流畅经典ROM...
  5. ds1302典型应用原理图_不同类型的光纤激光器,在工业中有哪些典型应用
  6. 越南 linux_从越南到阿姆斯特丹陷入Linux和开源
  7. 公司java框架让程序员变笨_框架会使程序员变笨吗?
  8. LINUX系统用户操作命令
  9. regex flag
  10. 400本以上电子书、1000门以上课程会员免费看,快来领取!
  11. Iocomp Ultra Pack ActiveX 5.12
  12. NVIDIA Nsight Compute,Nsight Systems, Nsight Graphics,Nsight Deep Learning Designer简介-草稿
  13. mysql下载 补丁_mysql 官方补丁在哪里下载?
  14. android youtube webview,android – 如何在WebView中全屏显示youtube视频
  15. STM32单片机的学习方法(方法大体适用所有开发版入门)
  16. CF140C New Year Snowmen(贪心+优先队列)
  17. android视频自动旋转,Android 使用PLDroidPlayer播放网络视频 根据视频角度自动旋转...
  18. 《蜂鸟摄影学院单反摄影宝典》读书笔记
  19. 好久没来,深夜来一发
  20. 专业系统维护:CleanMyMac X for mac

热门文章

  1. 去除 MIUI 开发版拍照声音
  2. web前端——页面设计
  3. partition by的用法
  4. 哪些人不建议学IT?
  5. vue 本地背景图片铺满整个屏幕
  6. 水仙花数(daffodil)
  7. Juniper SRX 简单命令一
  8. 高德地图加渐变色3D线段
  9. 苹果手机的屏幕是什么屏幕
  10. HTML+纯JS制作音乐播放器