主菜单支持选择关卡
2023年11月25日 2024年1月11日
Level Selection
素材
- | |
---|---|
frame.png | 用作选中关卡高亮框 |
level1.jpg | 关卡1预览图 |
level2.jpg | 关卡2预览图 |
level3.jpg | 关卡3预览图 |
预览图和关卡没有关系, 就只是三张看上去就不一样的图
导入素材
创建文件夹 ExternalContent/LevelImages
, 将素材导入该文件夹
说明
-
会在主菜单页面显示3个关卡, 供玩家选择
若玩家选择其一, 进入该关卡游玩; 若玩家未作出选择, 进入默认关卡 -
选中关卡高亮
-
关卡有预览图片, 以及显示名称
API导航
Image
设置显示图片
1/** 2 * Sets the Brush to the specified Texture. 3 * 4 * @param Texture Texture to use to set on Brush. 5 * @param bMatchSize If true, image will change its size to texture size. If false, texture will be stretched to image size. 6 */ 7UFUNCTION(BlueprintCallable, Category="Appearance") 8virtual void SetBrushFromTexture(UTexture2D* Texture, bool bMatchSize = false);
Text
FName > FText
1/** 2 * Generate an FText representing the pass name 3 */ 4static FText FromName( const FName& Val);
创建测试关卡
一共3个可选关卡
- |
---|
DefaultMap |
CubeLevel |
SphereLevel |
-
通过
File > New Level..
创建关卡, 选择Basic
, 保存到Content/Levels
, 命名为CubeLevel
-
设置
GameMode Override
为BP_STUGameModeBase
, 在关卡中添加4个PlayerStart
以及一个立方体
Place Actors > Shapes > Cube
-
拷贝
CubeLevel
, 命名为SphereLevel
Ctrl-D
移除立方体, 在关卡中添加一个球体
Place Actors > Shapes > Sphere
注意:
- 有PlayerStart使得NPC可以动态创建, 但没有添加导航, 所以测试时他们无法移动
- 添加立方体和球体在于之后进入不同关卡能够判断
创建C++类
- | |
---|---|
基类 | UserWidget |
路径 | Menu/UI |
名称 | STULevelItemWidget |
属性 | Public |
创建蓝图窗口部件
- | |
---|---|
基类 | STULevelItemWidget |
路径 | Menu/UI |
名称 | WBP_LevelItem |
设计WBP_LevelItem
-
添加
Canvas Panel
-
添加
Overlay
勾选
Size To Content
. 如果未添加Canvas Panel
, 无该选项. 以下级最大大小为准Overlay的存在使得备选关卡的元素可以组合在一起, 由4部分组成, 依次添加, 均作为变量
- 高亮框
- 关卡预览图片
- 关卡显示名称
- 按钮
变量名称 高亮框 FrameImage 关卡预览图片 LevelImage 关卡显示名称 LevelNameTextBlock 按钮 LevelSelectButton - 高亮框
-
为Overlay添加上级大小盒
- Width Override 250 Height Override 250
高亮框
- | |
---|---|
Alignment | 平铺: 高亮框大小小于关卡预览图片 |
Image | 选择 Content/ExternalContent/LevelImages/frame |
Visibility | Hidden: 默认不可见, 选择后修改可见性 |
关卡预览图片
- | |
---|---|
Padding | 15: 和高亮框留出间隔 |
Alignment | 居中 |
Brush > Image | 选择预览图片: Content/ExternalContent/LevelImages/level1 |
关卡显示名称
勾选 Is Variable
- | |
---|---|
Horizontal Alignment | 左对齐 |
Vertical Alignment | 靠近底部 |
Padding > Left | 22: 和左边界间隔 |
Padding > Bottom | 20: 和底部间隔 |
按钮
- | |
---|---|
Alignment | 平铺: 整个框框的点击都有效 |
Render Opacity | 0: 渲染按钮时使之透明 |
Cursor | 鼠标悬浮在按钮之上时, 显示为小手 |
修改WBP_Menu设计
-
添加水平盒
勾选Is Variable
, 设置变量名称为LevelItemsBox将在代码中, 往里添加可选关卡
-
添加分隔符
- Appearance > Size > Y 40: 纵向间隔 -
往水平盒中添加WBP_LevelItem, 查看预览效果
如果窗口部件的功能继续增加, 考虑创建名为LevelSelector的窗口部件, 封装往其中添加关卡项的逻辑; 而在主菜单添加LevelSelector
关卡选项存放在STUGameInstance
添加数据结构: 存放关卡选项信息
ShootThemUp: STUCoreTypes.h
1USTRUCT(BlueprintType) 2struct FLevelData 3{ 4 GENERATED_USTRUCT_BODY() 5 6 UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) 7 FName LevelName = NAME_None; 8 9 UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) 10 FName LevelDisplayName = NAME_None; 11 12 UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) 13 UTexture2D *LevelThumb; 14};
高亮框图片默认
添加数据成员: 保存关卡选项信息
之后会在BP_STUGameInstance中设置
protected
ShootThemUp: STUGameInstance.h
1#include "STUCoreTypes.h" 2 3UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) 4TArray<FLevelData> SelectedLevels; /* LevelsData */
提供接口: 获取关卡选项信息
public
ShootThemUp: STUGameInstance.h
1TArray<FLevelData> GetSelectedLevels() const { return SelectedLevels; }
在STUMenuWidget中初始化关卡选项
STUMenuWidget会创建窗口部件, 并根据配置初始化元素
STULevelItemWidget添加元素, 并提供设置接口
- 添加属性
protected
ShootThemUp: Menu/UI/STULevelItemWidget.h
1class UButton; 2class UImage; 3class UTextBlock; 4 5UPROPERTY(meta = (BindWidget)) 6UImage *FrameImage; 7 8UPROPERTY(meta = (BindWidget)) 9UImage *LevelImage; 10 11UPROPERTY(meta = (BindWidget)) 12UTextBlock *LevelNameTextBlock; 13 14UPROPERTY(meta = (BindWidget)) 15UButton *LevelSelectButton;
- 添加接口: 设置元素
public
ShootThemUp: Menu/UI/STULevelItemWidget.h
1#include "STUCoreTypes.h" 2 3void SetLevelData(const FLevelData &Data);
ShootThemUp: Menu/UI/STULevelItemWidget.cpp
1#include "Components/Image.h" 2#include "Components/TextBlock.h" 3 4void USTULevelItemWidget::SetLevelData(const FLevelData &Data) 5{ 6 if (LevelImage) 7 { 8 LevelImage->SetBrushFromTexture(Data.LevelThumb); 9 } 10 11 if (LevelNameTextBlock) 12 { 13 LevelNameTextBlock->SetText(FText::FromName(Data.LevelDisplayName)); 14 } 15}
STUMenuWidget创建窗口部件, 并初始化元素
- 添加属性: 保存关卡选项类型
protected
ShootThemUp: Menu/UI/STUMenuWidget.h
1UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) 2TSubclassOf<UUserWidget> SelectedLevelWidgetClass; /* LevelItemWidgetClass */
- 添加属性: 水平盒
protected
ShootThemUp: Menu/UI/STUMenuWidget.h
1class UHorizontalBox; 2 3UPROPERTY(meta = (BindWidget)) 4UHorizontalBox *LevelItemsBox;
- 添加辅助函数: 获取游戏实例
private
ShootThemUp: Menu/UI/STUMenuWidget.h
1class USTUGameInstance; 2 3USTUGameInstance *GetSTUGameInstance() const;
ShootThemUp: Menu/UI/STUMenuWidget.cpp
1USTUGameInstance *USTUMenuWidget::GetSTUGameInstance() const 2{ 3 if (!GetWorld()) return nullptr; 4 5 return GetWorld()->GetGameInstance<USTUGameInstance>(); 6}
- 添加美化函数: 创建关卡选项, 并添加到水平盒
private
ShootThemUp: Menu/UI/STUMenuWidget.h
1void InitLevelItemsBox(); /* InitLevelItems */
在NativeOnInitialized中调用
ShootThemUp: Menu/UI/STUMenuWidget.cpp
1// NativeOnInitialized 2InitLevelItemsBox();
实现
ShootThemUp: Menu/UI/STUMenuWidget.cpp
1#include "Components/HorizontalBox.h" 2#include "Menu/UI/STULevelItemWidget.h" 3 4void USTUMenuWidget::InitLevelItemsBox() 5{ 6 if (!LevelItemsBox) return; 7 if (!GetSTUGameInstance()) return; 8 9 const auto SelectedLevels = GetSTUGameInstance()->GetSelectedLevels(); 10 checkf(SelectedLevels.Num() != 0, TEXT("Levels data must not be empty!")); 11 // if (!SelectedLevels.Num()) return; 12 13 LevelItemsBox->ClearChildren(); 14 15 for (auto LevelData : SelectedLevels) 16 { 17 const auto LevelItemWidget = CreateWidget<USTULevelItemWidget>(GetWorld(), SelectedLevelWidgetClass); 18 if (!LevelItemWidget) continue; 19 20 LevelItemWidget->SetLevelData(LevelData); 21 22 LevelItemsBox->AddChild(LevelItemWidget); 23 } 24}
点击关卡选项时, 更新STUGameInstance的关卡信息
修改STUGameInstance存放关卡的结构体
ShootThemUp: STUGameInstance.h
-
修改关卡信息类型
由
1// protected 2 3UPROPERTY(EditDefaultsOnly) 4FName StartupLevelName = NAME_None;
改为
1// private 2FLevelData StartupLevel;
-
屏蔽获取关卡名接口, 添加获取关卡接口
由调用处自行取出分量1// FName GetStartupLevelName() const { return StartupLevelName; } 2FLevelData GetStartupLevel() const { return StartupLevel; }
-
修改GetStartupLevelName调用处
ShootThemUp: Menu/UI/STUMenuWidget.cpp
1// OnStartGame 2 3// if (STUGameInstance->GetStartupLevelName().IsNone()) 4if (STUGameInstance->GetStartupLevel().LevelName.IsNone()) 5{ 6 // ... 7} 8 9// UGameplayStatics::OpenLevel(GetWorld(), STUGameInstance->GetStartupLevelName()); 10UGameplayStatics::OpenLevel(GetWorld(), STUGameInstance->GetStartupLevel().LevelName);
-
添加设置接口
public
1void SetStartupLevel(const FLevelData &LevelData) { StartupLevel = LevelData; }
定义委托类型
若关卡被选中, 广播选中关卡信息
ShootThemUp: STUCoreTypes.h
1DECLARE_MULTICAST_DELEGATE_OneParam(FOnLevelSelectedSignature, const FLevelData &);
为STULevelItemWidget实现按钮点击回调函数
- 添加属性: 保存关卡选项, 用以委托处理函数返回
private
ShootThemUp: Menu/UI/STULevelItemWidget.h
1#include "STUCoreTypes.h" 2 3FLevelData LevelData;
- 修改SetLeverlData接口: 初始化LevelData成员
ShootThemUp: Menu/UI/STULevelItemWidget.cpp
1// SetLevelData 2LevelData = Data;
- 添加数据成员
public
ShootThemUp: Menu/UI/STULevelItemWidget.h
1FOnLevelSelectedSignature OnLevelItemSelected;
- 实现按钮回调函数
private
ShootThemUp: Menu/UI/STULevelItemWidget.h
1UFUNCTION() 2void OnLevelSelect(); /* OnLevelItemClicked */
ShootThemUp: Menu/UI/STULevelItemWidget.cpp
1void USTULevelItemWidget::OnLevelSelect() 2{ 3 OnLevelItemSelected.Broadcast(LevelData); 4}
- 覆写NativeOnInitialized, 注册按钮回调函数
protected
ShootThemUp: Menu/UI/STULevelItemWidget.h
1virtual void NativeOnInitialized() override;
ShootThemUp: Menu/UI/STULevelItemWidget.cpp
1#include "Components/Button.h" 2 3void USTULevelItemWidget::NativeOnInitialized() 4{ 5 Super::NativeOnInitialized(); 6 7 if (LevelSelectButton) 8 { 9 LevelSelectButton->OnClicked.AddDynamic(this, &USTULevelItemWidget::OnLevelSelect); 10 } 11}
在STUMenuWidget中注册关卡选中事件
STULevelItemWidget提供设置高亮框可见性接口
public
ShootThemUp: Menu/UI/STULevelItemWidget.h
1void SetFrameImageVisibility(bool Visible); /* SetSelected */
ShootThemUp: Menu/UI/STULevelItemWidget.cpp
1void USTULevelItemWidget::SetFrameImageVisibility(bool Visible) 2{ 3 if (!FrameImage) return; 4 FrameImage->SetVisibility(Visible ? ESlateVisibility::Visible : ESlateVisibility::Hidden); 5}
STUMenuWidget添加数组, 存放已创建的关卡选项
private
ShootThemUp: Menu/UI/STUMenuWidget.h
1class USTULevelItemWidget; 2 3UPROPERTY() 4TArray<USTULevelItemWidget *> LevelItemWidgets;
ShootThemUp: Menu/UI/STUMenuWidget.cpp
1// InitLevelItemsBox 2LevelItemWidgets.Add(LevelItemWidget);
STULevelItemWidget添加接口, 获取LevelData
public
ShootThemUp: Menu/UI/STULevelItemWidget.h
1FLevelData GetLevelData() const { return LevelData; }
实现处理函数
private
ShootThemUp: Menu/UI/STUMenuWidget.h
1#include "STUCoreTypes.h" 2 3void OnLevelSelected(const FLevelData &LevelData);
ShootThemUp: Menu/UI/STUMenuWidget.cpp
1void USTUMenuWidget::OnLevelSelected(const FLevelData &LevelData) 2{ 3 if (!GetSTUGameInstance()) return; 4 GetSTUGameInstance()->SetStartupLevel(LevelData); 5 6 for (auto LevelItem : LevelItemWidgets) 7 { 8 if (LevelItem) 9 { 10 LevelItem->SetFrameImageVisibility(LevelItem->GetLevelData().LevelName == LevelData.LevelName); 11 } 12 } 13}
注册服务
ShootThemUp: Menu/UI/STUMenuWidget.cpp
1// InitLevelItemsBox 2LevelItemWidget->OnLevelItemSelected.AddUObject(this, &USTUMenuWidget::OnLevelSelected);
设置默认关卡, 标记上次打开关卡
每回去到主关卡, 都会调用NativeOnInitialized, 而游戏实例中存储的StartupLevel设置后一直有值
- 开始时, 设置默认关卡
- 之后, StartupLevel保存上次打开关卡
ShootThemUp: Menu/UI/STUMenuWidget.cpp
1// InitLevelItemsBox 2 3if (GetSTUGameInstance()->GetStartupLevel().LevelName.IsNone()) 4{ 5 OnLevelSelected(GetSTUGameInstance()->GetSelectedLevels()[0]); 6} 7else 8{ 9 OnLevelSelected(GetSTUGameInstance()->GetStartupLevel()); 10}
查看
-
设置关卡选项类
WBP_Menu
- Selected Level Widget Class WBP_LevelItem -
配置关卡选项
BP_STUGameInstance
-
查看
如果关卡选项关卡名重复, 选择其中一个, 其他重复的也会高亮, 这是因为关卡使用关卡名标识, 没有标识ID的原因
配置时给出提示
ShootThemUp: STUGameInstance.h
1UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, meta = (ToolTip = "Level names must be unique!")) 2TArray<FLevelData> SelectedLevels;