Files
UnrealEngineUWP/Engine/Documentation/Source/Programming/Slate/InGameUI/QuickStart/SlateInGameUIQuickStart.CHN.udn
Mitchell Wilson 0b47855b71 Copying //UE4/Dev-Documentation to Samples-Main (//UE4/Samples-Main) CL - 4860397
#rb none

[CL 4860421 by Mitchell Wilson in Main branch]
2019-01-31 15:30:04 -05:00

563 lines
20 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
INTSourceChangelist:3238697
Title:Slate游戏内UI快速入门
Crumbs:
Description:
Availability:Docs
Version:4.9
## 项目设置
首先,创建一个新的基础代码项目:
[REGION:fullwidth]
![](image001.png)
[/REGION]
默认情况下,这样会提供以下类:
![](image003.png)
## 在屏幕上显示内容
1. 除了默认类我还添加了一个新类“MainMenu”来封装我将会创建的菜单。为了在屏幕上显示一些内容首先创建一些简单的函数来构造菜单并创建一些用于打开和关闭隐藏和显示菜单的函数。我还添加了一个成员变量“SWidget共享指针”来保持对菜单层级中的最上方Slate控件的引用。
#pragma once
#include "Slate.h"
class MainMenu : public TSharedFromThis<MainMenu>
{
public:
/** 构造菜单所需的控件等 */
void ConstructMenu();
/** 通过向GameViewport控件内容添加根控件来显示菜单 */
void OpenMenu();
/** 通过从GameViewport控件内容移除根控件来关闭菜单 */
void CloseMenu();
private:
/** 这是菜单的最根部Slate控件。其他都是它的子代。*/
TSharedPtr<SWidget> MenuRoot;
};
MainMenu类派生自TSharedFromThis模板因为这有助于传递对“this”的安全引用以用于“Slate菜单”事件的代理函数我们稍后再讨论这一点。
1. 为菜单类函数添加基本实现:
#include "SlateGameMenuExample.h"
#include "MainMenu.h"
#define LOCTEXT_NAMESPACE "MainMenu"
void MainMenu::ConstructMenu()
{
MenuRoot = SNew(SSearchBox)
.HintText(LOCTEXT("FilterSearch", "Search..."))
.ToolTipText(LOCTEXT("FilterSearchHint", "Type here to search").ToString());
}
void MainMenu::OpenMenu()
{
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->AddViewportWidgetContent(MenuRoot.ToSharedRef());
FSlateApplication::Get().SetKeyboardFocus(MenuRoot.ToSharedRef());
}
}
void MainMenu::CloseMenu()
{
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->RemoveViewportWidgetContent(MenuRoot.ToSharedRef());
FSlateApplication::Get().ClearKeyboardFocus(EFocusCause::Cleared);
}
}
#undef LOCTEXT_NAMESPACE
为了在屏幕上显示内容我从代码中的任意地方选择了一个随机Slate控件作为临时MenuRoot。然后它会被添加到GameViewport的ViewportWidgetContent这样就会显示出来。此外我们确保SlateMenu获得键盘焦点以便它能够响应输入。注意LOCTEXT宏的使用务必始终使用它来表示必须本地化为其他语言的任意文本。本博文将详细讨论这一点
[在UE4中创建可本地化的游戏第1部分——文本](https://www.unrealengine.com/blog/creating-a-localization-ready-game-in-ue4-part-1-text)
接下来我添加了一个新的GameMode来表示位于主菜单“MainMenuGameMode”的玩家的状态。这只是定义我们用于这种游戏模式的PlayerController类型使用BeginPlay事件打开主菜单并在共享指针中保持对MainMenu的引用。
#pragma once
#include "GameFramework/GameModeBase.h"
#include "MainMenuGameMode.generated.h"
class MainMenu;
/**
*
*/
UCLASS()
class AMainMenuGameMode : public AGameModeBase
{
GENERATED_UCLASS_BODY()
protected:
virtual void BeginPlay() override;
public:
TSharedPtr<MainMenu> MainMenuPtr;
};
这里需要注意GameMode是UClass因此需要标准的#include用于生成的头UCLASS()宏放在类定义前面GENERATED_UCLASS_BODY()放在类定义开头。
#include "SlateGameMenuExample.h"
#include "MainMenuGameMode.h"
#include "SlateGameMenuExamplePlayerController.h"
#include "MainMenu.h"
AMainMenuGameMode::AMainMenuGameMode(const FObjectInitializer& ObjectInitializer)
:Super(ObjectInitializer)
{
PlayerControllerClass = ASlateGameMenuExamplePlayerController::StaticClass();
}
void AMainMenuGameMode::BeginPlay()
{
MainMenuPtr = MakeShareable(new MainMenu());
MainMenuPtr->ConstructMenu();
MainMenuPtr->OpenMenu();
}
现在我们需要将这个新的GameMode指定为要使用的模式。这可以逐个关卡地在场景设置中指定
![](image005.png)
或者通过“项目设置Project Settings>贴图和模式Maps & Modes”中的默认GameMode设置
[REGION:fullwidth]
![](image007.png)
[/REGION]
这样我们就能够在屏幕上显示一些内容:
[REGION:fullwidth]
![](image009.png)
[/REGION]
我添加的这个搜索框控件占据了整个视口,但这是一个好的起点。
添加一些按钮
现在,让我们来处理一下这个菜单,让它看起来更像是一个常见的“主菜单”。或许类似于下面这个简单的布局:
![](image011.png)
这里在一个更大的区域中包含了三个按钮。“播放”按钮用于启动游戏“退出”按钮用于退出应用程序“选项”按钮可以转至另一个菜单。为此我修改了ConstructMenu函数如下
#include "StyleDefaults.h"
void MainMenu::ConstructMenu()
{
const FSlateBrush* NoBorderBrush = FStyleDefaults::GetNoBrush();
MenuRoot =
SNew(SBorder)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.FillHeight(1.f)
.HAlign(HAlign_Center)
[
SNew(SBorder)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.BorderImage(NoBorderBrush)
[
SNew(STextBlock)
.Text(LOCTEXT("SlateGameMenuTitle", "Slate Game Menu!"))
]
]
+ SVerticalBox::Slot()
.FillHeight(1.f)
.HAlign(HAlign_Center)
[
SNew(SBorder)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.BorderImage(NoBorderBrush)
[
SNew(SButton)
.OnClicked(FOnClicked::CreateSP(this, &MainMenu::StartGame))
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.Text(LOCTEXT("StartGameButtonText", "Start Game"))
]
]
+ SVerticalBox::Slot()
.FillHeight(1.f)
.HAlign(HAlign_Center)
[
SNew(SBorder)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.BorderImage(NoBorderBrush)
[
SNew(SButton)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
//.OnClicked(FOnClicked::CreateSP(this, &MainMenu::OpenOptionsMenu))
.Text(LOCTEXT("OptionsButtonText", "Options..."))
]
]
+ SVerticalBox::Slot()
.FillHeight(1.f)
.HAlign(HAlign_Center)
[
SNew(SBorder)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.BorderImage(NoBorderBrush)
[
SNew(SButton)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.OnClicked(FOnClicked::CreateSP(this, &MainMenu::QuitGame))
.Text(LOCTEXT("QuitButtonText", "Quit Game"))
]
]
]
;
}
如您所见Slate有一些特殊语法让您可以使用各种运算符来修改属性这些链接提供了一些有关Slate声明性语法和结构的信息。您可以在使用SNew(SWidget)后,通过“.”运算符来设置Slate属性并可以用括号运算符在其他控件内部嵌套子控件。
上述示例在所有内容外部使用了SBorder内部使用了一个SVerticalBox它有4个插槽用于包含菜单标题文本和三个按钮。使用了多种对齐方式和大小来得到所需的外观。该链接提供了有关Slate中的“布局”工作方式的更多信息。这些添加最终会产生类似于以下示例的菜单
![](image013.png)
这更接近于我们想要的效果。请注意上述代码中已经设置了SButtons的OnClicked属性。这是一个代理在您单击该按钮时将调用您选择的函数。“开始游戏”按钮的OnClicked声明类似于
.OnClicked(FOnClicked::CreateSP(this, &MainMenu::StartGame))
FOnClicked是控件想要通知用户已经单击过这些控件时调用的一类代理旨在供按钮和其他类似于按钮的控件使用。要关联代理您需要指定要对其调用函数的对象、要调用的函数和要传递给函数的可选参数。您可以使用不同的函数来指定代理我在这里使用的是CreateSP这是基于共享指针的成员函数代理这也是MainMenu派生自TSharedFromThis的原因所以我使用更安全的共享指针选项。您还可以结合使用原始C++指针和CreateRaw或者可以结合使用静态函数和CreateStatic这样就无需指定要对其调用函数的对象
现在,已经指定了“开始游戏”和“退出游戏”按钮的代理,接下来需要定义它们调用的函数。这些是非常简单的函数。
FReply MainMenu::StartGame()
{
if (Controller.IsValid())
{
Controller->GetWorld()->ServerTravel("/Game/GameLevel");
}
CloseMenu();
return FReply::Handled();
}
此函数使用ServerTravel来告诉引擎切换到另一个地图来开始游戏。然后我们关闭菜单。您或许注意到了该函数正在返回FReply。Reply是指Slate事件返回给系统以通知其事件处理方式的特定方面的信息。例如控件处理OnMouseDown事件的方式可能是告诉系统对特定控件给予鼠标捕获为此return FReply::CaptureMouse( NewMouseCapture )。)
FReply MainMenu::QuitGame()
{
CloseMenu();
if (MyPlayerController.IsValid())
{
MyPlayerController->ConsoleCommand("quit");
}
return FReply::Handled();
}
QuitGame函数仅使用控制台命令“quit”在关闭菜单后退出游戏。
这两个函数都使用对玩家控制器的引用来调用函数这意味着我必须修改MainMenu.cpp以保持对PlayerController的WeakObjectPtr并通过构造函数来进行这项设置
MainMenu::MainMenu(TWeakObjectPtr<class APlayerController> InController)
: MyPlayerController(InController)
{
}
MainMenuGameMode仅抓取第一个PlayerController并将抓取的信息传递给MainMenu类
#include "../../../../../../Engine/Source/Runtime/Engine/Classes/Kismet/GameplayStatics.h"
void AMainMenuGameMode::BeginPlay()
{
APlayerController* FirstPC = NULL;
if (GetWorld() != NULL)
{
// 玩家0获得UI
FirstPC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
}
MainMenuPtr = MakeShareable(new MainMenu(TWeakObjectPtr<APlayerController>(FirstPC)));
MainMenuPtr->ConstructMenu();
MainMenuPtr->OpenMenu();
}
这里的另外两个问题是在默认情况下没有绘制鼠标光标因此玩家很难单击我们刚刚创建的按钮。此外PlayerController仍在使用鼠标和键盘输入来控制场景中的角色和摄像机因此移动鼠标来单击按钮也会移动菜单背后的场景摄像机。要解决这两个问题可以启用CinematicMode来暂时禁用玩家输入并告诉PlayerController显示鼠标光标
void MainMenu::OpenMenu()
{
if (GEngine && GEngine->GameViewport)
{
// 将菜单控件内容添加到游戏视口以便能显示它
UGameViewportClient* const GVC = GEngine->GameViewport;
GVC->AddViewportWidgetContent(MenuRoot.ToSharedRef());
if (MyPlayerController.IsValid())
{
// 启用鼠标光标并禁用其他输入(移动鼠标不会旋转摄像机等)。
MyPlayerController->SetCinematicMode(true, false, false, true, true);
MyPlayerController->bShowMouseCursor = true;
}
}
}
关闭菜单起到相反作用:
void MainMenu::CloseMenu()
{
if (GEngine && GEngine->GameViewport)
{
// 从游戏视口移除菜单控件内容,以便不再显示它
UGameViewportClient* const GVC = GEngine->GameViewport;
GVC->RemoveViewportWidgetContent(MenuRoot.ToSharedRef());
FSlateApplication::Get().ClearKeyboardFocus(EFocusCause::Cleared);
if (MyPlayerController.IsValid())
{
// 重新启用其他输入并移除鼠标光标
MyPlayerController->SetCinematicMode(false, false, false, true, true);
MyPlayerController->bShowMouseCursor = false;
}
}
}
定义和使用Slate样式
接下来我们想要定义和使用一些Slate样式便于我们快速并轻松地更改菜单样式。样式只包含一些文本字符串用来引用笔刷即包含如何绘制Slate元素的相关信息的容器、控件样式、Slate字体信息等。
例如,您可以为工具栏按钮定义样式,如:
// 普通按钮
FButtonStyle Button = FButtonStyle()
.SetNormal(BOX_BRUSH("Button", FVector2D(32, 32), 8.0f / 32.0f))
.SetHovered(BOX_BRUSH("Button_Hovered", FVector2D(32, 32), 8.0f / 32.0f))
.SetPressed(BOX_BRUSH("Button_Pressed", FVector2D(32, 32), 8.0f / 32.0f))
.SetDisabled(BOX_BRUSH("Button_Disabled", 8.0f / 32.0f))
.SetNormalPadding(FMargin(2, 2, 2, 2))
.SetPressedPadding(FMargin(2, 3, 2, 1));
Style->Set("MyButtonStyle ", Button);
或文本样式,如:
const FTextBlockStyle NormalText = FTextBlockStyle()
.SetFont(TTF_FONT("Fonts/Roboto-Regular", 9))
.SetColorAndOpacity(FSlateColor::UseForeground())
.SetShadowOffset(FVector2D::ZeroVector)
.SetShadowColorAndOpacity(FLinearColor::Black)
.SetHighlightColor(FLinearColor(0.02f, 0.3f, 0.0f))
.SetHighlightShape(BOX_BRUSH("TextBlockHighlightShape", FMargin(3.f / 8.f)));
Style->Set("NormalText", NormalText);
为了完成代码上述BRUSH和FONT宏按如下所示定义以简化定义
#define IMAGE_BRUSH( RelativePath, ...) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ )
#define BOX_BRUSH( RelativePath, ...) FSlateBoxBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ )
#define BORDER_BRUSH( RelativePath, ...) FSlateBorderBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ )
#define TTF_FONT( RelativePath, ...) FSlateFontInfo( Style->RootToContentDir( RelativePath, TEXT(".ttf") ), __VA_ARGS__ )
#define OTF_FONT( RelativePath, ...) FSlateFontInfo( Style->RootToContentDir( RelativePath, TEXT(".otf") ), __VA_ARGS__ )
这些样式可以按如下所示用于按钮:
SNew(SButton) // Start Game button
.ButtonStyle(&FMySlateStyle::Get().GetWidgetStyle<FButtonStyle>("MyButtonStyle"))
.TextStyle(&FMySlateStyle::Get().GetWidgetStyle<FTextBlockStyle>("NormalText"))
.OnClicked(FOnClicked::CreateSP(this, &MainMenu::StartGame))
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.Text(LOCTEXT("StartGameButtonText", "Start Game"))
对于示例Slate菜单我创建了一个单独的类叫做MySlateStyle它创建了两个FSlateStyleSets样式组它们定义相同的样式但定义不同的视觉外观。这样我就能够轻松地切换两个样式集。上述创建的FLateStyleSet中的“Style”变量包含一组按照以上方式设置的样式然后按如下所示注册到Slate样式注册表中
FSlateStyleRegistry::RegisterSlateStyle(NewStyle);
最初我将一些随机样式输入进去并得出这样的结果:
![](image015.png)
当然不美观,但现在可以轻松地迭代来加以美化。
再添加一个菜单
上文我解释了如何让“开始游戏”和“退出游戏”按钮工作但“选项”按钮仍无法工作。该按钮应该是让玩家单击按钮后前往另一个菜单。毫无疑问可以使用ServerTravel移到另一个关卡打开另一个菜单然后单击“后退”时返回到MainMenu关卡但我采用了另一个方法。
我想在主菜单上弹出选项菜单所以将主菜单包含在SOverlay中这样就可以定义不同的可以重叠的内容插槽。这个重叠成为菜单的新根控件。
在构造函数中,我构造了两个菜单:
void MainMenu::ConstructMenu()
{
// ...
HeaderFontInfo = FMySlateStyle::Get().GetFontStyle("RichText.Header");
// 设置主菜单控件内容
MenuRoot =
SNew(SOverlay) // 用于允许“选项”菜单被“覆盖”或显示在该菜单上方的重叠
+SOverlay::Slot() // New Slot for the overlay.这只包含主菜单内容
[
// ...
// 主菜单控件
/ ...
]
;
// ...
// 构建选项菜单控件
OptionsMenuRoot = SNew(SBorder)
.Cursor(EMouseCursor::Default)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
// ...
// 主菜单控件
/ ...
]
;
}
当我想要显示选项菜单时我把选项菜单的根控件放入根SOverlay中的新插槽中
FReply MainMenu::OpenOptionsMenu()
{
SOverlay* MenuRootOverlay = (SOverlay*)MenuRoot.Get();
if (MenuRootOverlay)
{
// 将另一个插槽添加到主菜单的重叠,并在其中放入我们的“选项”菜单内容
OptionsMenuSlot = &MenuRootOverlay->AddSlot()
[
OptionsMenuRoot.ToSharedRef()
]
;
}
return FReply::Handled();
}
当我想要隐藏选项菜单时,我会移除该插槽:
FReply MainMenu::CloseOptionsMenu()
{
SOverlay* MenuRootOverlay = (SOverlay*)MenuRoot.Get();
if (MenuRootOverlay != NULL)
{
// 移除选项菜单重叠插槽
MenuRootOverlay->RemoveSlot(OptionsMenuSlot->Widget);
}
return FReply::Handled();
}
为选项菜单创建更多随机样式,如以下示例:
![](image017.png)
更改选项
在以上截图中,您会看到,有一些用于更改菜单样式和字体样式的选项。
这些同样使用一些简单的代理来调用这些函数。
void MainMenu::StyleComboBoxSelectionChanged(TSharedPtr<FString> StringItem, ESelectInfo::Type SelectInfo)
{
if (StringItem->Equals("Style1"))
{
FMySlateStyle::SetStyle1();
}
else
{
FMySlateStyle::SetStyle2();
}
// 进行这些更改后,关闭菜单,重新构造并再次打开
CloseOptionsMenu();
CloseMenu();
ConstructMenu();
OpenMenu();
OpenOptionsMenu();
}
void MainMenu::FontSize_ValueChanged(int32 InValue)
{
HeaderFontInfo.Size = InValue;
MenuHeaderText->SetFont(HeaderFontInfo);
OptionsMenuHeaderText->SetFont(HeaderFontInfo);
}
对STextBlock对象的引用通过在构造函数中使用SAssignNew来维护以将产生的控件对象分配给可以在代理函数中引用的成员变量
SAssignNew(OptionsMenuHeaderText, STextBlock) // Header text
.TextStyle(&FMySlateStyle::Get().GetWidgetStyle<FTextBlockStyle>("RichText.Header"))
.Text(LOCTEXT("OptionsMenuTitle", "Options!"))
由于更改样式复选框选择会导致菜单被重新构建,因此需要一点额外的代码来确保菜单在重新构建后选择正确的样式,这部分代码也在构造函数中编写:
// 保存样式列表
StyleList.Empty();
StyleList.Add(MakeShareable(new FString("Style1")));
StyleList.Add(MakeShareable(new FString("Style2")));
// 在菜单样式更改时调用构造函数菜单,因此确保菜单样式复选框中的选择在做出新选择时是最新的
FString CurrentStyleName = FMySlateStyle::Get().GetStyleSetName().ToString();
TSharedPtr<FString> CurrentlySelectedStyle;
for (TSharedPtr<FString> StyleString :StyleList)
{
if (StyleString->Equals(CurrentStyleName))
{
CurrentlySelectedStyle = StyleString;
}
}
if (!CurrentlySelectedStyle.IsValid() && StyleList.Num() > 0)
{
CurrentlySelectedStyle = StyleList[0];
}
现在,更改样式下拉菜单会产生另一种样式(虽然样式本身仍需要处理):
![](image019.png)
现在,更改字体滑块也会更改字体大小:
![](image021.png)
创建一些合适的背景和样式后,最终菜单应该类似于以下示例:
[REGION:imagetable]
|![](image023.png)(w:460) | ![](image025.png)(w:460) |
| ------ | ------ |
| 使用样式1的主菜单 | 使用样式2的选项菜单 |
[/REGION]
## 使用控件反射器
在设计Slate菜单时特别有用的一个工具是控件反射器可以从“开发者工具”菜单访问。
[REGION:fullwidth]
![](image027.png)
![](image029.png)
[/REGION]
它按照控件存在的层级显示存在的每一个Slate控件甚至显示其可见性以及创建它的代码文件和代码行。在调试菜单时或者调查其他菜单以了解它们的创建方式时该信息非常有用。此外“选取控件”按钮允许您从控件层级中查找鼠标所在的下部件
[REGION:fullwidth]
![](image031.png)
[/REGION]
您还可以再次按下这个按钮或按ESC键来冻结当前选中的控件。