UE4开发C++沙盒游戏教程笔记(十九)(对应教程 58 ~ 60)
57. 小地图敌人视野
在 Material 目录下创建一个 Material Function,取名 EnemyViewMatFun,在里面作如下更改:(左边的红色节点 234 都是要转换成参数然后改名的)
再创建一个 Material,取名 EnemyViewMat,在里面修改如下:(定义 10 个名为 Position_{n} 的 4 参数变量和 10 个名为 Angle_{n} 的单个参数变量,经过 EnemyViewMatFun(要输入 MaterialFunctionCall 才能获取这个节点)后全部通过 Add 节点添加起来(由于图片再截大点就看不清了,所以只截上面大部分,下部分的左边也是跟上部分一样的)。一对 Position 和 Angle 对应一个敌人的视野范围,所以这种做法最多只能在小地图上显示十个敌人的视野范围,我们后面也只打算在场景里放十个敌人。超出了十个敌人可能会有 Bug。
创建 EnemyViewMat 的材质实例,取名 EnemyViewMatInst。把里面 Position_1~10、Angle_1~10 和 Scale 的变量都勾选上。
来到代码方面,在敌人类添加一个方法用于判定敌人是否锁定了玩家。这个返回的 bool 结果决定了敌人的扇形视野是否会在小地图上显示。
SlAiEnemyCharacter.h
public:
// 获取是否已经锁定了玩家
bool IsLockPlayer();
SlAiEnemyCharacter.cpp
bool ASlAiEnemyCharacter::IsLockPlayer()
{
if (SEController) return SEController->IsLockPlayer;
return false;
}
来到 GameMode,给之前声明的一些没有填入元素的数组对象进行填充。
SlAiGameMode.cpp
// 引入头文件
#include "SlAiEnemyCharacter.h"
#include "EngineUtils.h"
void ASlAiGameMode::InitializeMiniMapCamera()
{
if (IsCreateMiniMap) {
MiniMapCamera->UpdateTransform(SPCharacter->GetActorLocation(), SPCharacter->GetActorRotation());
TArray<FVector2D> EnemyPosList;
TArray<bool> EnemyLockList;
TArray<float> EnemyRotateList;
// 获取场景中的敌人
for (TActorIterator<ASlAiEnemyCharacter> EnemyIt(GetWorld()); EnemyIt; ++EnemyIt) {
FVector EnemyPos = FVector((*EnemyIt)->GetActorLocation().X - SPCharacter->GetActorLocation().X, (*EnemyIt)->GetActorLocation().Y - SPCharacter->GetActorLocation().Y, 0.f);
EnemyPos = FQuat(FVector::UpVector, FMath::DegreesToRadians(-SPCharacter->GetActorRotation().Yaw - 90.f)) * EnemyPos;
EnemyPosList.Add(FVector2D(EnemyPos.X, EnemyPos.Y));
EnemyLockList.Add((*EnemyIt)->IsLockPlayer());
EnemyRotateList.Add((*EnemyIt)->GetActorRotation().Yaw - SPCharacter->GetActorRotation().Yaw);
}
UpdateMapData.ExecuteIfBound(SPCharacter->GetActorRotation(), MiniMapCamera->GetMapSize(), &EnemyPosList, &EnemyLockList, &EnemyRotateList);
}
}
来到小地图界面,添加一个显示敌人视野的 SImage。再添加三个跟敌人视野的显示有关的变量。
SSlAiMiniMapWidget.h
private:
// 显示敌人视野的图片
TSharedPtr<SImage> EnemyViewImage;
// 小地图尺寸
float MapSize;
// 敌人相对于玩家的位置
TArray<FVector2D> EnemyPos;
// 敌人是否锁定了玩家
TArray<bool> EnemyLock;
在小地图界面添加敌人视野的界面结构;在 RegisterMiniMap() 实例化敌人视野的材质和笔刷并赋给 SImage 指针。
在 UpdateMapData() 里面添加数据计算逻辑,包括敌人标志该显示在小地图上的什么位置,敌人视野该在地图上如何显示等等(这一段的数学运算可能会比较复杂)。
随后在绘制函数里面绘制敌人在小地图上的圆点。
SSlAiMiniMapWidget.cpp
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiMiniMapWidget::Construct(const FArguments& InArgs)
{
GameStyle = &SlAiStyle::Get().GetWidgetStyle<FSlAiGameStyle>("BPSlAiGameStyle");
ChildSlot
[
// ... 省略
// 渲染敌人视野的图片
+SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SAssignNew(EnemyViewImage, SImage)
]
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiMiniMapWidget::RegisterMiniMap(class UTextureRenderTarget2D* MiniMapRender)
{
// 敌人视野材质设定
UMaterialInterface* EnemyViewMatInst = LoadObject<UMaterialInterface>(NULL, TEXT("MaterialInstanceConstant'/Game/Material/EnemyViewMatInst.EnemyViewMatInst'"));
// 创建动态材质
EnemyViewMatDynamic = UMaterialInstanceDynamic::Create(EnemyViewMatInst, nullptr);
// 实例化 EnemyView 笔刷
FSlateBrush* EnemyViewBrush = new FSlateBrush();
// 设置属性
EnemyViewBrush->ImageSize = FVector2D(280.f, 280.f);
EnemyViewBrush->DrawAs = ESlateBrushDrawType::Image;
// 绑定材质资源文件
EnemyViewBrush->SetResourceObject(EnemyViewMatDynamic);
// 将笔刷作为 MiniMapImage 的笔刷
EnemyViewImage->SetImage(EnemyViewBrush);
// 颜色为透明绿
EnemyViewImage->SetColorAndOpacity(FLinearColor(0.3f, 1.f, 0.32f, 0.4f));
}
void SSlAiMiniMapWidget::UpdateMapData(const FRotator PlayerRotator, const float MiniMapSize, const TArray<FVector2D>* EnemyPosList, const TArray<bool>* EnemyLockList, const TArray<float>* EnemyRotateList)
{
// 地图尺寸
MapSize = MiniMapSize;
// 清空现在的敌人列表
EnemyPos.Empty();
// 清空敌人是否锁定列表
EnemyLock.Empty();
// 比例
float DPIRatio = 280.f / MapSize;
// 保存视野旋转信息
TArray<float> EnemyViewRotate;
// 保存视野位置信息
TArray<FVector2D> EnemyViewPos;
// 保存视野锁定信息
TArray<bool> EnemyViewLock;
// 获取敌人信息
for (int i = 0; i < (*EnemyPosList).Num(); ++i) {
// 计算实际长度
float RealDistance = (*EnemyPosList)[i].Size();
// 如果长度小于地图实际半径
if (RealDistance * 2 < MapSize) {
// 屏幕位置
EnemyPos.Add((*EnemyPosList)[i] * DPIRatio + FVector2D(160.f, 160.f));
// 是否锁定玩家
EnemyLock.Add((*EnemyLockList)[i]);
}
// 如果长度小于地图实际半径再加上 2000,就渲染到视野
if (RealDistance * 2 < MapSize + 2000.f) {
// 屏幕位置
EnemyViewPos.Add((*EnemyPosList)[i] * DPIRatio + FVector2D(160.f, 160.f));
// 是否锁定玩家
EnemyViewLock.Add((*EnemyLockList)[i]);
// 添加旋转信息,格式化为 0-1
float RotVal = -(*EnemyRotateList)[i];
if (RotVal > 180.f) RotVal -= 360.f;
if (RotVal < -180.f) RotVal += 360.f;
// 序列化到 0-360
RotVal += 180.f;
// 序列化 0-1
RotVal /= 360.f;
// 转个 180 度
RotVal = RotVal + 0.5f > 1.f ? RotVal - 0.5f : RotVal + 0.5f;
// 添加进数组
EnemyViewRotate.Add(RotVal);
}
}
int ViewCount = 0;
// 修改敌人视野缩放比例
EnemyViewMatDynamic->SetScalarParameterValue(FName("Scale"), 1000.f / MapSize);
for (int i = 0; i < EnemyViewPos.Num(); ++i, ++ViewCount) {
FString PosName = FString("Position_") + FString::FromInt(i + 1);
FString AngleName = FString("Angle_") + FString::FromInt(i + 1);
// 如果没锁定玩家就渲染
if (!EnemyViewLock[i]) {
EnemyViewMatDynamic->SetVectorParameterValue(FName(*PosName), FLinearColor((EnemyViewPos[i].X - 20.f) / 280.f, (EnemyViewPos[i].Y - 20.f) / 280.f, 0.f, 0.f));
EnemyViewMatDynamic->SetScalarParameterValue(FName(*AngleName), EnemyViewRotate[i]);
}
else
{
EnemyViewMatDynamic->SetVectorParameterValue(FName(*PosName), FLinearColor(0.f, 0.f, 0.f, 0.f));
EnemyViewMatDynamic->SetScalarParameterValue(FName(*AngleName), 0.f);
}
}
// 把剩下的视野都不渲染
for (ViewCount += 1; ViewCount < 11; ++ViewCount) {
FString PosName = FString("Position_") + FString::FromInt(ViewCount);
FString AngleName = FString("Angle_") + FString::FromInt(ViewCount);
EnemyViewMatDynamic->SetVectorParameterValue(FName(*PosName), FLinearColor(0.f, 0.f, 0.f, 0.f));
EnemyViewMatDynamic->SetScalarParameterValue(FName(*AngleName), 0.f);
}
}
int32 SSlAiMiniMapWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
// 渲染敌人位置
for (int i = 0; i < EnemyPos.Num(); ++i) {
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId + 10,
AllottedGeometry.ToPaintGeometry(EnemyPos[i] - FVector2D(5.f, 5.f), FVector2D(10.f, 10.f)),
&GameStyle->PawnPointBrush,
ESlateDrawEffect::None,
EnemyLock[i] ? FLinearColor(1.f, 0.f, 0.f, 1.f) : FLinearColor(0.f, 1.f, 0.f, 1.f)
);
}
return LayerId;
}
编译后在场景放置 10 个敌人(因为代码限制所以不能超过十个,如果小地图上显示超出十个视野范围,其他的视野范围就不显示)。运行游戏,可以看到代表敌人的绿点出现在小地图上,并且有一个扇形的视野范围;如果敌人发现了玩家,它的绿点会变成红点,并且扇形视野范围消失。
同样,笔者在测试的时候,敌人的绿点需要把渲染层级调成 LayerId + 12 才能看到。而且敌人多了之后发现他们都重叠起来了,读者可以把 EnemyProfile 里对 Enemy 的碰撞响应改为 Block。
可能有细心的读者发现了,运行一段时间后游戏会自动崩溃 = = 💧,经过一位老哥指点后发现是因为动态材质被 UE4 自带的 GC(垃圾回收)机制给处理掉了,导致访问到空指针。如果遇到这个情况的读者可以去了解下 UE4 的 GC 相关知识点。由于笔者水平欠佳,还请读者自行查找相关文章。
这里就仅贴出修改方法:利用 AddToRoot() 将动态材质添加到根列表,这样它就不会被 GC;并且在销毁时调用 RemoveFromRoot() 让其回归到销毁列表。
SSlAIMiniMapWidget.h
public:
// 声明析构函数
~SSlAiMiniMapWidget();
private:
// 小地图视野材质
class UMaterialInstanceDynamic* MiniMapMatDynamic;
// 敌人视野材质
UMaterialInstanceDynamic* EnemyViewMatDynamic;
SSlAIMiniMapWidget.cpp
void SSlAiMiniMapWidget::RegisterMiniMap(class UTextureRenderTarget2D* MiniMapRender)
{
// ... 省略
MiniMapMatDynamic = UMaterialInstanceDynamic::Create(MiniMapMatInst, nullptr);
// 添加到根列表
MiniMapMatDynamic->AddToRoot();
// ... 省略
EnemyViewMatDynamic = UMaterialInstanceDynamic::Create(EnemyViewMatInst, nullptr);
// 添加到根列表
EnemyViewMatDynamic->AddToRoot();
// ... 省略
}
SSlAiMiniMapWidget::~SSlAiMiniMapWidget()
{
MiniMapMatDynamic->RemoveFromRoot();
MiniMapMatDynamic = nullptr;
EnemyViewMatDynamic->RemoveFromRoot();
EnemyViewMatDynamic = nullptr;
}
修改后,运行游戏,一段时间后也没有崩溃,说明 bug 修好了。
58. 聊天信息显示
在 Public/UI/Widget 目录下新建一个 C++ 的 Slate Widget 类,取名 SlAiChatShowWidget,作为游戏界面左下角的聊天栏界面。
来到聊天栏界面类,获取样式,填写结构这些操作就不多解释了。需要一提的是聊天栏结构只是一个垂直框,一条条消息会在聊天栏中从底向上排列。所以声明一个垂直框的指针。
添加两个单条消息结构体(头文件顶部先声明这个结构体,具体定义在 .cpp 中)类型的数组,分为已激活消息和未激活消息,激活消息会显示在聊天栏内,未激活消息则不显示。
添加一个初始化未激活消息的方法;然后再添加一个 AddMessage() 方法用于给外部调用,添加内容到单条消息然后激活消息,让其显示在聊天栏中。
重写聊天框界面的 Tick() 方法,在里面编写每一条消息的逐渐消失逻辑。
SSlAiChatShowWidget.h
struct ChatShowItem;// 保存每一条信息的结构体
class SVerticalBox; // 提前声明类
class SLAICOURSE_API SSlAiChatShowWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SSlAiChatShowWidget)
{}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
// 重写 Tick 函数
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
// 添加信息
void AddMessage(FText NewName, FText NewContent);
private:
// 初始化 Item(老师的单词拼写错了,这里改过来)
void InitializeItem();
private:
// 获取 GameStyle
const struct FSlAiGameStyle* GameStyle;
TSharedPtr<SVerticalBox> ChatBox;
// 已经激活的序列
TArray<TSharedPtr<ChatShowItem>> ActiveList;
// 未激活的序列
TArray<TSharedPtr<ChatShowItem>> UnActiveList;
}
在 .cpp 里定义一个结构体,作为单条聊天信息显示在聊天栏内。并且在结构体的构造函数内编写其结构布局,再添加一个注入内容的激活方法和一个调节透明度到慢慢消失的失活方法(即显示一段时间后逐渐隐藏)。
SSlAiChatShowWidget.cpp
// 引入所有要用的头文件
#include "SlAiStyle.h"
#include "SlAiGameWidgetStyle.h"
#include "SBox.h"
#include "SOverlay.h"
#include "SBoxPanel.h"
#include "SBorder.h"
#include "STextBlock.h"
#include "SMultiLineEditableText.h"
struct ChatShowItem
{
// 透明值
float Alpha;
// 水平组件
TSharedPtr<SHorizontalBox> CSBox;
// 名字
TSharedPtr<STextBlock> CSName;
// 内容框
TSharedPtr<SBorder> CSBorder;
// 内容
TSharedPtr<SMultiLineEditableText> CSContent;
// 构造函数
ChatShowItem(const FSlateBrush* EmptyBrush, const FSlateFontInfo FontInfo)
{
Alpha = 0.f;
// 实例化组件
SAssignNew(CSBox, SHorizontalBox)
+SHorizontalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.AutoWidth()
[
SAssignNew(CSName, STextBlock)
.Font(FontInfo)
.ColorAndOpacity(FSlateColor(FLinearColor(0.f, 1.f, 0.f, 1.f)))
]
+ SHorizontalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.FillWidth(1.f)
[
SAssignNew(CSBorder, SBorder)
.BorderImage(EmptyBrush)
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SAssignNew(CSContent, SMultiLineEditableText)
.WrappingPolicy(ETextWrappingPolicy::AllowPerCharacterWrapping)
.AutoWrapText(true)
.Font(FontInfo)
]
];
}
// 激活组件
TSharedPtr<SHorizontalBox> ActiveItem(FText NewName, FText NewContent)
{
CSName->SetText(NewName);
CSContent->SetText(NewContent);
Alpha = 1.f;
CSName->SetColorAndOpacity(FSlateColor(FLinearColor(0.f, 1.f, 0.f, Alpha)));
CSBorder->SetColorAndOpacity(FLinearColor(1.f, 1.f, 1.f, Alpha));
return CSBox;
}
// 逐渐消失,返回是否已经未激活
bool DeltaDisappear(float DeltaTime)
{
Alpha = FMath::Clamp<float>(Alpha - DeltaTime * 0.05f, 0.f, 1.f);
CSName->SetColorAndOpacity(FSlateColor(FLinearColor(0.f, 1.f, 0.f, Alpha)));
CSBorder->SetColorAndOpacity(FLinearColor(1.f, 1.f, 1.f, Alpha));
if (Alpha == 0.f) {
return true;
}
return false;
}
};
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiChatShowWidget::Construct(const FArguments& InArgs)
{
// 获取 GameStyle
GameStyle = &SlAiStyle::Get().GetWidgetStyle<FSlAiGameStyle>("BPSlAiGameStyle");
ChildSlot
[
SNew(SBox)
.WidthOverride(500.f)
.HeightOverride(600.f)
.HAlign(HAlign_Fill)
.VAlign(VAlign_Bottom)
[
SAssignNew(ChatBox, SVerticalBox)
]
];
InitializeItem();
}
void SSlAiChatShowWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
// 临时序列
TArray<TSharedPtr<ChatShowItem>> TempList;
// 循环遍历已经激活的信息
for (TArray<TSharedPtr<ChatShowItem>>::TIterator It(ActiveList); It; ++It) {
// 如果已经完全隐藏了
if ((*It)->DeltaDisappear(InDeltaTime)) {
// 从列表中删除这个信息
ChatBox->RemoveSlot((*It)->CSBox->AsShared());
// 将这个信息从添加到临时序列
TempList.Push(*It);
}
}
// 更新激活和未激活列表
for (int i = 0; i < TempList.Num(); ++i) {
ActiveList.Remove(TempList[i]);
UnActiveList.Push(TempList[i]);
}
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiChatShowWidget::AddMessage(FText NewName, FText NewContent)
{
TSharedPtr<ChatShowItem> InsertItem;
// 如果未激活列表不为空
if (UnActiveList.Num() > 0) {
// 从未激活列表中提取出一个信息
InsertItem = UnActiveList[0];
UnActiveList.RemoveAt(0);
}
else {
// 将激活列表的最前面一个提取出来
InsertItem = ActiveList[0];
ActiveList.RemoveAt(0);
// 移出 UI
ChatBox->RemoveSlot(InsertItem->CSBox->AsShared());
}
// 将这个信息激活并且添加到 UI
ChatBox->AddSlot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.FillHeight(1.f)
[
InsertItem->ActiveItem(NewName, NewContent)->AsShared()
];
// 将信息插入激活序列
ActiveList.Push(InsertItem);
}
void SSlAiChatShowWidget::InitializeItem()
{
for (int i = 0; i < 10; ++i) {
UnActiveList.Add(MakeShareable(new ChatShowItem(&GameStyle->EmptyBrush, GameStyle->Font_16)));
}
}
在游玩主界面添加聊天栏界面。
由于我们现在编写的项目是单机,所以聊天室用处不大,所以这里我们重写 Tick() 方法,把添加聊天消息的逻辑写在里面。并且声明一个 float 变量便于间隔一段时间来发送一条消息。
SSlAiGameHUDWidget.h
public:
// 重写 Tick 方法
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
public:
TSharedPtr<class SSlAiMiniMapWidget> MiniMapWidget;
// 聊天显示栏引用
TSharedPtr<class SSlAiChatShowWidget> ChatShowWidget;
private:
// 消息计时器
float MessageTimeCount;
SSlAiGameHUDWidget.cpp
// 添加头文件
#include "SSlAiChatShowWidget.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiGameHUDWidget::Construct(const FArguments& InArgs)
{
UIScaler.Bind(this, &SSlAiGameHUDWidget::GetUIScaler);
ChildSlot
[
// ... 省略
+SOverlay::Slot()
.HAlign(HAlign_Right)
.VAlign(VAlign_Top)
[
SAssignNew(MiniMapWidget, SSlAiMiniMapWidget)
]
// 聊天显示栏
+SOverlay::Slot()
.HAlign(HAlign_Left)
.VAlign(VAlign_Bottom)
.Padding(FMargin(20.f, 0.f, 0.f, 15.f))
[
SAssignNew(ChatShowWidget, SSlAiChatShowWidget)
]
// ... 省略
];
InitUIMap();
}
void SSlAiGameHUDWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
// 每五秒插入一条信息
if (MessageTimeCount < 5.f) {
MessageTimeCount += InDeltaTime;
}
else {
ChatShowWidget->AddMessage(NSLOCTEXT("SlAiGame", "Enemy", "Enemy"), NSLOCTEXT("SlAiGame", "EnemyDialogue", ": Fight with me !"));
MessageTimeCount = 0.f;
}
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiGameHUDWidget::InitUIMap()
{
// 消息计时器初始设置为 0
MessageTimeCount = 0.f;
}
运行游戏,左下角会出现 “与我一战” 的消息,并且随着时间推移,发出的消息会逐渐消失。
59. 聊天室与敌人血条修正
完善聊天室界面
到样式类添加聊天室的背景笔刷。
SlAiGameWidgetStyle.h
UPROPERTY(EditAnywhere, Category = "PlayerState")
FSlateBrush PlayerHeadBrush;
// 聊天室背景图片
UPROPERTY(EditAnywhere, Category = "ChatRoom")
FSlateBrush ChatRoomBGBrush;
来到聊天室界面,获取样式后,编写界面结构。聊天室界面除了消息显示面板(一个滚动框控件)以外,还有消息输入框和发送按钮,后两者都是要绑定相应方法来响应发送消息这个操作的。
既然要显示消息,那也要声明一个与聊天框界面那里一样的消息结构体,区别在于它没有逐渐消失的逻辑。
添加一个添加消息到消息显示面板的方法,并且声明一个单条消息结构体的指针数组,用于存储消息面板上的消息。
添加一个委托来于发送信息;然后声明一个提交事件和按钮事件,它们两个分别对应直接按回车键提交和点击 “发送” 按钮提交。并且它们在方法内都会给发送的消息添加玩家的前缀,然后调用前面的委托。
最后是添加一个滑动到滚动框的最底部的方法,毕竟消息是从下往上出现的,那么每次发送完消息,滚动框就要滑动到底部来显示最新消息。
SSlAiChatRoomWidget.h
struct ChatMessItem; // 消息结构体
DECLARE_DELEGATE_TwoParams(FPushMessage, FText, FText)
class SLAICOURSE_API SSlAiChatRoomWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SSlAiChatRoomWidget)
{}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
// 提交事件
void SubmitEvent(const FText& SubmitText, ETextCommit::Type CommitType);
// 按钮事件
FReply SendEvent();
// 添加信息
void AddMessage(FText NewName, FText NewContent);
// 滑动到最底下
void ScrollToEnd();
public:
FPushMessage PushMessage;
private:
// 获取 GameStyle
const struct FSlAiGameStyle *GameStyle;
// 滚动框
TSharedPtr<class SScrollBox> ScrollBox;
// 保存输入框
TSharedPtr<class SEditableTextBox> EditTextBox;
// 保存数组
TArray<TSharedPtr<ChatMessItem>> MessageList;
}
SSlAiChatRoomWidget.cpp
// 删除之前添加的头文件,添加新头文件
#include "SlAiStyle.h"
#include "SlAiGameWidgetStyle.h"
#include "SBox.h"
#include "SOverlay.h"
#include "SBoxPanel.h"
#include "SBorder.h"
#include "STextBlock.h"
#include "SMultiLineEditableText.h"
#include "SScrollBox.h"
#include "SEditableTextBox.h"
#include "SButton.h"
struct ChatMessItem
{
// 水平组件
TSharedPtr<SHorizontalBox> CSBox;
// 名字
TSharedPtr<STextBlock> CSName;
// 内容框
TSharedPtr<SBorder> CSBorder;
// 内容
TSharedPtr<SMultiLineEditableText> CSContent;
// 构造
ChatMessItem(const FSlateBrush* EmptyBrush, const FSlateFontInfo FontInfo)
{
// 实例化组件
SAssignNew(CSBox, SHorizontalBox)
+SHorizontalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.AutoWidth()
[
SAssignNew(CSName, STextBlock)
.Font(FontInfo)
.ColorAndOpacity(TAttribute<FSlateColor>(FSlateColor(FLinearColor(0.f, 1.f, 0.f, 1.f))))
]
+SHorizontalBox::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.FillWidth(1.f)
[
SAssignNew(CSBorder, SBorder)
.BorderImage(EmptyBrush)
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SAssignNew(CSContent, SMultiLineEditableText)
.WrappingPolicy(ETextWrappingPolicy::AllowPerCharacterWrapping)
.AutoWrapText(true)
.Font(FontInfo)
]
];
}
// 激活组件
TSharedPtr<SHorizontalBox> ActiveItem(FText NewName, FText NewContent)
{
CSName->SetText(NewName);
CSContent->SetText(NewContent);
return CSBox;
}
};
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiChatRoomWidget::Construct(const FArguments& InArgs)
{
//获取GameStyle
GameStyle = &SlAiStyle::Get().GetWidgetStyle<FSlAiGameStyle>("BPSlAiGameStyle");
ChildSlot
[
// 去掉之前测试用的结构
SNew(SBox)
.WidthOverride(600.f)
.HeightOverride(1080.f)
[
SNew(SBorder)
.BorderImage(&GameStyle->ChatRoomBGBrush)
[
SNew(SOverlay)
+SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Bottom)
.Padding(FMargin(0.f, 0.f, 0.f, 80.f))
[
SAssignNew(ScrollBox, SScrollBox)
]
+SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(FMargin(0.f, 1000.f, 0.f, 0.f))
[
SNew(SOverlay)
+SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(FMargin(0.f, 0.f, 120.f, 0.f))
[
SAssignNew(EditTextBox, SEditableTextBox)
.Font(GameStyle->Font_30)
.OnTextCommitted(this, &SSlAiChatRoomWidget::SubmitEvent)
]
+SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding(FMargin(480.f, 0.f, 0.f, 0.f))
[
SNew(SButton)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.OnClicked(this, &SSlAiChatRoomWidget::SendEvent)
[
SNew(STextBlock)
.Font(GameStyle->Font_30)
.Text(NSLOCTEXT("SlAiGame", "Send", "Send"))
]
]
]
]
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiChatRoomWidget::SubmitEvent(const FText& SubmitText, ETextCommit::Type CommitType)
{
FString MessageStr = EditTextBox->GetText().ToString();
if (MessageStr.IsEmpty()) return ;
MessageStr = FString(": ") + MessageStr;
AddMessage(NSLOCTEXT("SlAiGame", "Player", "Player"), FText::FromString(MessageStr));
EditTextBox->SetText(FText::FromString(""));
PushMessage.ExecuteIfBound(NSLOCTEXT("SlAiGame", "Player", "Player"), FText::FromString(MessageStr));
}
FReply SSlAiChatRoomWidget::SendEvent()
{
FString MessageStr = EditTextBox->GetText().ToString();
if (MessageStr.IsEmpty()) return FReply::Handled();
MessageStr = FString(": ") + MessageStr;
AddMessage(NSLOCTEXT("SlAiGame", "Player", "Player"), FText::FromString(MessageStr));
EditTextBox->SetText(FText::FromString(""));
PushMessage.ExecuteIfBound(NSLOCTEXT("SlAiGame", "Player", "Player"), FText::FromString(MessageStr));
return FReply::Handled();
}
void SSlAiChatRoomWidget::AddMessage(FText NewName, FText NewContent)
{
TSharedPtr<ChatMessItem> InsertItem;
if (MessageList.Num() < 30) {
// 新建一个控件
InsertItem = MakeShareable(new ChatMessItem(&GameStyle->EmptyBrush, GameStyle->Font_30));
MessageList.Add(InsertItem);
ScrollBox->AddSlot()
[
InsertItem->ActiveItem(NewName, NewContent)->AsShared()
];
}
else {
// 从序列里面提取
InsertItem = MessageList[0];
// 出队列
MessageList.Remove(InsertItem);
ScrollBox->RemoveSlot(InsertItem->CSBox->AsShared());
// 入队列
ScrollBox->AddSlot()
[
InsertItem->ActiveItem(NewName, NewContent)->AsShared()
];
MessageList.Push(InsertItem);
}
// 设置滑动到最底下
ScrollBox->ScrollToEnd();
}
void SSlAiChatRoomWidget::ScrollToEnd()
{
ScrollBox->ScrollToEnd();
}
来到游戏主界面绑定委托,并添加测试用的消息到聊天室。
打开聊天室时,自动滚动到滑动框的最底部来查看最新消息。
SSlAiGameHUDWidget.cpp
void SSlAiGameHUDWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
if (MessageTimeCount < 5.f) {
MessageTimeCount += InDeltaTime;
}
else {
ChatShowWidget->AddMessage(NSLOCTEXT("SlAiGame", "Enemy", "Enemy"), NSLOCTEXT("SlAiGame", "EnemyDialogue", ": Fight with me !"));
// 添加消息到聊天室
ChatRoomWidget->AddMessage(NSLOCTEXT("SlAiGame", "Enemy", "Enemy"), NSLOCTEXT("SlAiGame", "EnemyDialogue", ": Fight with me !"));
MessageTimeCount = 0.f;
}
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiGameHUDWidget::ShowGameUI(EGameUIType::Type PreUI, EGameUIType::Type NextUI)
{
if (PreUI == EGameUIType::Game) {
BlackShade->SetVisibility(EVisibility::Visible);
}
else {
UIMap.Find(PreUI)->Get()->SetVisibility(EVisibility::Hidden);
}
if (NextUI == EGameUIType::Game) {
BlackShade->SetVisibility(EVisibility::Hidden);
}
else {
UIMap.Find(NextUI)->Get()->SetVisibility(EVisibility::Visible);
// 显示现在状态对应的 UI
if (NextUI == EGameUIType::ChatRoom) ChatRoomWidget->ScrollToEnd();
}
}
void SSlAiGameHUDWidget::InitUIMap()
{
// 绑定委托
ChatRoomWidget->PushMessage.BindRaw(ChatShowWidget.Get(), &SSlAiChatShowWidget::AddMessage);
MessageTimeCount = 0.f;
}
编译后,在游玩样式蓝图作如下更改,调整聊天室背景色:
运行后,按 T 键打开聊天室,可见发出的消息,玩家也能发送消息;退到游戏界面也能看到自己刚刚发送的消息。
让敌人血条朝向摄像机
来到玩家角色类,添加一个获取摄像机位置的方法。
SlAiPlayerCharacter.h
public:
// 获取摄像机位置
FVector GetCameraPos();
SlAiPlayerCharacter.cpp
FVector ASlAiPlayerCharacter::GetCameraPos()
{
switch(GameView)
{
case EGameViewMode::First:
return FirstCamera->K2_GetComponentLocation();
case EGameViewMode::Third:
return ThirdCamera->K2_GetComponentLocation();
}
return FirstCamera->K2_GetComponentLocation();
}
来到玩家控制器,让血条朝向摄像机的位置。
SlAiEnemyController.cpp
void ASlAiEnemyController::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 如果玩家指针和角色指针存在,一直修改血条朝向玩家摄像机
if (SECharacter && SPCharacter) SECharacter->UpdateHPBarRotation(SPCharacter->GetCameraPos());
}
运行游戏,现在敌人的血条会朝向玩家的摄像机,但是从上往下看还是会变成一张纸的厚度。