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 才误打误撞(多谢一位老哥的指点)发现,解决办法有二:

  1. 只要在场景中没有资源类 Actor 的情况下运行游戏,再退出,然后再往场景里添加资源类 Actor,这样就不会报错了。
  2. 在 SlAiDataHandle 里面的构造函数加入 InitResourceAttrMap(); 也就是将资源类 TMap 数据的读取放到 SlAiDataHandle 的构造函数,让数据在一开始就读取好。(原本是通过 GameMode 在其 BeginPlay() 方法加载资源类的 TMap,如果采用方法二则原本 SlAiDataHandle 的 InitializeGameData() 方法中调用 InitResourceAttrMap() 的语句可以去掉了)

个人推测可能是游戏关卡地图里面已经存在的 Actor 的 BeginPlay() 要比 GameMode 的 BeginPlay() 要早。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值