UE4开发C++沙盒游戏教程笔记(九)(对应教程 28 ~ 30)
27. 手持物品与动作切换
让物品出现到角色手上
接下来实现切换手上物品为快捷栏内选中的物品。
给手上物品类添加一个通过 ID 返回物品 Actor 的工厂方法。
SlAiHandObject.h
public:
// 根据物品 ID 返回物品的工厂方法
// 这个 TSubclassOf<Type> 类型,其实就是 Type 这个类以及子类的所有类型。
// 所以这个方法可以返回任何 AActor 类或 AActor 的子类,一般在获取蓝图的时候使用。
static TSubclassOf<AActor> SpawnHandObject(int ObjectID);
SlAiHandObject.cpp
// 手持物品类的头文件
#include "SlAiHandNone.h"
#include "SlAiHandWood.h"
#include "SlAiHandStone.h"
#include "SlAiHandApple.h"
#include "SlAiHandMeat.h"
#include "SlAiHandAxe.h"
#include "SlAiHandHammer.h"
#include "SlAiHandSword.h"
TSubclassOf<AActor> ASlAiHandObject::SpawnHandObject(int ObjectID)
{
switch (ObjectID)
{
case 0:
return ASlAiHandNone::StaticClass();
case 1:
return ASlAiHandWood::StaticClass();
case 2:
return ASlAiHandStone::StaticClass();
case 3:
return ASlAiHandApple::StaticClass();
case 4:
return ASlAiHandMeat::StaticClass();
case 5:
return ASlAiHandAxe::StaticClass();
case 6:
return ASlAiHandHammer::StaticClass();
case 7:
return ASlAiHandSword::StaticClass();
}
return ASlAiHandNone::StaticClass();
}
随后让角色类应用这个工厂方法。
要考虑到的是,由于第一人称和第三人称的模型的手部位置不一样,所以要在切换视角的时候同步修改手持物品的位置,使其绑定到对应的模型的插槽上。
SlAiPlayerCharacter.h
public:
// 修改当前的手持物品
void ChangeHandObject(TSubclassOf<AActor> HandObjectClass);
SlAiPlayerCharacter.cpp
void ASlAiPlayerCharacter::BeginPlay()
{
Super::BeginPlay();
HandObject->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, FName("RHSocket"));
// 将之前注释掉的代码更改成如下
// 添加 Actor 到 HandObject
HandObject->SetChildActorClass(ASlAiHandObject::SpawnHandObject(0));
}
void ASlAiPlayerCharacter::ChangeView(EGameViewMode::Type NewGameView)
{
GameView = NewGameView;
switch (GameView)
{
case EGameViewMode::First:
FirstCamera->SetActive(true);
ThirdCamera->SetActive(false);
MeshFirst->SetOwnerNoSee(false);
GetMesh()->SetOwnerNoSee(true);
// 修改 HandObject 绑定的位置
HandObject->AttachToComponent(MeshFirst, FAttachmentTransformRules::SnapToTargetNotIncludingScale, FName("RHSocket"));
break;
case EGameViewMode::Third:
FirstCamera->SetActive(false);
ThirdCamera->SetActive(true);
MeshFirst->SetOwnerNoSee(true);
GetMesh()->SetOwnerNoSee(false);
// 修改 HandObject 绑定的位置
HandObject->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, FName("RHSocket"));
break;
}
}
void ASlAiPlayerCharacter::ChangeHandObject(TSubclassOf<AActor> HandObjectClass) // 此处有些多余代码,下一集会删,这里我就先删掉了
{
// 设置物品到 HandObject
HandObject->SetChildActorClass(HandObjectClass);
}
玩家控制器负责滚轮滑动来切换选中的快捷栏,所以它来负责调用角色类里面的切换手持物品的方法。
SlAiPlayerController.h
{
public:
// 对 Character 的手持物品进行更改,这个函数在 PlayerState 内会调用
void ChangeHandObject();
}
SlAiPlayerController.cpp
// 引入头文件
#include "SlAiHandObject.h"
void ASlAiPlayerController::ChangeHandObject()
{
// 生成手持物品
SPCharacter->ChangeHandObject(ASlAiHandObject::SpawnHandObject(SPState->GetCurrentHandObjectIndex()));
}
void ASlAiPlayerController::ScrollUpEvent()
{
if (!SPCharacter->IsAllowSwitch) return;
if (IsLeftButtonDown || IsRightButtonDown) return;
SPState->ChooseShortcut(true);
// 更改 Character 的手持物品
ChangeHandObject();
}
void ASlAiPlayerController::ScrollDownEvent()
{
if (!SPCharacter->IsAllowSwitch) return;
if (IsLeftButtonDown || IsRightButtonDown) return;
SPState->ChooseShortcut(false);
// 更改 Character 的手持物品
ChangeHandObject();
}
运行后可看到,此时物品已经可以正确地随着快捷栏格子的切换而出现在角色手中了。并且切换视角后也能看到物品能正确地被握持。
根据手上物品切换相应动作
在玩家状态类里添加一个方法,用于获取玩家手中物品的类型
SlAiPlayerState.h
public:
// 获取当前手持物品的物品类型
EObjectType::Type GetCurrentObjectType();
SlAiPlayerState.cpp
EObjectType::Type ASlAiPlayerState::GetCurrentObjectType()
{
TSharedPtr<ObjectAttribute> ObjectAttr;
ObjectAttr = *SlAiDataHandle::Get()->ObjectAttrMap.Find(GetCurrentHandObjectIndex());
return ObjectAttr->ObjectType;
}
玩家控制类根据手持物品的类型切换上半身预动作。
SlAiPlayerController.h
{
private:
// 修改预动作
void ChangePreUpperType(EUpperBody::Type RightType);
}
SlAiPlayerController.cpp
void ASlAiPlayerController::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
// 临时代码
ChangePreUpperType(EUpperBody::None);
}
void ASlAiPlayerController::ChangePreUpperType(EUpperBody::Type RightType = EUpperBody::None)
{
// 根据当前手持物品的类型来修改预动作
switch (SPState->GetCurrentObjectType())
{
case EObjectType::Normal:
LeftUpperType = EUpperBody::Punch;
RightUpperType = RightType;
break;
case EObjectType::Food:
LeftUpperType = EUpperBody::Punch;
// 如果右键状态是拾取,那就赋予优先级较高的拾取状态
RightUpperType = RightType == EUpperBody::None ? EUpperBody::Eat : RightType;
break;
case EObjectType::Tool:
LeftUpperType = EUpperBody::Hit;
RightUpperType = RightType;
break;
case EObjectType::Weapon:
LeftUpperType = EUpperBody::Fight;
RightUpperType = RightType;
break;
}
}
运行游戏,可以看到角色在持有相应物品的时候,按下左右键会出现对应的动作。
测试手中物品的碰撞交互检测
SlAiHandObject.cpp
// 添加头文件
#include "SlAiHelper.h"
ASlAiHandObject::ASlAiHandObject()
{
// 临时测试更改(同样因为版本不同而更改)
//AffectCollision->bGenerateOverlapEvents = true;
AffectCollision->SetGenerateOverlapEvents(true);
}
virtual void OnOverlayBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
SlAiHelper::Debug(FString("OnOverlayBegin"), 3.f);
}
virtual void OnOverlayEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
SlAiHelper::Debug(FString("OnOverlayEnd"), 3.f);
}
再到 GameMap 里任意一个资源(路边墙角柱子)勾选 “产生碰撞事件”。运行后,用剑碰到这个资源,可以看到左上角有输出。说明碰撞交互检测没有问题。但是我们还没砍上去,仅仅是模型碰到就触发了,显然是不合适的,下节课会改进。
28. 动作通知与读取资源 Json
通过动画的 Notify 开启和关闭碰撞检测
这集完善一下碰撞检测的开启时机,应该让剑在挥动的过程才能与场景内容交互。
在手持物品类添加一个根据传入参数决定是否开启碰撞检测的方法。
SlAiHandObject.h
public:
// 是否允许检测
void ChangeOverlayDetect(bool IsOpen);
SlAiHandObject.cpp
ASlAiHandObject::ASlAiHandObject()
{
// 修改上一集结尾的测试用代码,默认不开启
//AffectCollision->bGenerateOverlapEvents = false;
AffectCollision->SetGenerateOverlapEvents(false);
}
void ASlAiHandObject::ChangeOverlayDetect(bool IsOpen)
{
// 后面再有就直接用新版的方法了,再写出来影响观感
//AffectCollision->bGenerateOverlapEvents = IsOpen;
AffectCollision->SetGenerateOverlapEvents(IsOpen);
}
让角色类来调用这个开关检测的方法。
SlAiPlayerCharacter.h
public:
// 修改手持物品的碰撞检测是否开启
void ChangeHandObjectDetect(bool IsOpen);
SlAiPlayerCharacter.cpp
void ASlAiPlayerCharacter::ChangeHandObject(TSubclassOf<class AActor> HandObjectClass)
{
HandObject->SetChildActorClass(HandObjectClass);
}
void ASlAiPlayerCharacter::ChangeHandObjectDetect(bool IsOpen)
{
// 获取手上物品
ASlAiHandObject* HandObjectClass = Cast<ASlAiHandObject>(HandObject->GetChildActor());
if (HandObjectClass) HandObjectClass->ChangeOverlayDetect(IsOpen);
}
随后在动画类添加一个蓝图可调用的开关交互检测方法,给蒙太奇动画里面的 Notify(通知)调用。
SlAiPlayerAnim.h
public:
// 开启和关闭手上物品的交互检测
UFUNCTION(BlueprintCallable, Category = "SlAi")
void ChangeDetection(bool IsOpen);
SlAiPlayerAnim.cpp
void USlAiPlayerAnim::ChangeDetection(bool IsOpen)
{
if (!SPCharacter) return;
SPCharacter->ChangeHandObjectDetect(IsOpen);
}
老师准备好的动画文件已经放置好了 Notify,我们只需要在动画蓝图里面调用 Notify 节点就好了
第一人称的动画蓝图也作类似修改,这里就不截图了
运行后,再去碰可以响应碰撞交互的物品,不会再弹出信息;手持物品只有在挥动的时候,左上角才会出现碰撞响应的 Debug 信息。
加载可供采集的资源的 Json 数据
现在要将可交互的资源加载到场景中,而 Content/Res/ConfigData/ResourceAttribute.json 里存储了资源的数据。
在数据结构类定义 资源类型的枚举 和 资源属性的结构体。
SlAiTypes.h
// 资源类型
namespace EResourceType {
enum Type
{
Plant = 0,
Metal,
Animal
};
}
// 资源属性结构体
struct ResourceAttribute
{
FText EN; // 英文名
FText ZH; // 中文名
EResourceType::Type ResourceType;
int HP;
TArray<TArray<int>> FlobObjectInfo;
ResourceAttribute(const FText ENName, const FText ZHName, const EResourceType::Type RT, const int HPValue, TArray<TArray<int>>* FOI) {
EN = ENName;
ZH = ZHName;
ResourceType = RT;
HP = HPValue;
// 将数组元素迭代进本地数组
for (TArray<TArray<int>>::TIterator It(*FOI); It; ++It) {
TArray<int> FlobObjectInfoItem;
for (TArray<int>::TIterator Ih(*It); Ih; ++Ih) {
FlobObjectInfoItem.Add(*Ih);
}
FlobObjectInfo.Add(FlobObjectInfoItem);
}
}
// 临时代码,Debug 输出检测结果
FString ToString() {
FString InfoStr;
for (TArray<TArray<int>>::TIterator It(FlobObjectInfo); It; ++It) {
for (TArray<int>::TIterator Ih(*It); Ih; ++Ih) {
InfoStr += FString::FromInt(*Ih) + FString(".");
}
InfoStr += FString("__");
}
return EN.ToString() + FString("--") + ZH.ToString() + FString("--") + FString::FromInt((int)ResourceType) + FString("--") + FString::FromInt(HP) + FString("--") + InfoStr;
}
};
依旧是让 Json 数据处理类读取 Json 数据,依旧是硬编码的转换字符串类型到枚举类型 : )
SlAiJsonHandle.h
{
public:
// 解析资源属性函数
void ResourceAttrJsonRead(TMap<int, TSharedPtr<ResourceAttribute>>& ResourceAttrMap);
private:
// 定义一个从 FString 转换到 ResourceType 的方法
EResourceType::Type StringToResourceType(const FString ArgStr);
private:
// 资源属性文件名
FString ResourceAttrFileName;
};
SlAiJsonHandle.cpp
SlAiJsonHandle::SlAiJsonHandle()
{
// 添加资源属性 Json 文件的名字
ResourceAttrFileName = FString("ResourceAttribute.json");
}
void SlAiJsonHandle::ResourceAttrJsonRead(TMap<int, TSharedPtr<ResourceAttribute>>& ResourceAttrMap)
{
FString JsonValue;
LoadStringFromFile(ResourceAttrFileName, RelativePath, JsonValue);
TArray<TSharedPtr<FJsonValue>> JsonParsed;
TSharedRef<TJsonReader<TCHAR>> JsonReader = TJsonReaderFactory<TCHAR>::Create(JsonValue);
if (FJsonSerializer::Deserialize(JsonReader, JsonParsed)) {
for (int i = 0; i < JsonParsed.Num(); ++i) {
// 资源没有序号 0,从 1 开始
TArray<TSharedPtr<FJsonValue>> ResourceAttr = JsonParsed[i]->AsObject()->GetArrayField(FString::FromInt(i + 1));
FText EN = FText::FromString(ResourceAttr[0]->AsObject()->GetStringField("EN"));
FText ZH = FText::FromString(ResourceAttr[1]->AsObject()->GetStringField("ZH"));
EResourceType::Type ResourceType = StringToResourceType(ResourceAttr[2]->AsObject()->GetStringField("ResourceType"));
int HP = ResourceAttr[3]->AsObject()->GetIntegerField("HP");
TArray<TArray<int>> FlobObjectInfoArray;
TArray<TSharedPtr<FJsonValue>> FlobObjectInfo = ResourceAttr[4]->AsObject()->GetArrayField(FString("FlobObjectInfo"));
for (int j = 0; j < FlobObjectInfo.Num(); ++j) {
FString FlobObjectInfoItem = FlobObjectInfo[j]->AsObject()->GetStringField(FString::FromInt(j));
FString ObjectIndexStr;
FString RangeStr;
FString RangeMinStr;
FString RangeMaxStr;
FlobObjectInfoItem.Split(FString("_"), &ObjectIndexStr, &RangeStr);
RangeStr.Split(FString(","), &RangeMinStr, &RangeMaxStr);
TArray<int> FlobObjectInfoList;
FlobObjectInfoList.Add(FCString::Atoi(*ObjectIndexStr));
FlobObjectInfoList.Add(FCString::Atoi(*RangeMinStr));
FlobObjectInfoList.Add(FCString::Atoi(*RangeMaxStr));
FlobObjectInfoArray.Add(FlobObjectInfoList);
}
TSharedPtr<ResourceAttribute> ResourceAttrPtr = MakeShareable(new ResourceAttribute(EN, ZH, ResourceType, HP, &FlobObjectInfoArray));
ResourceAttrMap.Add(i + 1, ResourceAttrPtr);
}
}
else {
SlAiHelper::Debug(FString("Deserialize Failed"), 10.f);
}
}
EResourceType::Type SlAiJsonHandle::StringToResourceType(const FString ArgStr)
{
if (ArgStr.Equals(FString("Plant"))) return EResourceType::Plant;
if (ArgStr.Equals(FString("Metal"))) return EResourceType::Metal;
if (ArgStr.Equals(FString("Animal"))) return EResourceType::Animal;
return EResourceType::Plant;
}
Json 数据读取出来后依旧是放到数据控制类里用 TMap 进行存储。
SlAiDataHandle.h
class SLAICOURSE_API SlAiDataHandle
{
public:
// 资源属性图
TMap<int, TSharedPtr<ResourceAttribute>> ResourceAttrMap;
private:
// 初始化资源属性图
void InitResourceAttrMap();
};
SlAiDataHandle.cpp
void SlAiDataHandle::InitializeGameData()
{
// 初始化资源属性图
InitResourceAttrMap();
}
void SlAiDataHandle::InitResourceAttrMap()
{
SlAiSingleton<SlAiJsonHandle>::Get()->ResourceAttrJsonRead(ResourceAttrMap);
// 测试用,如果后面没有需要可以删除
for (TMap<int, TSharedPtr<ResourceAttribute>>::TIterator It(ResourceAttrMap); It; ++It) {
SlAiHelper::Debug((It->Value)->ToString(), 120.f);
}
}
运行后可见左上角输出了资源的数据。
接下来创建资源的 C++ 类。
新建一个 Actor,路径位于 /Public/Resource,取名为 SlAiResourceObject,作为资源的基类。
基于这个基类,在同路径创建 SlAiResourceRock、SlAiResourceTree 这两个资源类。
SlAiTypes.h 里面资源属性结构体的临时代码、上面数据控制类测试用的 Debug 代码也可以去掉了。本集课程结束。
29. 资源和可拾取物品
完善资源类
接上节课,继续完善资源类的逻辑。
创建一个物体通道 Resource,默认回应为 Block。
创建一个新的碰撞配置:
然后让 ToolProfile 预设对 Resource 的追踪类型改为 Overlap。
给资源基类添加识别 ID、根组件、网格体和一个存储网格体地址的数组。因为资源设定为拥有很多种形态,所以要添加这个数组来读取不同的网格体模型。然后修改一下网格体的碰撞预设为刚刚新建的预设。
SlAiResourceObject.h
class SLAICOURSE_API ASlAiResourceObject : public AActor
{
GENERATED_BODY()
public:
ASlAiResourceObject();
virtual void Tick(float DeltaTime) override;
public:
// 资源 ID
int ResourceIndex;
protected:
virtual void BeginPlay() override;
protected:
// 根组件
USceneComponent* RootScene;
// 静态模型
UStaticMeshComponent* BaseMesh;
// 保存资源模型的地址,用于随机刷不同形态的资源
TArray<FString> ResourcePath;
}
让网格体默认开启交互检测。
SlAiResourceObject.cpp
// 引入头文件
#include "Components/StaticMeshComponent.h"
ASlAiResourceObject::ASlAiResourceObject()
{
PrimaryActorTick.bCanEverTick = true;
// 实例化根节点
RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
RootComponent = RootScene;
// 实例化模型组件
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMesh"));
BaseMesh->SetupAttachment(RootComponent);
BaseMesh->SetCollisionProfileName(FName("ResourceProfile"));
// 开启交互检测
BaseMesh->SetGenerateOverlapEvents(true);
}
资源基类的两个子类就要额外添加一些生成不同形态资源的逻辑。
SlAiResourceTree.h
class SLAICOURSE_API ASlAiResourceTree : public ASlAiResourceObject
{
GENERATED_BODY()
public:
ASlAiResourceTree();
}
SlAiResourceTree.cpp
// 添加头文件
#include "Components/StaticMeshComponent.h"
#include "ConstructorHelpers.h"
ASlAiResourceTree::ASlAiResourceTree()
{
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_Tree_07.SM_Env_Tree_07'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_Tree_016.SM_Env_Tree_016'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_Tree_014_Snow.SM_Env_Tree_014_Snow'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_Tree_012.SM_Env_Tree_012'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_TreePine_01_Snow.SM_Env_TreePine_01_Snow'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_Tree_01_Snow.SM_Env_Tree_01_Snow'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_Tree_06_Snow.SM_Env_Tree_06_Snow'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_TreeDead_02_Snow.SM_Env_TreeDead_02_Snow'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_TreeDead_01.SM_Env_TreeDead_01'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_Tree_012_Snow.SM_Env_Tree_012_Snow'"));
FRandomStream Stream;
// 产生新的随机种子
Stream.GenerateNewSeed();
int RandIndex = Stream.RandRange(0, ResourcePath.Num() - 1);
// 给模型组件添加上模型,这里不能用静态变量
ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(*ResourcePath[RandIndex]);
// 绑定模型到 Mesh 组件
BaseMesh->SetStaticMesh(StaticBaseMesh.Object);
ResourceIndex = 1;
}
SlAiResourceRock.h
class SLAICOURSE_API ASlAiResourceRock : public ASlAiResourceObject
{
GENERATED_BODY()
public:
ASlAiResourceRock();
}
SlAiResourceRock.cpp
// 添加头文件
#include "Components/StaticMeshComponent.h"
#include "ConstructorHelpers.h"
ASlAiResourceRock::ASlAiResourceRock()
{
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_Rock_02.SM_Env_Rock_02'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_Rock_03.SM_Env_Rock_03'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_Rock_03_Snow.SM_Env_Rock_03_Snow'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_Rock_04.SM_Env_Rock_04'"));
ResourcePath.Add(FString("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_Rock_04_Snow.SM_Env_Rock_04_Snow'"));
FRandomStream Stream;
// 产生新的随机种子
Stream.GenerateNewSeed();
int RandIndex = Stream.RandRange(0, ResourcePath.Num() - 1);
// 给模型组件添加上模型,这里不能用静态变量
ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(*ResourcePath[RandIndex]);
// 绑定模型到 Mesh 组件
BaseMesh->SetStaticMesh(StaticBaseMesh.Object);
ResourceIndex = 2;
}
运行游戏,可以看到资源类拖到场景会自动随机一个外形,按道理它的网格体组件的碰撞配置也已经修改成对应的配置,但是笔者的 4.26 引擎因为网格体组件没有添加反射宏所以无法查看。
添加可拾取物品
创建一个物体通道 Pickup,默认回应为 Block。
新建一个预设:
新建一个 C++ 的 Actor 类,路径为 /Public/Pickup,取名为 SlAiPickupObject,作为可拾取物的基类。
再基于这个新建的类,新建 SlAiPickupWood、SlAiPickupStone 这两个类到同目录。
给可拾取物基类添加根组件、网格体以及识别 ID。然后修改一下网格体的碰撞预设为刚刚新建的预设。
SlAiPickupObject.h
class SLAICOURSE_API ASlAiPickupObject : public AActor
{
GENERATED_BODY()
public:
ASlAiPickupObject();
virtual void Tick(float DeltaTime) override;
public:
int ObjectIndex;
protected:
virtual void BeginPlay() override;
protected:
// 根组件
USceneComponent* RootScene;
// 静态模型
UStaticMeshComponent* BaseMesh;
}
SlAiPickupObject.cpp
// 添加头文件
#include "Components/StaticMeshComponent.h"
ASlAiPickupObject::ASlAiPickupObject()
{
PrimaryActorTick.bCanEverTick = true;
// 实例化根节点
RootScene = CreateDefaultSubobject<USceneComponent>("RootScene");
RootComponent = RootScene;
// 在这里实现模型组件但是不进行模型绑定
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMesh"));
BaseMesh->SetupAttachment(RootComponent);
// 设置模型碰撞为 PickupProfile
BaseMesh->SetCollisionProfileName(FName("PickupProfile"));
}
可拾取物的模型各自只有一种,就只需简单地赋给一个模型的地址就够了。
SlAiPickupStone.h
class SLAICOURSE_API ASlAiPickupStone : public ASlAiPickupObject
{
GENERATED_BODY()
public:
ASlAiPickupStone();
}
SlAiPickupStone.cpp
// 添加头文件
#include "Components/StaticMeshComponent.h"
#include "ConstructorHelpers.h"
ASlAiPickupStone::ASlAiPickupStone()
{
// 给模型组件添加上模型
static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Prop_StoneBlock_01.SM_Prop_StoneBlock_01'"));
// 绑定模型到 Mesh 组件
BaseMesh->SetStaticMesh(StaticBaseMesh.Object);
BaseMesh->SetRelativeScale3D(FVector(0.8f, 0.8f, 0.5f));
ObjectIndex = 2;
}
SlAiPickupWood.h
class SLAICOURSE_API ASlAiPickupWood : public ASlAiPickupObject
{
GENERATED_BODY()
public:
ASlAiPickupWood();
}
SlAiPickupWood.cpp
// 添加头文件
#include "Components/StaticMeshComponent.h"
#include "ConstructorHelpers.h"
ASlAiPickupWood::ASlAiPickupWood()
{
// 给模型组件添加上模型
static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Prop_Loghalf_01.SM_Prop_Loghalf_01'"));
// 绑定模型到 Mesh 组件
BaseMesh->SetStaticMesh(StaticBaseMesh.Object);
BaseMesh->SetRelativeScale3D(FVector(0.4f));
ObjectIndex = 1;
}
此时运行游戏,能够看到可拾取物的石头和木材被拖到场景后可以正确显示,它们的网格体组件的碰撞配置应该也是对的。
添加用于显示面前物体的介绍框界面
新建一个 C++ 的 SlateWidget 类,路径为 /Public/UI/Widget,取名为 SlAiRayInfoWidget,用于显示角色面前的射线追踪到的对象的信息。角色射线检测功能后续再添加。
给这个 Widget 准备新的背景笔刷。
SlAiGameStyle.h
USTRUCT()
struct SLAICOURSE_API FSlAiGameStyle : public FSlateWidgetStyle
{
UPROPERTY(EditAnywhere, Category = "Package")
FSlateBrush ObjectBrush_7;
// 射线检测信息面板背景
UPROPERTY(EditAnywhere, Category = "Info")
FSlateBrush RayInfoBrush;
}
把介绍框 Widget 添加到根界面
SSlAiGameHUDWidget.h
class SLAICOURSE_API SSlAiGameHUDWidget : public SCompoundWidget
{
public:
TSharedPtr<class SSlAiShortcutWidget> ShortcutWidget;
// 射线信息框
TSharedPtr<class SSlAiRayInfoWidget> RayInfoWidget;
};
SSlAiGameHUDWidget.cpp
// 引入头文件
#include "SSlAiRayInfoWidget.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiGameHUDWidget::Construct(const FArguments& InArgs)
{
UIScaler.Bind(this, &SSlAiGameHUDWidget::GetUIScaler);
ChildSlot
[
SNew(SDPIScaler)
.DPIScaler(UIScaler)
[
SNew(SOverlay)
// 快捷栏
+SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Bottom)
[
SAssignNew(ShortcutWidget, SSlAiShortcutWidget)
]
// 射线信息
+SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Top)
[
SAssignNew(RayInfoWidget, SSlAiRayInfoWidget)
]
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
随后完善介绍框 Widget 的布局与一些逻辑。重写 Tick 函数用于初始化;同时声明一个委托,用于让 PlayerState 进行绑定,到时候射线检测到的信息会先传给 PlayerState 然后再通过委托同步到介绍框 Widget。
SSlAiRayInfoWidget.h
class STextBlock; // 提前声明
// 声明委托
DECLARE_DELEGATE_OneParam(FRegisterRayInfoEvent, TSharedPtr<STextBlock>)
class SLAICOURSE_API SSlAiRayInfoWidget : public SCompoundWidget
{
public:
// 重写 Tick 方法
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
public:
FRegisterRayInfoEvent RegisterRayInfoEvent;
private:
// 保存显示射线信息的文本
TSharedPtr<STextBlock> RayInfoTextBlock;
// 获取 GameStyle
const struct FSlAiGameStyle* GameStyle;
// 是否已经初始化事件
bool IsInitRayInfoEvent;
};
SSlAiRayInfoWidget.cpp
// 引入头文件
#include "SlAiStyle.h"
#include "SlAiGameWidgetStyle.h"
#include "SBorder.h"
#include "SBox.h"
#include "STextBlock.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiRayInfoWidget::Construct(const FArguments& InArgs)
{
// 获取 GameStyle
GameStyle = &SlAiStyle::Get().GetWidgetStyle<FSlAiGameStyle>("BPSlAiGameStyle");
ChildSlot
[
SNew(SBox)
.WidthOverride(400.f)
.HeightOverride(100.f)
[
SNew(SBorder)
.BorderImage(&GameStyle->RayInfoBrush)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SAssignNew(RayInfoTextBlock, STextBlock)
.Font(GameStyle->Font_Outline_50)
.ColorAndOpacity(GameStyle->FontColor_Black)
]
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiRayInfoWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
// 第一帧的时候进行初始化
if (!IsInitRayInfoEvent)
{
RegisterRayInfoEvent.ExecuteIfBound(RayInfoTextBlock);
IsInitRayInfoEvent = true;
}
}
依旧是让 PlayerState 来保存一下面对的物体的数据。
SlAiPlayerState.h
public:
// 提供给 RayInfoWidget 的注册射线信息事件
void RegisterRayInfoEvent(TSharedPtr<STextBlock> RayInfoTextBlock);
private:
// 获取射线检测信息
FText GetRayInfoText() const;
private:
// 射线信息参数
TAttribute<FText> RayInfoTextAttr;
SlAiPlayerState.cpp
void ASlAiPlayerState::RegisterRayInfoEvent(TSharedPtr<STextBlock> RayInfoTextBlock)
{
RayInfoTextAttr.BindUObject(this, &ASlAiPlayerState::GetRayInfoText);
// 绑定射线检测信息
RayInfoTextBlock->SetText(RayInfoTextAttr);
}
FText ASlAiPlayerState::GetShortcutInfoText() const
{
// ... 省略
}
FText GetRayInfoText() const
{
// 临时代码作测试用
return FText::FromString("hahaha");
}
随后让 HUD 绑定一下介绍框的委托到 PlayerState 的方法。
SlAiGameHUD.cpp
#include "SSlAiShortcutWidget.h"
// 引入头文件
#include "SSlAiRayInfoWidget.h"
void ASlAiGameHUD::BeginPlay()
{
GameHUDWidget->ShortcutWidget->RegisterShortcutContainer.BindUObject(GM->SPState, &ASlAiPlayerState::RegisterShortcutContainer);
// 绑定注册射线信息文本事件
GameHUDWidget->RayInfoWidget->RegisterRayInfoEvent.BindUObject(GM->SPState, &ASlAiPlayerState::RegisterRayInfoEvent);
}
在游玩样式类中配置好笔刷。运行后可以看到游戏界面顶部出现信息展示 Widget,内部有 hahaha 的文本。