UE4开发C++沙盒游戏教程笔记(十八)(对应教程 55 ~ 57)
54. 敌人死亡 AI
完善敌人的死亡逻辑
给敌人的动画类添加一个停止所有动画的方法,毕竟敌人不可能挂了还在播放攻击动画。
SlAiEnemyAnim.h
public:
// 停止所有动画
void StopAllAction();
SlAiEnemyAnim.cpp
void USlAiEnemyAnim::StopAllAction()
{
// 停止全部动画
Montage_Stop(0);
}
给敌人的 AI 控制器添加死亡后需要执行的方法,包括停止行为树和停止更新敌人一些数据的计时器。
SlAiEnemyController.h
public:
// 死亡
void EnemyDead();
SlAiEnemyController.cpp
void ASlAiEnemyController::EnemyDead()
{
// 停止行为树
if (BehaviorComp) BehaviorComp->StopTree(EBTStopMode::Safe);
// 临时代码,注销时间函数
if (EPDisHandle.IsValid()) GetWorld()->GetTimerManager().ClearTimer(EPDisHandle);
}
给敌人类声明两个死亡动画指针。这里不用蒙太奇动画是因为它播放完会立刻进入下一个动作,而普通的动画资源可以一直停留在动作末尾,即敌人倒下的时候。
添加一个死亡时间委托和用于绑定的函数,让敌人播放完动画后过一阵才销毁敌人。
在项目设计中敌人也属于资源的一种,并且敌人被击败后也会掉落物品,所以我们先给敌人类添加一个资源 ID;然后再添加一个生成掉落物的方法。
既然敌人也是资源,那视角对着敌人的时候,上方名称栏也应该显示敌人的名称,所以添加一个获取物品名字信息的方法。
SlAiEnemyCharacter.h
class UAnimationAsset; // 声明类
UCLASS()
class SLAICOURSE_API ASlAiEnemyCharacter : public ACharacter
{
public:
// 销毁函数
void DestroyEvent();
// 获取物品信息
FText GetInfoText() const;
public:
// 资源 ID
int ResourceIndex;
protected:
// 生成掉落物函数
void CreateFlobObject();
private:
// 死亡动画资源
UAnimationAsset* AnimDead_I;
UAnimationAsset* AnimDead_II;
// 死亡时间委托
FTimerHandle DeadHandle;
}
在敌人接收伤害的方法里添加执行死亡的相关方法的逻辑。
SlAiEnemyCharacter.cpp
// 引入头文件
#include "SlAiDataHandle.h"
#include "SlAiFlobObject.h"
ASlAiEnemyCharacter::ASlAiEnemyCharacter()
{
// 加载死亡动画资源
AnimDead_I = Cast<UAnimationAsset>(StaticLoadObject(UAnimationAsset::StaticClass(), NULL,
*FString("AnimSequence'/Game/Res/PolygonAdventure/Mannequin/Enemy/Animation/FightGroup/Enemy_Dead_I.Enemy_Dead_I'")));
AnimDead_II = Cast<UAnimationAsset>(StaticLoadObject(UAnimationAsset::StaticClass(), NULL,
*FString("AnimSequence'/Game/Res/PolygonAdventure/Mannequin/Enemy/Animation/FightGroup/Enemy_Dead_II.Enemy_Dead_II'")));
}
void ASlAiEnemyCharacter::BeginPlay()
{
// 设置资源 ID 是 3
ResourceIndex = 3;
}
void ASlAiEnemyCharacter::CreateFlobObject()
{
TSharedPtr<ResourceAttribute> ResourceAttr = *SlAiDataHandle::Get()->ResourceAttrMap.Find(ResourceIndex);
// 遍历生成
for (TArray<TArray<int>>::TIterator It(ResourceAttr->FlobObjectInfo); It; ++It) {
// 随机六
FRandomStream Stream;
// 产生新的随机种子
Stream.GenerateNewSeed();
// 生成数量
int Num = Stream.RandRange((*It)[1], (*It)[2]);
if (GetWorld()) {
for (int i = 0; i < Num; ++i) {
// 生成掉落资源
ASlAiFlobObject* FlobObject = GetWorld()->SpawnActor<ASlAiFlobObject>(GetActorLocation() +
FVector(0.f, 0.f, 40.f), FRotator::ZeroRotator);
FlobObject->CreateFlobObject((*It)[0]);
}
}
}
}
void ASlAiEnemyCharacter::AcceptDamage(int DamageVal)
{
// 从判断生命值变为 0 开始补充
// 如果生命值小于 0 并且角色的死亡计时器委托还没有开始
if (HP == 0.f && !DeadHandle.IsValid()) {
// 告诉控制器死亡
SEController->EnemyDead();
// 停止所有动画
SEAnim->StopAllAction();
float DeadDuration = 0.f;
FRandomStream Stream;
Stream.GenerateNewSeed();
int SelectIndex = Stream.RandRange(0, 1);
if (SelectIndex == 0) {
GetMesh()->PlayAnimation(AnimDead_I, false);
DeadDuration = AnimDead_I->GetMaxCurrentTime() * 2;
}
else {
GetMesh()->PlayAnimation(AnimDead_II, false);
DeadDuration = AnimDead_II->GetMaxCurrentTime() * 2;
}
// 生成掉落物
CreateFlobObject();
// 添加事件委托,销毁自身
FTimerDelegate TimerDelegate = FTimerDelegate::CreateUObject(this, &ASlAiEnemyCharacter::DestoryEvent);
GetWorld()->GetTimerManager().SetTimer(DeadHandle, TimerDelegate, DeadDuration, false);
}
else {
if (SEController) SEController->UpdateDamageRatio(HP / 200.f);
}
}
void ASlAiEnemyCharacter::DestroyEvent()
{
// 注销时间函数
if (DeadHandle.IsValid()) GetWorld()->GetTimerManager().ClearTimer(DeadHandle);
// 销毁自己
GetWorld()->DestroyActor(this);
}
FText ASlAiEnemyCharacter::GetInfoText() const
{
TSharedPtr<ResourceAttribute> ResourceAttr = *SlAiDataHandle::Get()->ResourceAttrMap.Find(ResourceIndex);
switch (SlAiDataHandle::Get()->CurrentCulture)
{
case ECultureTeam::EN:
return ResourceAttr->EN;
break;
case ECultureTeam::ZH:
return ResourceAttr->ZH;
break;
}
return ResourceAttr->ZH;
}
到玩家控制器类的射线检测方法里,添加对敌人获取名称的判断。同时也去到 StateMachine() 方法添加对准敌人时准心变化的判断。
SlAiPlayerController.cpp
// 引入头文件
#include "SlAiEnemyCharacter.h"
void ASlAiPlayerController::RunRayCast()
{
if (Cast<ASlAiPickupObject>(RayActor)) {
IsDetected = true;
SPState->RayInfoText = Cast<ASlAiPickupObject>(RayActor)->GetInfoText();
}
if (Cast<ASlAiResourceObject>(RayActor)) {
IsDetected = true;
SPState->RayInfoText = Cast<ASlAiResourceObject>(RayActor)->GetInfoText();
}
// 添加对敌人的获取名称逻辑
if (Cast<ASlAiEnemyCharacter>(RayActor)) {
IsDetected = true;
SPState->RayInfoText = Cast<ASlAiEnemyCharacter>(RayActor)->GetInfoText();
}
if (!IsDetected) {
SPState->RayInfoText = FText();
}
}
void ASlAiPlayerController::StateMachine()
{
ChangePreUpperType(EUpperBody::None);
// 在判断条件中添加对敌人的判断
if (!Cast<ASlAiResourceObject>(RayActor) && !Cast<ASlAiPickupObject>(RayActor) && !Cast<ASlAiEnemyCharacter>(RayActor)) {
UpdatePointer.ExecuteIfBound(false, 1.f);
}
// 如果检测到敌人
if (Cast<ASlAiEnemyCharacter>(RayActor)) {
// 准星锁定模式
UpdatePointer.ExecuteIfBound(false, 0.f);
}
if (Cast<ASlAiResourceObject>(RayActor)) {
// ... 省略
}
}
老师在运行的时候游戏崩溃,决定调整一下代码。
首先是防御状态的定时器在敌人死亡之后依旧在运行,所以来把这个定时器设定成不循环。
SlAiEnemyTaskDefence.cpp
EBTNodeResult::Type USlAiEnemyTaskDefence::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
// 这里改成 false,不循环
SEController->GetWorld()->GetTimerManager().SetTimer(TimerHandle, TimerDelegate, 2.f, false);
return EBTNodeResult::Succeeded;
}
其次是在敌人 AI 控制器里将敌人停止防御状态的两行代码移到开头,这样的话敌人一定会在 2 秒后结束防御状态。
SlAiEnemyController.cpp
void ASlAiEnemyController::FinishStateDefence()
{
// 这两行代码放到这里
ResetProcess(true);
SECharacter->StopDefence();
float SEToSP = FVector::Distance(SECharacter->GetActorLocation(), GetPlayerLocation());
if (SPCharacter->IsAttack && SEToSP < 200.f) {
}
else {
// 把两行代码移位
if (HPRatio < 0.2f) {
BlackboardComp->SetValueAsEnum("EnemyState", (uint8)EEnemyAIState::ES_Escape);
}
else {
BlackboardComp->SetValueAsEnum("EnemyState", (uint8)EEnemyAIState::ES_Attack);
}
}
}
添加敌人对玩家的伤害
给 PlayerState 添加一个接受伤害的方法和一个用于判断玩家是否死亡的 bool 值。
SlAiPlayerState.h
public:
// 接受伤害
void AcceptDamage(int DamageVal);
private:
// 是否已经死亡
bool IsDead;
SlAiPlayerState.cpp
ASlAiPlayerState::ASlAiPlayerState()
{
// 初始化是否死亡
IsDead = false;
}
void ASlAiPlayerState::Tick(float DeltaSeconds)
{
// 如果生命值等于 0 但是没死
if (HP == 0.f && !IsDead) {
IsDead = true;
}
}
void ASlAiPlayerState::AcceptDamage(int DamageVal)
{
HP = FMath::Clamp<float>(HP - DamageVal, 0.f, 500.f);
UpdateStateWidget.ExecuteIfBound(HP / 500.f, Hunger / 500.f);
// 如果生命值等于 0 但是没死
if (HP == 0 && !IsDead) {
IsDead = true;
}
}
在玩家角色类添加一个接受伤害的方法,毕竟这个项目中,HP 的变量是放在 PlayerState 的,所以这里是角色挨了揍然后把受伤数值传给 PlayerState。
SlAiPlayerCharacter.h
public:
// 接受伤害
void AcceptDamage(int DamageVal);
SlAiPlayerCharacter.cpp
void ASlAiPlayerCharacter::AcceptDamage(int DamageVal)
{
if (SPController->SPState) SPController->SPState->AcceptDamage(DamageVal);
}
来到敌人武器类,重写敌人武器的碰撞方法。
注意,在笔者的 4.26.2 版本这里重写碰撞交互检测的方法时,编译会显示说这个类的父类的同名方法不可以添加 UFUNCTION()
反射宏,如果读者遇到了就去父类 SlAiEnemyTool.h 里把同名函数的这个反射宏去掉,这里就不贴出来了。
SlAiEnemyWeapon.h
protected:
// 重写碰撞交互检测的方法
UFUNCTION()
virtual void OnOverlayBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) override;
SlAiEnemyWeapon.cpp
// 引入头文件
#include "SlAiPlayerCharacter.h"
void ASlAiEnemyWeapon::OnOverlayBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (Cast<ASlAiPlayerCharacter>(OtherActor)) {
// 为了方便直接把敌人武器造成的伤害写死为 20
Cast<ASlAiPlayerCharacter>(OtherActor)->AcceptDamage(20);
}
}
当然,敌人武器的碰撞检测不能时刻开着,只允许在敌人攻击动画播放时候开启。
在敌人类里添加武器开关碰撞检测的方法。
SlAiEnemyCharacter.h
public:
// 修改手持物品的碰撞检测是否开启
void ChangeWeaponDetect(bool IsOpen);
SlAiEnemyCharacter.cpp
void ASlAiEnemyCharacter::ChangeWeaponDetect(bool IsOpen)
{
// 如果手持物品存在,修改检测
ASlAiEnemyTool* WeaponClass = Cast<ASlAiEnemyTool>(WeaponSocket->GetChildActor());
if (WeaponClass) WeaponClass->ChangeOverlayDetect(IsOpen);
}
在敌人动画类添加一个可供蓝图调用的开关检测方法,让攻击动画的 Notify 来调用。
SlAiEnemyAnim.h
public:
// 开启和关闭交互动作时的碰撞检测
UFUNCTION(BlueprintCallable, Category = "EnemyAnim")
void ChangeDetection(bool IsOpen);
SlAiEnemyAnim.cpp
void USlAiEnemyAnim::ChangeDetection(bool IsOpen)
{
if (SECharacter) SECharacter->ChangeWeaponDetect(IsOpen);
}
编译后到敌人的动画蓝图作如下更改:
运行游戏,此时鼠标对着敌人会显示它的名字、鼠标准心也会变绿;敌人被打死后会播放死亡动画并掉落物品,玩家受击也会正确地减少生命值。
55. 渲染小地图
在 Material 目录下创建名为 MiniMapMat 的材质,作如下修改:(上面的材质节点要转换为变量并更改成这个名称)
然后在同目录创建其材质实例 MiniMapMatInst,勾选 MiniMapTex 的变量。
在 Public/UI/Widget 目录下创建 C++ 的 Slate Widget 类,取名 SlAiMiniMapWidget。作为小地图的界面。
在 Public/Common 目录下创建 C++ 的 Scene Capture 2D 类,取名 SlAiSceneCapture2D。作为一个摄像机,其固定在角色上方往下拍摄,所得到的画面会作为 2D 图像渲染在小地图界面中。
在游玩主界面添加小地图界面。
SSlAiGameHUDWidget.h
public:
TSharedPtr<class SSlAiPlayerStateWidget> PlayerStateWidget;
// 小地图引用
TSharedPtr<class SSlAiMiniMapWidget> MiniMapWidget;
SSlAiGameHUDWidget.cpp
// 引入头文件
#include "SSlAiMiniMapWidget.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiGameHUDWidget::Construct(const FArguments& InArgs)
{
+SOverlay::Slot()
.HAlign(HAlign_Left)
.VAlign(VAlign_Top)
[
SAssignNew(PlayerStateWidget, SSlAiPlayerStateWidget)
]
// 小地图
+SOverlay::Slot()
.HAlign(HAlign_Right)
.VAlign(VAlign_Top)
[
SAssignNew(MiniMapWidget, SSlAiMiniMapWidget)
]
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
样式类给小地图的背景添加一个笔刷。
SlAiGameStyle.h
UPROPERTY(EditAnywhere, Category = "Info")
FSlateBrush PointerBrush;
// 添加小地图笔刷
UPROPERTY(EditAnywhere, Category = "MiniMap")
FSlateBrush MiniMapBGBrush;
来到场景摄像机类,先给它声明构造函数,用于初始化一些设置。
然后声明一个返回 Texture 散图资源的方法,用于将摄像机捕获到的画面作为 2D 画面,并且返回 Texture。
摄像机并不是绑定在玩家身上的,所以我们声明一个变换摄像机位置的方法。后面会通过委托来调用这个方法,实现持续更新摄像机的位置。
SlAiSceneCapture2D.h
class UTextureRenderTarget2D; // 声明类
UCLASS()
class SLAICOURSE_API ASlAiSceneCapture2D : public ASceneCapture2D
{
GENERATED_BODY()
public:
ASlAiSceneCapture2D();
// 获取 MiniMapTex
UTextureRenderTarget2D* GetMiniMapTex();
// 更新变换
void UpdateTransform(FVector NormLocation, FRotator NormRotator);
private:
UTextureRenderTarget2D* MiniMapTex;
};
SlAiSceneCapture2D.cpp
// 引入头文件
#include "Components/SceneCaptureComponent2D.h"
#include "Engine/TextureRenderTarget2D.h"
ASlAiSceneCapture2D::ASlAiSceneCapture2D()
{
// 设置每帧更新
GetCaptureComponent2D()->bCaptureEveryFrame = true;
// 设置渲染图片的格式是 xxx,不然渲染出来的图片会有马赛克
GetCaptureComponent2D()->CaptureSource = ESceneCaptureSource::SCS_SceneColorSceneDepth;
// 设置视野模式为正交模式
GetCaptureComponent2D()->ProjectionType = ECameraProjectionMode::Orthographic;
// 设置初始化正交视野范围为 3000
GetCaptureComponent2D()->OrthoWidth = 3000.f;
// 设置摄像机旋转,头朝下,以后只改变后两个参数
SetActorRotation(FRotator(-90.f, 0.f, 0.f));
}
UTextureRenderTarget2D* ASlAiSceneCapture2D::GetMiniMapTex()
{
// 创建渲染贴图
MiniMapTex = NewObject<UTextureRenderTarget2D>();
// 最好是 4 的倍数
MiniMapTex->InitAutoFormat(256, 256);
// 绑定到渲染摄像机
GetCaptureComponent2D()->TextureTarget = MiniMapTex;
// 返回资源
return MiniMapTex;
}
void ASlAiSceneCapture2D::UpdateTransform(FVector NormLocation, FRotator NormRotator)
{
// 更新位置为玩家上方 1000
SetActorLocation(NormLocation + FVector(0.f, 0.f, 1000.f));
// 更新旋转为玩家旋转
SetActorRotation(FRotator(-90.f, NormRotator.Yaw, NormRotator.Roll));
}
来到小地图界面,依旧先是获取样式、添加界面结构。
给小地图声明一个用于放置地图图片的 SImage 指针。
声明一个注册方法,用于获取先前创建的材质实例,配上传入的 Texture 作为遮罩;再声明一个笔刷,与遮罩共同作用到小地图的 SImage 上。
SSlAiMiniMapWidget.h
class SImage; // 提前声明
class SLAICOURSE_API SSlAiMiniMapWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SSlAiMiniMapWidget)
{}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
// 委托接受 GameMode 传过来的 Texture 资源
void RegisterMiniMap(class UTextureRenderTarget2D* MiniMapRender);
private:
// 获取 GameStyle
const struct FSlAiGameStyle* GameStyle;
// 显示 MiniMap 的图片
TSharedPtr<SImage> MiniMapImage;
// 敌人视野材质(下下节课会用到)
class UMaterialInstanceDynamic* EnemyViewMatDynamic;
// 小地图笔刷
struct FSlateBrush* MiniMapBrush;
};
SSlAiMiniMapWidget.cpp
// 引入头文件
#include "SlAiStyle.h"
#include "SlAiGameWidgetStyle.h"
#include "SBox.h"
#include "SImage.h"
#include "SOverlay.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Engine/TextureRenderTarget2D.h"
#include "SlateBrush.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiMiniMapWidget::Construct(const FArguments& InArgs)
{
// 获取样式类
GameStyle = &SlAiStyle::Get().GetWidgetStyle<FSlAiGameStyle>("BPSlAiGameStyle");
ChildSlot
[
SNew(SBox)
.WidthOverride(320.f)
.HeightOverride(320.f)
[
SNew(SOverlay)
// MiniMap 背景图片
+SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SNew(SImage)
.Image(&GameStyle->MiniMapBGBrush)
]
// 渲染 MiniMap 的图片
+SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SAssignNew(MiniMapImage, SImage)
]
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiMiniMapWidget::RegisterMiniMap(class UTextureRenderTarget2D* MiniMapRender)
{
// 获取材质,这个材质是遮罩
UMaterialInterface* MiniMapMatInst = LoadObject<UMaterialInterface>(NULL, TEXT("MaterialInstanceConstant'/Game/Material/MiniMapMatInst.MiniMapMatInst'"));
// 创建动态材质
UMaterialInstanceDynamic* MiniMapMatDynamic = UMaterialInstanceDynamic::Create(MiniMapMatInst, nullptr);
// 绑定材质属性
MiniMapMatDynamic->SetTextureParameterValue(FName("MiniMapTex"), MiniMapRender);
// 实例化MiniMap笔刷
MiniMapBrush = new FSlateBrush();
// 设置属性
MiniMapBrush->ImageSize = FVector2D(280.f, 280.f);
MiniMapBrush->DrawAs = ESlateBrushDrawType::Image;
// 绑定材质资源文件
MiniMapBrush->SetResourceObject(MiniMapMatDynamic);
//将笔刷作为 MiniMapImage 的笔刷
MiniMapImage->SetImage(MiniMapBrush);
}
来到 GameMode 创建并初始化摄像机。其中要声明一个相机类型的指针、以及一个判断摄像机是否生成的 bool 值、一个初始化方法。初始化方法在 Tick() 里调用。
顺便在初始化方法里执行委托,传入摄像机类的 GetMiniMapTex() 返回的 Texture。并且如果摄像机已经创建,则利用 Tick() 来持续更新摄像机位置。
SlAiGameMode.h
DECLARE_DELEGATE(FInitPackageManager)
// 注册 MiniMap 的贴图和材质
DECLARE_DELEGATE_OneParam(FRegisterMiniMap, class UTextureRenderTarget2D*)
UCLASS()
class SLAICOURSE_API ASlAiGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
// 定义委托,绑定的方法是 MiniMapWidget 的 RegisterMiniMap
FRegisterMiniMap RegisterMiniMap;
protected:
// 初始化与更新小地图摄像机
void InitializeMiniMapCamera();
private:
// 是否已经生成小地图摄像机
bool IsCreateMiniMap;
// 小地图相机指针
class ASlAiSceneCapture2D* MiniMapCamera;
}
SlAiGameMode.cpp
// 引入头文件
#include "SlAiSceneCapture2D.h"
void ASlAiGameMode::Tick(float DeltaSeconds)
{
// 初始化与更新小地图摄像机
InitializeMiniMapCamera();
InitializePackage();
}
void ASlAiGameMode::InitializeMiniMapCamera()
{
// 如果摄像机还不存在并且世界已经存在
if (!IsCreateMiniMap && GetWorld()) {
// 生成小地图摄像机
MiniMapCamera = GetWorld()->SpawnActor<ASlAiSceneCapture2D>(ASlAiSceneCapture2D::StaticClass());
// 运行委托给 MiniMapWidget 的 RegisterMiniMap ()方法传递渲染的 MiniMapTex
RegisterMiniMap.ExecuteIfBound(MiniMapCamera->GetMiniMapTex());
// 设置已经生成小地图
IsCreateMiniMap = true;
}
// 如果小地图已经创建
if (IsCreateMiniMap) {
// 每帧更新小地图摄像机的位置和旋转
MiniMapCamera->UpdateTransform(SPCharacter->GetActorLocation(), SPCharacter->GetActorRotation());
}
}
最后让 HUD 绑定 GameMode 的委托到小地图界面的注册方法。
SlAiGameHUD.cpp
#include "SSlAiPackageWidget.h"
#include "SSlAiMiniMapWidget.h" // 引入头文件
void ASlAiGameHUD::BeginPlay()
{
// 绑定注册小地图贴图委托
GM->RegisterMiniMap.BindRaw(GameHUDWidget->MiniMapWidget.Get(), &SSlAiMiniMapWidget::RegisterMiniMap);
}
编译后打开游戏样式类的蓝图,添加小地图背景笔刷的图片引用:
运行游戏,可看见游戏界面右上角出现黑色圆环包裹的圆形小地图。并且小地图的内容会跟随玩家的位置和旋转变动。
56. 小地图玩家点与方位
增加地图缩放功能
添加两个 Action Mapping 用于缩小和放大地图。(老师设置的按键在位置上是左放大右缩小,不太方便,我这里就把按键位置对调了一下。)
AddMapSize -> O
ReduceMapSize -> I
给摄像机添加一个更改正交视距宽度的方法。
SlAiSceneCapture2D.h
public:
// 更新视野范围
void UpdateMiniMapWidth(int Delta);
SlAiSceneCapture2D.cpp
void ASlAiSceneCapture2D::UpdateMiniMapWidth(int Delta)
{
const float PreWidth = GetCaptureComponent2D()->OrthoWidth;
GetCaptureComponent2D()->OrthoWidth = FMath::Clamp<float>(PreWidth + Delta, 2000.f, 4000.f);
}
在数据类里添加小地图缩放的三种状态枚举。
SlAiTypes.h
// 小地图缩放状态
namespace EMiniMapSizeMode
{
enum Type
{
None,
Add,
Reduce
};
}
在玩家控制器里声明一个委托,传入更改的数值到摄像机的调视距方法。
添加 4 个用于放大缩小地图的方法和一个缩放状态枚举变量。
添加一个放在 Tick() 里的方法,配合上面的方法和变量来持续改变地图大小。
SlAiPlayerController.h
DECLARE_DELEGATE_TwoParams(FShowGameUI, EGameUIType::Type, EGameUIType::Type)
// 修改小地图视野范围委托
DECLARE_DELEGATE_OneParam(FUpdateMiniMapWidth, int)
UCLASS()
class SLAICOURSE_API ASlAiPlayerController : public APlayerController
{
GENERATED_BODY()
public:
// 小地图缩放事件
void AddMapSizeStart();
void AddMapSizeStop();
void ReduceMapSizeStart();
void ReduceMapSizeStop();
// 在 Tick 函数处理小地图事件
void TickMiniMap();
public:
// 修改小地图视野范围委托,注册函数是 SlAiSceneCapture2D 的 UpdateMiniMapWidth
FUpdateMiniMapWidth UpdateMiniMapWidth;
private:
// 小地图缩放状态
EMiniMapSizeMode::Type MiniMapSizeMode;
}
添加输入事件绑定,并且初始化缩放状态。
SlAiPlayerController.cpp
void ASlAiPlayerController::BeginPlay()
{
// 设置缩放状态为无
MiniMapSizeMode = EMiniMapSizeMode::None;
}
void ASlAiPlayerController::Tick(float DeltaSeconds)
{
// 处理小地图更新
TickMiniMap();
}
void ASlAiPlayerController::SetupInputComponent()
{
// 绑定缩放小地图事件
InputComponent->BindAction("AddMapSize", IE_Pressed, this, &ASlAiPlayerController::AddMapSizeStart);
InputComponent->BindAction("AddMapSize", IE_Released, this, &ASlAiPlayerController::AddMapSizeStop);
InputComponent->BindAction("ReduceMapSize", IE_Pressed, this, &ASlAiPlayerController::ReduceMapSizeStart);
InputComponent->BindAction("ReduceMapSize", IE_Released, this, &ASlAiPlayerController::ReduceMapSizeStop);
}
void ASlAiPlayerController::AddMapSizeStart()
{
// 如果操作被锁住,直接返回
if (SPCharacter->IsInputLocked) return;
// 设置缩放状态为增加
MiniMapSizeMode = EMiniMapSizeMode::Add;
}
void ASlAiPlayerController::AddMapSizeStop()
{
// 如果操作被锁住,直接返回
if (SPCharacter->IsInputLocked) return;
// 设置缩放状态为无
MiniMapSizeMode = EMiniMapSizeMode::None;
}
void ASlAiPlayerController::ReduceMapSizeStart()
{
// 如果操作被锁住,直接返回
if (SPCharacter->IsInputLocked) return;
// 设置缩放状态为减少
MiniMapSizeMode = EMiniMapSizeMode::Reduce;
}
void ASlAiPlayerController::ReduceMapSizeStop()
{
// 如果操作被锁住,直接返回
if (SPCharacter->IsInputLocked) return;
// 设置缩放状态为无
MiniMapSizeMode = EMiniMapSizeMode::None;
}
void ASlAiPlayerController::TickMiniMap()
{
switch (MiniMapSizeMode)
{
case EMiniMapSizeMode::Add:
UpdateMiniMapWidth.ExecuteIfBound(5); // 实际上 5 有点太慢了,建议是 50
break;
case EMiniMapSizeMode::Reduce:
UpdateMiniMapWidth.ExecuteIfBound(-5);
break;
}
}
最后来到 GameMode 来绑定委托。
SlAiGameMode.cpp
void ASlAiGameMode::InitializeMiniMapCamera()
{
if (!IsCreateMiniMap && GetWorld()) {
MiniMapCamera = GetWorld()->SpawnActor<ASlAiSceneCapture2D>(ASlAiSceneCapture2D::StaticClass());
RegisterMiniMap.ExecuteIfBound(MiniMapCamera->GetMiniMapTex());
// 绑定修改小地图视野的委托
SPController->UpdateMiniMapWidth.BindUObject(MiniMapCamera, &ASlAiSceneCapture2D::UpdateMiniMapWidth);
IsCreateMiniMap = true;
}
}
运行游戏,此时通过 I 键和 O 键可以缩小和放大地图。
显示小地图上的玩家 Icon
游戏样式类声明玩家 Icon 笔刷。
SlAiGameStyle.h
UPROPERTY(EditAnywhere, Category = "MiniMap")
FSlateBrush MiniMapBGBrush;
// 小地图的玩家 Icon
UPROPERTY(EditAnywhere, Category = "MiniMap")
FSlateBrush PawnPointBrush;
本地化头文件添加今后要用到的一些本地化文本注册。
SlAiInternation.h
#define LOCTEXT_NAMESPACE "SlAiGame"
SlAiInternation::Register(LOCTEXT("E", "E")); //东
SlAiInternation::Register(LOCTEXT("S", "S")); //南
SlAiInternation::Register(LOCTEXT("W", "W")); //西
SlAiInternation::Register(LOCTEXT("N", "N")); //北
SlAiInternation::Register(LOCTEXT("Player", "Player")); //玩家
SlAiInternation::Register(LOCTEXT("Enemy", "Enemy")); //敌人
SlAiInternation::Register(LOCTEXT("EnemyDialogue", ": Fight with me !")); //敌人的对话
SlAiInternation::Register(LOCTEXT("Send", "Send")); //发送
SlAiInternation::Register(LOCTEXT("GameOption", "GameOption")); //游戏设置
SlAiInternation::Register(LOCTEXT("SaveGame", "SaveGame")); //保存游戏
SlAiInternation::Register(LOCTEXT("SaveCompleted", "SaveCompleted")); //保存完毕
SlAiInternation::Register(LOCTEXT("QuitGame", "QuitGame")); //退出游戏
SlAiInternation::Register(LOCTEXT("GoBack", "GoBack")); //返回
#undef LOCTEXT_NAMESPACE
来到小地图类,添加一个方法用于更新小地图上的数据,传入的形参都是小地图上的动态对象要用到的参数。
重写绘制函数,小地图上的动态对象都是在绘制函数里面绘制的。
声明 4 个二维向量变量用于计算出东南西北的标记应处于小地图上的哪个位置。
SSlAiMiniMapWidget.h
public:
// 重写绘制函数
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
// 委托接受 GameMode 传过来的玩家旋转,绑定的委托是 GameMode 的 UpdateMapData
void UpdateMapData(const FRotator PlayerRotator, const float MiniMapSize, const TArray<FVector2D>* EnemyPosList, const TArray<bool>* EnemyLockList, const TArray<float>* EnemyRotateList);
private:
// 四个方向的渲染位置
FVector2D NorthLocation;
FVector2D SouthLocation;
FVector2D EastLocation;
FVector2D WestLocation;
SSlAiMiniMapWidget.cpp
void SSlAiMiniMapWidget::UpdateMapData(const FRotator PlayerRotator, const float MiniMapSize, const TArray<FVector2D>* EnemyPosList, const TArray<bool>* EnemyLockList, const TArray<float>* EnemyRotateList)
{
// 获取 Yaw,这个 Yaw 从 180 到 -180,我们把它变成负的,然后通过加角度来计算
float YawDir = -PlayerRotator.Yaw;
// 使用三角函数来计算
NorthLocation = FVector2D(FMath::Sin(FMath::DegreesToRadians(YawDir)), FMath::Cos(FMath::DegreesToRadians(YawDir))) * 150.f + FVector2D(160.f, 160.f);
EastLocation = FVector2D(FMath::Sin(FMath::DegreesToRadians(YawDir + 90.f)), FMath::Cos(FMath::DegreesToRadians(YawDir + 90.f))) * 150.f + FVector2D(160.f, 160.f);
SouthLocation = FVector2D(FMath::Sin(FMath::DegreesToRadians(YawDir + 180.f)), FMath::Cos(FMath::DegreesToRadians(YawDir + 180.f))) * 150.f + FVector2D(160.f, 160.f);
WestLocation = FVector2D(FMath::Sin(FMath::DegreesToRadians(YawDir + 270.f)), FMath::Cos(FMath::DegreesToRadians(YawDir + 270.f))) * 150.f + FVector2D(160.f, 160.f);
}
int32 SSlAiMiniMapWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
// 先调用一下父类函数
SCompoundWidget::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
// 渲染玩家图标
FSlateDrawElement::MakeBox(
OutDrawElements,
LayerId + 10,
AllottedGeometry.ToPaintGeometry(FVector2D(155.f, 155.f), FVector2D(10.f, 10.f)),
&GameStyle->PawnPointBrush,
ESlateDrawEffect::None,
FLinearColor(1.f, 1.f, 0.f, 1.f)
);
// 渲染东西南北文字
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId + 10,
AllottedGeometry.ToPaintGeometry(NorthLocation - FVector2D(8.f, 8.f), FVector2D(16.f, 16.f)),
NSLOCTEXT("SlAiGame", "N", "N"),
GameStyle->Font_16,
ESlateDrawEffect::None,
FLinearColor(1.f, 1.f, 1.f, 1.f)
);
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId + 10,
AllottedGeometry.ToPaintGeometry(SouthLocation - FVector2D(8.f, 8.f), FVector2D(16.f, 16.f)),
NSLOCTEXT("SlAiGame", "S", "S"),
GameStyle->Font_16,
ESlateDrawEffect::None,
FLinearColor(1.f, 1.f, 1.f, 1.f)
);
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId + 10,
AllottedGeometry.ToPaintGeometry(EastLocation - FVector2D(8.f, 8.f), FVector2D(16.f, 16.f)),
NSLOCTEXT("SlAiGame", "E", "E"),
GameStyle->Font_16,
ESlateDrawEffect::None,
FLinearColor(1.f, 1.f, 1.f, 1.f)
);
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId + 10,
AllottedGeometry.ToPaintGeometry(WestLocation - FVector2D(8.f, 8.f), FVector2D(16.f, 16.f)),
NSLOCTEXT("SlAiGame", "W", "W"),
GameStyle->Font_16,
ESlateDrawEffect::None,
FLinearColor(1.f, 1.f, 1.f, 1.f)
);
return LayerId;
}
在摄像机类添加一个获取小地图尺寸的方法,用在小地图的 UpdateMapData() 方法中当第二个实参。
SlAiSceneCapture2D.h
public:
// 获取小地图尺寸
float GetMapSize();
SlAiSceneCapture2D.cpp
float ASlAiSceneCapture2D::GetMapSize()
{
return GetCaptureComponent2D()->OrthoWidth;
}
在 GameMode 声明一个委托,对标的是小地图的 UpdateMapData() 方法。
SlAiGameMode.h
DECLARE_DELEGATE_OneParam(FRegisterMiniMap, class UTextureRenderTarget2D*)
// 更新 MiniMap 的数据
DECLARE_DELEGATE_FiveParams(FUpdateMapData, const FRotator, const float, const TArray<FVector2D>*, const TArray<bool>*, const TArray<float>*)
UCLASS()
class SLAICOURSE_API ASlAiGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
// 定义委托,用于更新小地图的方向文字位置,绑定的方法为 MiniMapWidget 的 UpdateMapData()
FUpdateMapData UpdateMapData;
}
SlAiGameMode.cpp
void ASlAiGameMode::InitializeMiniMapCamera()
{
if (IsCreateMiniMap) {
MiniMapCamera->UpdateTransform(SPCharacter->GetActorLocation(), SPCharacter->GetActorRotation());
// 声明一些变量,后续会填充内容
TArray<FVector2D> EnemyPosList;
TArray<bool> EnemyLockList;
TArray<float> EnemyRotateList;
// 每帧更新小地图的方向文字位置
UpdateMapData.ExecuteIfBound(SPCharacter->GetActorRotation(), MiniMapCamera->GetMapSize(), &EnemyPosList, &EnemyLockList, &EnemyRotateList);
}
}
最后在 HUD 绑定委托。
SlAiGameHUD.cpp
void ASlAiGameHUD::BeginPlay()
{
// 绑定更新小地图数据委托
GM->UpdateMapData.BindRaw(GameHUDWidget->MiniMapWidget.Get(), &SSlAiMiniMapWidget::UpdateMapData);
}
编译后在游戏样式蓝图里面配置小地图玩家 Icon 笔刷。
然后在本地化面板 Gather Text -> 编写 zh 翻译文本 -> 保存 -> Count Words -> Compile Text。完成新添加的本地化文本翻译。
运行游戏,此时可以在小地图看见东南西北的显示,并且小地图中间显示玩家的 Icon。
与背包界面遇到的问题类似,我将玩家 Icon 的层级设置成 LayerId + 12 才能在小地图上看到,如果有读者遇到了这个情况可以酌情提高显示层级。