UE4开发C++沙盒游戏教程笔记(十)(对应教程 31 ~ 33)
30. 准星与 C++ 操控材质
在根目录创建一个文件夹 Material,在里面新建一个材质,取名 PointerMat,用来作为准心的材质。
在材质里面连接如下节点:(记得左下角要的材质配置也要改;最顶部的颜色随意选,只决定环形进度条填充的颜色;Range 节点是一个参数节点)
随后根据这个材质创建材质实例,取名为 PointerMatInst,可配合 C++ 代码修改其进度条填充程度。
创建一个 C++ 的 SlateWidget 类,取名为 SlAiPointerWidget,路径是 /Public/UI/Widget。
给准星准备笔刷。
SlAiGameStyle.h
USTRUCT()
struct SLAICOURSE_API FSlAiGameStyle : public FSlateWidgetStyle
{
UPROPERTY(EditAnywhere, Category = "Info")
FSlateBrush RayInfoBrush;
// 准星材质
UPROPERTY(EditAnywhere, Category = "Info")
FSlateBrush PointerBrush;
}
把准星 Widget 加入到根 Widget。
SSlAiGameHUDWidget.h
class SLAICOURSE_API SSlAiGameHUDWidget : public SCompoundWidget
{
public:
TSharedPtr<class SSlAiRayInfoWidget> RayInfoWidget;
// 准星
TSharedPtr<class SSlAiPointerWidget> PointerWidget;
};
SSlAiGameHUDWidget.cpp
// 引入头文件
#include "SSlAiPointerWidget.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiGameHUDWidget::Construct(const FArguments& InArgs)
{
ChildSlot
[
SNew(SDPIScaler)
.DPIScaler(UIScaler)
[
SNew(SOverlay)
// ... 省略
// 准星
+SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SAssignNew(PointerWidget, SSlAiPointerWidget)
]
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
完善准心 Widget 的逻辑,包括应用材质、界面布局、尺寸动态变化等。
SSlAiPointerWidget.h
class SLAICOURSE_API SSlAiPointerWidget : public SCompoundWidget
{
public:
// 重写 Tick
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
// 给 PlayerController 绑定的事件,修改准星是否锁定以及加载进度
void UpdatePointer(bool IsAim, float Range);
private:
// 给 SBox 大小变化绑定的函数
FOptionalSize GetBoxWidth() const; // 老师写成了 Widget,看读者改不改过来
FOptionalSize GetBoxHeight() const;
private:
// 获取 GameStyle
const struct FSlAiGameStyle* GameStyle;
TSharedPtr<class SBox> RootBox;
// 实时改变的大小,改变这个变量就可以改变准星大小
float CurrentSize;
// 获取材质实例
class UMaterialInstanceDynamic* PointerMaterial;
// 是否改变大小状态
bool IsAimed;
};
SSlAiPointerWidget.cpp
// 引入头文件
#include "SlAiStyle.h"
#include "SlAiGameWidgetStyle.h"
#include "SBox.h"
#include "SImage.h"
#include "Materials/MaterialInstanceDynamic.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SSlAiPointerWidget::Construct(const FArguments& InArgs)
{
// 获取 GameStyle
GameStyle = &SlAiStyle::Get().GetWidgetStyle<FSlAiGameStyle>("BPSlAiGameStyle");
// 初始化大小为 20
CurrentSize = 20.f;
IsAimed = false;
ChildSlot
[
SAssignNew(RootBox, SBox)
.WidthOverride(TAttribute<FOptionalSize>(this, &SSlAiPointerWidget::GetBoxWidth))
.HeightOverride(TAttribute<FOptionalSize>(this, &SSlAiPointerWidget::GetBoxHeight))
[
SNew(SImage)
.Image(&GameStyle->PointerBrush)
]
];
// 加载材质实例
static ConstructorHelpers::FObjectFinder<UMaterialInstance> StaticPointerMaterialInstance(TEXT("MaterialInstanceConstant'/Game/Material/PointerMatInst.PointerMatInst'"));
// 转换为动态材质实例
PointerMaterial = (UMaterialInstanceDynamic*)StaticPointerMaterialInstance.Object;
}
void SSlAiPointerWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
// 实时修改空间大小
CurrentSize = IsAimed ? FMath::FInterpTo(CurrentSize, 130.f, InDeltaTime, 10.f) : FMath::FInterpTo(CurrentSize, 20.f, InDeltaTime, 10.f);
}
void SSlAiPointerWidget::UpdatePointer(bool IsAim, float Range)
{
IsAimed = IsAim;
// 这里修改的是刚刚新建材质实例里面添加的参数,也就是进度条的填充百分比
PointerMaterial->SetScalarParameterValue(FName("Range"), Range);
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
FOptionalSize SSlAiPointerWidget::GetBoxWidth() const
{
return FOptionalSize(CurrentSize);
}
FOptionalSize SSlAiPointerWidget::GetBoxHeight() const
{
return FOptionalSize(CurrentSize);
}
控制器内声明一个委托,后面会给控制器类补充射线检测的功能,所以控制器来执行这个让准心 Widget 大小变化的方法。
SlAiPlayerController.h
// 修改准星委托
DECLARE_DELEGATE_TwoParams(FUpdatePointer, bool, float)
{
public:
// 实时修改准星的委托,注册的函数是 PointerWidget 的 UpdatePointer
FUpdatePointer UpdatePointer;
}
SlAiPlayerController.cpp
void ASlAiPlayerController::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
ChangePreUpperType(EUpperBody::None);
// 以下代码用于查看效果,测试结束后要删掉
static float TestPointer = 1;
TestPointer = FMath::FInterpTo(TestPointer, 0, DeltaSeconds, 1.f);
UpdatePointer.ExecuteIfBound(true, FMath::Clamp(TestPointer, 0.f, 1.f));
}
让 HUD 来绑定委托,实现控制器与准心 Widget 的数据交互。
SlAiGameHUD.cpp
#include "SSlAiRayInfoWidget.h"
// 引入头文件
#include "SSlAiPointerWidget.h"
void ASlAiGameHUD::BeginPlay()
{
GameHUDWidget->RayInfoWidget->RegisterRayInfoEvent.BindUObject(GM->SPState, &ASlAiPlayerState::RegisterRayInfoEvent);
// 绑定修改准星委托(BindRaw 是专门绑定的 C++ 的方法)
GM->SPController->UpdatePointer.BindRaw(GameHUDWidget->PointerWidget.Get(), &SSlAiPointerWidget::UpdatePointer);
}
在游玩样式类配置准星的笔刷。
运行后可见屏幕中心的环形进度条在逐渐填充,然而只能在第一次运行时能看到,除非你的启动模式是 Standalone。
31. 射线检测伐木挖矿捡东西
实现射线检测
在项目设置的碰撞设置里添加一个追踪通道,取名为 ViewTrace,默认回应选 Block。
然后修改一下 PlayerProfile 和 ToolProfile 对 ViewTrace 的追踪类型为 Ignore。
在游玩控制器类里面添加三个方法,用于实现射线检测功能。
SlAiPlayerController.h
{
private:
// 射线检测结果
FHitResult RayGetHitResult(FVector TraceStart, FVector TraceEnd);
// 射线绘制
void DrawRayLine(FVector StartPos, FVector EndPos, float Duration);
// 进行射线检测
void RunRayCast();
};
SlAiPlayerController.cpp
#include "SlAiPlayerState.h"
// 添加头文件
#include "Components/LineBatchComponent.h"
#include "Camera/CameraComponent.h"
void ASlAiPlayerController::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
ChangePreUpperType(EUpperBody::None);
// 进行射线检测
RunRayCast();
}
FHitResult ASlAiPlayerController::RayGetHitResult(FVector TraceStart, FVector TraceEnd)
{
// 在 4.26 版本里,这个结构体已经不能只传一个 bool 类型的变量进构造函数了
//FCollisionQueryParams TraceParams(true);
FCollisionQueryParams TraceParams(FName(TEXT("Trace")), true, GetPawn());
TraceParams.AddIgnoredActor(SPCharacter);
//TraceParams.bTraceAsyncScene = true; // 这个在 4.26 的引擎已经不存在了,笔者已经注释掉
TraceParams.bReturnPhysicalMaterial = false;
TraceParams.bTraceComplex = true;
FHitResult Hit(ForceInit);
// ECC_GameTraceChannel1 其实就是刚刚创建的追踪通道 ViewTrace
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECollisionChannel::ECC_GameTraceChannel1, TraceParams)) {
// 绘制射线
DrawRayLine(TraceStart, TraceEnd, 5.f);
}
return Hit;
}
void ASlAiPlayerController::DrawRayLine(FVector StartPos, FVector EndPos, float Duration)
{
ULineBatchComponent* const LineBatcher = GetWorld()->PersistentLineBatcher;
if (LineBatcher != nullptr) {
float LineDurationTime = (Duration > 0.f) ? Duration : LineBatcher->DefaultLifeTime;
LineBatcher->DrawLine(StartPos, EndPos, FLinearColor::Red, 10, 0.f, LineDuration);
}
}
void ASlAiPlayerController::RunRayCast()
{
FVector StartPos(0.f);
FVector EndPos(0.f);
switch (SPCharacter->GameView)
{
case EGameViewMode::First:
StartPos = SPCharacter->FirstCamera->K2_GetComponentLocation();
EndPos = StartPos + SPCharacter->FirstCamera->GetForwardVector() * 2000.f;
break;
case EGameViewMode::Third:
StartPos = SPCharacter->ThirdCamera->K2_GetComponentLocation();
StartPos = StartPos + SPCharacter->ThirdCamera->GetForwardVector() * 300.f;
EndPos = StartPos + SPCharacter->ThirdCamera->GetForwardVector() * 2000.f;
break;
}
FHitResult Hit = RayGetHitResult(StartPos, EndPos);
}
此时运行游戏,可看见第三人称和第一人称都能看到红色的射线,两个人称的射线都出发自当前摄像机。
完善采集和拾取功能
给可拾取物和资源添加一个获取物体名字的方法。再添加一个被拾取的方法。
SlAiPickupObject.h
#include "SlAiTypes.h" // 引入头文件
#include "SlAiPickupObject.generated.h"
class SLAICOURSE_API ASlAiPickupObject : public AActor
{
GENERATED_BODY()
public:
// 获取物体名字
FText GetInfoText() const;
// 被拾取,返回物品 ID
int TakePickup();
}
SlAiPickupObject.cpp
// 引入头文件
#include "SlAiDataHandle.h"
#include "Engine/GameEngine.h"
FText ASlAiPickupObject::GetInfoText() const
{
TSharedPtr<ObjectAttribute> ObjectAttr = *SlAiDataHandle::Get()->ObjectAttrMap.Find(ObjectIndex);
// 根据当前语言类型返回对应的名字
switch (SlAiDataHandle::Get()->CurrentCulture)
{
case ECultureTeam::EN:
return ObjectAttr->EN;
case ECultureTeam::ZH:
return ObjectAttr->ZH;
}
return ObjectAttr->ZH;
}
int ASlAiPickupObject::TakePickup()
{
BaseMesh->SetCollisionResponseToAllChannels(ECR_Ignore);
if (GetWorld()) GetWorld()->DestroyActor(this);
return ObjectIndex;
}
给资源类额外添加采集相关逻辑:资源的两个生命值变量;获取资源类型、血量的百分比、承受采集伤害的方法。
SlAiResourceObject.h
#include "SlAiTypes.h" // 引入头文件
#include "SlAiResourceObject.generated.h"
class SLAICOURSE_API ASlAiResourceObject : public AActor
{
public:
// 获取物体名字
FText GetInfoText() const;
// 获取资源类型
EResourceType::Type GetResourceType();
// 获取血量百分比
float GetHPRange();
// 获取伤害
ASlAiResourceObject* TakeObjectDamage(int Damage);
protected:
// 血量
int HP;
// 基础血量
int BaseHP;
};
SlAiResourceObject.cpp
// 引入头文件
#include "SlAiDataHandle.h"
#include "Engine/GameEngine.h"
void ASlAiResourceObject::BeginPlay()
{
Super::BeginPlay();
// 初始化资源的生命值
TSharedPtr<ResourceAttribute> ResourceAttr = *SlAiDataHandle::Get()->ResourceAttrMap.Find(ResourceIndex);
HP = BaseHP = ResourceAttr->HP;
}
FText ASlAiResourceObject::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;
}
EResourceType::Type ASlAiResourceObject::GetResourceType()
{
TSharedPtr<ResourceAttribute> ResourceAttr = *SlAiDataHandle::Get()->ResourceAttrMap.Find(ResourceIndex);
return ResourceAttr->ResourceType;
}
float ASlAiResourceObject::GetHPRange()
{
return FMath::Clamp<float>((float)HP / (float)BaseHP, 0.f, 1.f);
}
ASlAiResourceObject* ASlAiResourceObject::TakeObjectDamage(int Damage)
{
HP = FMath::Clamp<int>(HP - Damage, 0, BaseHP);
// 如果资源生命值小于等于 0
if (HP <= 0) {
// 让所有的碰撞检测失效
BaseMesh->SetCollisionResponseToAllChannels(ECR_Ignore);
// 销毁物体
GetWorld()->DestroyActor(this);
}
return this;
}
给 PlayerState 添加一个变量用于保存当前射线检测到的物体的名字,通过 GetRayInfoText() 方法返回这个变量。
添加获取手上物品攻击范围的方法。以及一个获取当前手上物品对当前检测到的资源的伤害值的方法。
SlAiPlayerState.h
public:
// 获取手上物品的攻击范围
int GetAffectRange();
// 获取伤害值
int GetDamageValue(EResourceType::Type ResourceType);
public:
// 当前射线检测到的物体的名字,由 PlayerController 进行更新
FText RayInfoText;
SlAiPlayerState.cpp
int ASlAiPlayerState::GetAffectRange()
{
TSharedPtr<ObjectAttribute> ObjectAttr;
ObjectAttr = *SlAiDataHandle::Get()->ObjectAttrMap.Find(GetCurrentHandObjectIndex());
// 获取当前手上物品的作用范围
return ObjectAttr->AffectRange;
}
// 根据正在采集的资源以及手持物品的类型来返回伤害
int ASlAiPlayerState::GetDamageValue(EResourceType::Type ResourceType)
{
TSharedPtr<ObjectAttribute> ObjectAttr;
ObjectAttr = *SlAiDataHandle::Get()->ObjectAttrMap.Find(GetCurrentHandObjectIndex());
switch (ResourceType)
{
case EResourceType::Plant:
return ObjectAttr->PlantAttack;
break;
case EResourceType::Metal:
return ObjectAttr->MetalAttack;
break;
case EResourceType::Animal:
return ObjectAttr->AnimalAttack;
break;
}
return ObjectAttr->PlantAttack;
}
FText ASlAiPlayerState::GetRayInfoText() const
{
// 替换掉临时代码
return RayInfoText;
}
前面做的都是准备工作,现在要在游玩控制类里用上这些变量和方法。
给游玩控制类添加一个 AActor 类型的指针,指向当前检测到的资源 Actor 对象。
添加一个方法,用于更改检测到资源和未检测到物体时上半身的预动作;以及对着资源按着左键进行采集的逻辑。
SlAiPlayerController.h
{
private:
// 行为状态机
void StateMachine();
private:
// 检测到的资源
AActor* RayActor;
};
SlAiPlayerController.cpp
// 引入资源和拾取物头文件
#include "SlAiPickupObject.h"
#include "SlAiResourceObject.h"
void ASlAiPlayerController::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
// 去掉临时代码,用下面的 StateMachine() 替代
RunRayCast();
// 处理动作状态
StateMachine();
}
FHitResult ASlAiPlayerController::RayGetHitResult(FVector TraceStart, FVector TraceEnd)
{
if (GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECollisionChannel::ECC_GameTraceChannel1, TraceParams)) {
// 这个绘制射线的代码如果用不到就注释掉
//DrawRayLine(TraceStart, TraceEnd, 5.f);
}
return Hit;
}
void ASlAiPlayerController::RunRayCast()
{
// ... 省略
// 添加一个 bool 类型的变量,用于判定是否检测到物品
bool IsDetected = false;
FHitResult Hit = RayGetHitResult(StartPos, EndPos);
RayActor = Hit.GetActor();
// 如果检测到的物体可以强制转换为 可拾取类,则调用获取名字的方法然后赋值给 PlayerState 的变量
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 (!IsDetected) {
SPState->RayInfoText = FText();
}
}
void ASlAiPlayerController::StateMachine()
{
// 普通模式
ChangePreUpperType(EUpperBody::None);
// 如果没检测到资源或可拾取物
if (!Cast<ASlAiResourceObject>(RayActor) && !Cast<ASlAiPickupObject>(RayActor)) {
// 准星显示未锁定
UpdatePointer.ExecuteIfBound(false, 1.f);
}
// 如果检测到资源
if (Cast<ASlAiResourceObject>(RayActor)) {
// 如果左键没有按下,在资源模式下右键没有特殊意义
if (!IsLeftButtonDown) {
// 准星锁定模式
UpdatePointer.ExecuteIfBound(false, 0.f);
}
// 如果左键已经按下并且资源在攻击范围以内
if (IsLeftButtonDown && FVector::Distance(RayActor->GetActorLocation(), SPCharacter->GetActorLocation()) < SPState->GetAffectRange()) {
// 获取实际伤害
int Damage = SPState->GetDamageValue(Cast<ASlAiResourceObject>(RayActor)->GetResourceType());
float Range = Cast<ASlAiResourceObject>(RayActor)->TakeObjectDamage(Damage)->GetHPRange();
// 更新准星
UpdatePointer.ExecuteIfBound(true, Range);
}
}
// 如果检测到可拾取物品,并且两者的距离小于 300
if (Cast<ASlAiPickupObject>(RayActor) && FVector::Distance(RayActor->GetActorLocation(), SPCharacter->GetActorLocation()) < 300.f)
{
// 改变右键预状态为拾取
ChangePreUpperType(EUpperBody::PickUp);
// 修改准星锁定模式
UpdatePointer.ExecuteIfBound(false, 0);
// 如果右键按下
if (IsRightButtonDown) {
// 把物品捡起来
Cast<ASlAiPickupObject>(RayActor)->TakePickup();
}
}
}
往场景里放资源和可拾取物,用于测试采集和拾取。
运行后,使用不同工具砍伐不同资源会有相应的速度,资源生命值到 0 会自动消失;而且可拾取物可被拾取。手上拿着食物的时候也是优先右键拾取。
32. 生成掉落物与动态创建材质
拾取时隐藏手持物品
上节课结尾的时候拾取动作有一些瑕疵——手上拿着物品的时候捡东西,物品依旧在手里。待会添加逻辑将它在拾取的时候隐藏掉。
拾取的蒙太奇动画里已经准备好了两个通知,让这俩通知调用隐藏手持物品的方法就好了。
先在角色类里添加隐藏手持物品的方法。
SlAiPlayerCharacter.h
public:
// 是否渲染手上物品,由 Anim 进行调用
void RenderHandObject(bool IsRender);
SlAiPlayerCharacter.cpp
void ASlAiPlayerCharacter::RenderHandObject(bool IsRender)
{
// 如果手上没有物品
if (!HandObject->GetChildActor()) return;
// 如果有物品
HandObject->GetChildActor()->SetActorHiddenInGame(!IsRender);
}
然后在动画类里添加一个可被蓝图调用的方法,用来调用角色的隐藏手持物品方法。
SlAiPlayerAnim.h
public:
// 开启和关闭手上物品的显示与否,在捡东西的时候调用
UFUNCTION(BlueprintCallable, Category = "PlayeAnim")
void RenderHandObject(bool IsRender);
SlAiPlayerAnim.cpp
void USlAiPlayerAnim::RenderHandObject(bool IsRender)
{
if (!SPCharacter) return;
SPCharacter->RenderHandObject(IsRender);
}
随后在第一人称和第三人称的动画蓝图绑定通知如下:
运行后,拾取物品时手上物品会消失,拾取动画播放完后重新显示。
此时如果前面的功能都没有问题的话,Temp 文件夹可以删掉了。
添加掉落物
新建一个碰撞物体通道 Flob,默认响应为 Block。
再新建一个碰撞预设:
将预设 PlayerProfile 对 Flob 的物体类型响应调成 Overlap;
ToolProfile 对 Flob 的响应调成 Ignore。
新建一个 C++ 的 Actor 类,路径为 /Public/Flob,取名为 SlAiFlobObject,作为掉落物。
在 Material 目录下创建一个材质,命名为 FlobIconMat,给掉落物用。
给材质赋上贴图,完成后效果如下:(记得贴图要转化为变量)
再以它为基础创建材质实例 FlobIconMatInst。点开后勾选 ObjectTex,然后点击 Save。
给掉落物类添加碰撞体盒子和网格体,网格体只用一个片面。此外还要添加几个方法和变量来给掉落物的网格体赋予材质。并且添加掉落的表现。
SlAiFlobObject.h
#include "SlAiTypes.h" // 引入头文件
#include "SlAiFlobObject.generated.h"
UCLASS()
class SLAICOURSE_API ASlAiFlobObject : public AActor
{
GENERATED_BODY()
public:
ASlAiFlobObject();
// Tick 函数放上来
virtual void Tick(float DeltaTime) override;
// 生成物品初始化
void CreateFlobObject(int ObjectID);
protected:
virtual void BeginPlay() override;
private:
// 渲染贴图
void RenderTexture();
private:
class UBoxComponent* BoxCollision;
class UStaticMeshComponent* BaseMesh;
// 物品 ID
int ObjectIndex;
class UTexture* ObjectIconTex;
class UMaterialInstanceDynamic* ObjectIconMatDynamic;
};
SlAiFlobObject.cpp
// 引入头文件
#include "ConstructorHelpers.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "SlAiDataHandle.h"
ASlAiFlobObject::ASlAiFlobObject()
{
PrimaryActorTick.bCanEverTick = true;
BoxCollision = CreateDefaultSubobject<UBoxComponent>(TEXT("BoxCollision"));
RootComponent = (USceneComponent*)BoxCollision;
// 设置碰撞属性
BoxCollision->SetCollisionProfileName(FName("FlobProfile"));
// 启动物体模拟
BoxCollision->SetSimulatePhysics(true);
// 锁定旋转
BoxCollision->SetConstraintMode(EDOFMode::Default);
BoxCollision->GetBodyInstance()->bLockXRotation = true;
BoxCollision->GetBodyInstance()->bLockYRotation = true;
BoxCollision->GetBodyInstance()->bLockZRotation = true;
// 设置大小
BoxCollision->SetBoxExtent(FVector(15.f));
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMesh"));
BaseMesh->SetupAttachment(RootComponent);
static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Engine/BasicShapes/Plane.Plane'"));
//BaseMesh->SetStaticMesh(StaticBaseMesh.Object);
BaseMesh->SetCollisionResponseToChannels(ECR_Ignore);
BaseMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// 设置变换
BaseMesh->SetRelativeRotation(FRotator(0.f, 0.f, 90.f));
BaseMesh->SetRelativeScale3D(FVector(0.3f));
UMaterialInterface* StaticObjectIconMat = LoadObject<UMaterialInterface>(NULL, TEXT("MaterialInstanceConstant'/Game/Material/FlobIconMatInst.FlobIconMatInst'"));
// 动态创建材质
ObjectIconMatDynamic = UMaterialInstanceDynamic::Create(StaticObjectIconMat, nullptr);
}
void ASlAiFlobObject::CreateFlobObject(int ObjectID)
{
// 指定 ID
ObjectIndex = ObjectID;
// 渲染贴图
RenderTexture();
// 做随机方向的力
FRandomStream Stream;
Stream.GenerateNewSeed();
int DirYaw = Stream.RandRange(-180, 180);
FRotator ForceRot = FRotator(0.f, DirYaw, 0.f);
// 添加力(下一集会增大喷发力度的数值,我这里先增大)
BoxCollision->AddForce((FVector(0.f, 0.f, 4.f) + ForceRot.Vector()) * 100000.f);
}
void ASlAiFlobObject::RenderTexture()
{
TSharedPtr<ObjectAttribute> ObjectAttr = *SlAiDataHandle::Get()->ObjectAttrMap.Find(ObjectIndex);
ObjectIconTex = LoadObject<UTexture>(NULL, *ObjectAttr->TexPath);
ObjectIconMatDynamic->SetTextureParameterValue(FName("ObjectTex"), ObjectIconTex);
BaseMesh->SetMaterial(0, ObjectIconMatDynamic);
}
最后给资源类补上生成掉落物的逻辑。
SlAiResourceObject.h
class SLAICOURSE_API ASlAiResourceObject : public AActor
{
protected:
// 生成掉落物
void CreateFlobObject();
};
SlAiResourceObject.cpp
// 引入头文件
#include "SlAiFlobObject.h"
void ASlAiResourceObject::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, 20.f), FRotator::ZeroRotator);
FlobObject->CreateFlobObject((*It)[0]);
}
}
}
}
ASlAiResourceObject* ASlAiResourceObject::TakeObjectDamage(int Damage)
{
HP = FMath::Clamp<int>(HP - Damage, 0, BaseHP);
if (HP <= 0) {
BaseMesh->SetCollisionResponseToAllChannels(ECR_Ignore);
// 创建掉落物
CreateFlobObject();
GetWorld()->DestroyActor(this);
}
}
运行游戏,采集资源可可以看见掉落物掉出来可拾取资源。但是目前资源只是一个纸片一样的外形,而且一动不动的,侧面看的时候几乎看不到。下一节课会让它有相应的动效。
笔者在做到这里的时候发现了一个问题,就是如果场景里在先前已经有了一个 C++ 的资源类 Actor,运行游戏的时候其 BeginPlay() 的初始化资源属性那句代码会报错,然后引擎崩溃;但是按逻辑来看是没有问题的,笔者检查的时候发现其资源类的 BeginPlay() 竟然比 SlAiGameMode 的 BeginPlay() 还要早执行,这就有点令人意外了。最后找了两天 bug 才误打误撞(多谢一位老哥的指点)发现,解决办法有二:
- 只要在场景中没有资源类 Actor 的情况下运行游戏,再退出,然后再往场景里添加资源类 Actor,这样就不会报错了。
- 在 SlAiDataHandle 里面的构造函数加入 InitResourceAttrMap(); 也就是将资源类 TMap 数据的读取放到 SlAiDataHandle 的构造函数,让数据在一开始就读取好。(原本是通过 GameMode 在其 BeginPlay() 方法加载资源类的 TMap,如果采用方法二则原本 SlAiDataHandle 的 InitializeGameData() 方法中调用 InitResourceAttrMap() 的语句可以去掉了)
个人推测可能是游戏关卡地图里面已经存在的 Actor 的 BeginPlay() 要比 GameMode 的 BeginPlay() 要早。