UE4开发C++沙盒游戏教程笔记(八)(对应教程集数 25 ~ 27)

24. 注册快捷栏到 PlayerState

这节课准备正式用一下在数据结构类写好的快捷栏容器结构体 ShortcutContainer。

首先在样式类声明 7 个笔刷,专门给物品的图片来使用。

SlAiGameStyle.h

USTRUCT()
struct SLAICOURSE_API FSlAiGameStyle : public FSlateWidgetStyle
{

	// 物品的 Brush
	UPROPERTY(EditAnywhere, Category = "Package")
	FSlateBrush ObjectBrush_1;
	UPROPERTY(EditAnywhere, Category = "Package")
	FSlateBrush ObjectBrush_2;
	UPROPERTY(EditAnywhere, Category = "Package")
	FSlateBrush ObjectBrush_3;
	UPROPERTY(EditAnywhere, Category = "Package")
	FSlateBrush ObjectBrush_4;
	UPROPERTY(EditAnywhere, Category = "Package")
	FSlateBrush ObjectBrush_5;
	UPROPERTY(EditAnywhere, Category = "Package")
	FSlateBrush ObjectBrush_6;
	UPROPERTY(EditAnywhere, Category = "Package")
	FSlateBrush ObjectBrush_7;
	
}

然后通过数据处理类来获取这些笔刷,组合成一个笔刷数组。

SlAiDataHandle.h

class SLAICOURSE_API SlAiDataHandle
{
public:
	
	// 物品贴图资源数组
	TArray<const FSlateBrush*> ObjectBrushList;

private:

	// 获取 GameStyle 
	const struct FSlAiGameStyle* GameStyle;
};

SlAiDataHandle.cpp

#include "SlAiGameWidgetStyle.h"	// 添加头文件

void SlAiDataHandle::InitObjectAttr()
{
	SlAiSingleton<SlAiJsonHandle>::Get()->ObjectAttrJsonRead(ObjectAttrMap);
	// 获取 GameStyle
	GameStyle = &SlAiStyle::Get().GetWidgetStyle<FSlAiGameStyle>("BPSlAiGameStyle");
	// 填充笔刷数组
	ObjectBrushList.Add(&GameStyle->EmptyBrush);
	ObjectBrushList.Add(&GameStyle->ObjectBrush_1);
	ObjectBrushList.Add(&GameStyle->ObjectBrush_2);
	ObjectBrushList.Add(&GameStyle->ObjectBrush_3);
	ObjectBrushList.Add(&GameStyle->ObjectBrush_4);
	ObjectBrushList.Add(&GameStyle->ObjectBrush_5);
	ObjectBrushList.Add(&GameStyle->ObjectBrush_6);
	ObjectBrushList.Add(&GameStyle->ObjectBrush_7);

	// 动态生成 Object 的图片 Brush,这段代码会引起崩溃,放在这里仅供参考
	/*
	for (int i = 1; i < ObjectAttrMap.Num(); ++i) {
		// 测试函数,动态创建 FSlateBrush,一定要创建指针,否则会在函数结束时销毁资源
		FSlateBrush* ObjectBrush = new FSlateBrush();
		ObjectBrush->ImageSize = FVector2D(80.f, 80.f);
		ObjectBrush->DrawAs = ESlateBrushDrawType::Image;
		UTexture2D* ObjectTex = LoadObject<UTexture2D>(NULL, *(*ObjectAttrMap.Find(i))->TexPath);
		ObjectBrushList.Add(ObjectBrush);
	}	
	*/
}

SSlAiShortcutWidget.h

#include "SlAiTypes.h"	// 引入头文件

把准备好的物品笔刷数组用在实例化物品容器结构体上。

SSlAiShortcutWidget.cpp

#include "SlAiDataHandle.h"		// 引入头文件


void SSlAiShortcutWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
	if (!IsInitializeContainer) {
		InitializeContainer();
		IsInitializeContainer = true;
	}
}

void SSlAiShortcutWidget::InitializeContainer()
{
	// 声明容器结构体的指针数组
	TArray<TSharedPtr<ShortcutContainer>> ContainerList;

	for (int i = 0; i < 9; ++i) {
		//... 省略
		
		// 实例化一个容器结构体
		TSharedPtr<ShortcutContainer> Container = MakeShareable(new ShortcutContainer(ContainerBorder, ObjectImage, 
			ObjectNumText, &GameStyle->NormalContainerBrush, &GameStyle->ChoosedContainerBrush, 
			&SlAiDataHandle::Get()->ObjectBrushList));
		
		// 如果是第一个容器,设置它为选中状态
		if (i == 0)	Container->SetChoosed(true);
	
		ContainerList.Add(Container);
	}
}

准备工作都做好了。后续我们想通过滑动滚轮切换选中的快捷栏,滑动滚轮的事件是要在玩家控制器上进行的。但我们不能直接让玩家控制器来持有一个快捷栏 UI 对象,往里面注入数据,这样做的耦合度太高了。

况且我们不能只把物品信息存储在快捷栏 UI 上,这样的话 UI 一但销毁就丢失快捷栏内物品信息了。按照标准来说,数据都是暂存在 UI 里供显示使用的,现在就只需要找一个对象来存放这个快捷栏里面有什么物品。

PlayerState 类就是一个很好的存储玩家数据的对象,PlayerState 与 UI 的数据交互我们通过委托来实现。控制器的滑动滚轮事件留到下节课再讲。

首先在快捷栏界面类里声明委托。

SSlAiShortcutWidget.h

// 注册容器到 PlayerState 类的委托(为了统一,委托名我把 cut 的 c 换成小写了)
DECLARE_DELEGATE_TwoParams(FRegisterShortcutContainer, TArray<TSharedPtr<ShortcutContainer>>*, TSharedPtr<STextBlock>)

class SLAICOURSE_API SSlAiShortcutWidget : public SCompoundWidget
{
public:

	// 声明委托的变量
	FRegisterShortcutContainer RegisterShortcutContainer;
}

随后在 .cpp 里执行这个委托

SSlAiShortcutWidget.cpp

void SSlAiShortcutWidget::InitializeContainer()
{

	// 将实例化的结构体注册进 PlayerState 的容器数组
	RegisterShortcutContainer.ExecuteIfBound(&ContainerList, ShortcutInfoTextBlock);
}

在 PlayerState 里添加存储快捷栏内物品信息结构体的数组容器、物品名字信息的文本变量;以及添加两个方法,一个是给快捷栏界面提供所有格子内物品结构体的(用来给快捷栏界面的委托绑定用),一个是获取物品名字信息的。

SlAiPlayerState.h

#include "SlAiTypes.h"	// 引入头文件

#include "SlAiPlayerState.generated.h"

class STextBlock;	// 提前声明

UCLASS()
class SLAICOURSE_API ASlAiPlayerState : public APlayerState
{
	GENERATED_BODY()
	
public:

	ASlAiPlayerState();

	// 提供给 ShortcutWidget 的添加快捷栏容器委托
	void RegisterShortcutContainer(TArray<TSharedPtr<ShortcutContainer>>* ContainerList, 
		TSharedPtr<STextBlock> ShortcutInfoTextBlock);

private:

	// 获取快捷栏物品信息
	FText GetShortcutInfoText() const;

private:

	// 快捷栏序列
	TArray<TSharedPtr<ShortcutContainer>> ShortcutContainerList;

	// 快捷栏信息参数
	TAttribute<FText> ShortcutInfoTextAttr;
};

SlAiPlayerState.cpp

#include "STextBlock.h"	// 引入头文件

ASlAiPlayerState::ASlAiPlayerState()
{

}

void ASlAiPlayerState::RegisterShortcutContainer(TArray<TSharedPtr<ShortcutContainer>>* ContainerList, 
	TSharedPtr<STextBlock> ShortcutInfoTextBlock)
{
	for (TArray<TSharedPtr<ShortcutContainer>>::TIterator It(*ContainerList); It; ++It) {
		ShortcutContainerList.Add(*It);
	}

	ShortcutInfoTextAttr.BindUObject(this, &ASlAiPlayerState::GetShortcutInfoText);
	// 绑定快捷栏信息 TextBlock
	ShortcutInfoTextBlock->SetText(ShortcutInfoTextAttr);

	// 临时测试代码,设置快捷栏的物品
	ShortcutContainerList[1]->SetObject(1)->SetObjectNum(5);
	ShortcutContainerList[2]->SetObject(2)->SetObjectNum(15);
	ShortcutContainerList[3]->SetObject(3)->SetObjectNum(1);
	ShortcutContainerList[4]->SetObject(4)->SetObjectNum(35);
	ShortcutContainerList[5]->SetObject(5)->SetObjectNum(45);
	ShortcutContainerList[6]->SetObject(6)->SetObjectNum(55);
	ShortcutContainerList[7]->SetObject(7)->SetObjectNum(64);
}

FText ASlAiPlayerState::GetShortcutInfoText() const
{
	// 供测试使用
	return FText::FromString("hahaha");
}

HUD 类可以作为控制器与 UI 界面沟通的一个桥梁。它可以获得快捷栏界面,但是又怎么让它获取到 PlayerState,让快捷栏界面的委托能够绑定到它里面的方法呢?这时候就要借助下 GameMode 了。

在 GameMode 里初始化 PlayState,顺便也把角色和控制器类也初始化了。

SlAiGameMode.h

public:

	// 组件赋值,给 GameHUD 调用,避免空引用引起崩溃
	void InitGamePlayModule();

public:

	class ASlAiPlayerController* SPController;

	class ASlAiPlayerCharacter* SPCharacter;

	class ASlAiPlayerState* SPState;
}

SlAiGameMode.cpp

void ASlAiGameMode::Tick(float DeltaSeconds)
{
	
}

void ASlAiGameMode::InitGamePlayModule()
{
	// 添加引用
	SPController = Cast<ASlAiPlayerController>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
	SPCharacter = Cast<ASlAiPlayerCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
	SPState = Cast<ASlAiPlayerState>(SPController->PlayerState);
}

void ASlAiGameMode::BeginPlay()
{
	SlAiDataHandle::Get()->InitializeGameData();
	
	// 如果控制器存在则初始化组件
	if (!SPController) InitGamePlayModule();
}

然后 HUD 再通过 GameMode 获取 PlayerState,让快捷栏界面的委托能顺利绑定到 PlayerState 的方法。

SlAiGameHUD.h

{
public:

	// 保存 GameMode 指针
	class ASlAiGameMode* GM;

protected:
	// 重写 BeginPlay()
	virtual void BeginPlay() override;

}

SlAiGameHUD.cpp

#include "UI/HUD/SlAiGameHUD.h"
#include "Engine/Engine.h"
#include "SlateBasics.h"
#include "Kismet/GameplayStatics.h"	// 引入头文件

#include "SSlAiGameHUDWidget.h"
// 引入头文件
#include "SSlAiShortcutWidget.h"

// 引入头文件
#include "SlAiPlayerController.h"
#include "SlAiPlayerState.h"
#include "SlAiGameMode.h"


void ASlAiGameHUD::BeginPlay()
{
	Super::BeginPlay();

	GM = Cast<ASlAiGameMode>(UGameplayStatics::GetGameMode(GetWorld()));
	if (!GM) return;
	// 先确保要调用的组件都已经实现
	GM->InitGamePlayModule();
	// 绑定注册快捷栏容器
	GameHUDWidget->ShortcutWidget->RegisterShortcutContainer.BindUObject(GM->SPState, &ASlAiPlayerState::RegisterShortcutContainer);
}

最后在游戏样式蓝图类配置物品笔刷。

配置物品笔刷
运行可见下方快捷栏的提示文字和物品。(这里物品名字的 Font_Outline_40 的描边是黑色的)

在这里插入图片描述

25. 快捷栏切换与右手插槽

实现切换选中的快捷栏

接下来写一下鼠标滑轮滚动切换快捷栏选中的功能。

添加两个动作映射的按键绑定,这里就用文字表述了:
ScrollUp -> Mouse Wheel Up
ScrollDown -> Mouse Wheel Down

切换快捷栏所用到的数据和方法也是在 PlayerState 里面来实现。

SlAiPlayerState.h

#include "SlAiTypes.h"
// 引入头文件
#include "SlAiDataHandle.h"

class STextBlock;

UCLASS()
class SLAICOURSE_API ASlAiPlayerState : public APlayerState
{
	GENERATED_BODY()
public:


	// 切换快捷栏
	void ChooseShortcut(bool IsPre);

	// 获取选中容器内的物品的 Index
	int GetCurrentHandObjectIndex() const;

public:

	// 当前被选中的快捷栏序号
	int CurrentShortcutIndex;
	
}

SlAiPlayerState.cpp

#include "STextBlock.h"

ASlAiPlayerState::ASlAiPlayerState()
{
	// 当前选中的快捷栏序号
	CurrentShortcutIndex = 0;
}


void ASlAiPlayerState::ChooseShortcut(bool IsPre)
{
	// 下一个被选择的容器的下标
	int NextIndex = 0;

	if (IsPre) {
		NextIndex = CurrentShortcutIndex - 1 < 0 ? 8 : CurrentShortcutIndex - 1;
	}
	else {
		NextIndex = CurrentShortcutIndex + 1 > 8 ? 0 : CurrentShortcutIndex + 1;
	}
	ShortcutContainerList[CurrentShortcutIndex]->SetChoosed(false);
	ShortcutContainerList[NextIndex]->SetChoosed(true);

	// 更新当前选中的容器 Index
	CurrentShortcutIndex = NextIndex;
}

int ASlAiPlayerState::GetCurrentHandObjectIndex() const
{
	if (ShortcutContainerList.Num() == 0) return 0;
	return ShortcutContainerList[CurrentShortcutIndex]->ObjectIndex;
}

FText ASlAiPlayerState::GetShortcutInfoText() const
{
	// 把之前的测试代码去掉
	TSharedPtr<ObjectAttribute> ObjectAttr;
	ObjectAttr = *SlAiDataHandle::Get()->ObjectAttrMap.Find(GetCurrentHandObjectIndex());
	switch (SlAiDataHandle::Get()->CurrentCulture)
	{
	case ECultureTeam::EN:
		return ObjectAttr->EN;
	case ECultureTeam::ZH:
		return ObjectAttr->ZH;
	}
	return ObjectAttr->ZH;
}

接下来在玩家控制器类绑定滑轮滚动的事件。此外我们在按住鼠标做动作的时候不允许切换选中的快捷栏,否则会有 bug(比如吃东西的途中又切换剑,此时又要播放劈砍动作)

SlAiPlayerController.h

#include "SlAiTypes.h"

{
	
public:


	// 获取玩家状态
	class ASlAiPlayerState* SPState;

private:

	// 滑轮上下滚动事件
	void ScrollUpEvent();
	void ScrollDownEvent();

private:


	// 是否按着鼠标左右键
	bool IsLeftButtonDown;
	bool IsRightButtonDown;
}

SlAiPlayerController.cpp

// 引入头文件
#include "SlAiPlayerState.h"


void ASlAiPlayerController::BeginPlay()
{
	Super::BeginPlay();
	if (!SPCharacter) SPCharacter = Cast<ASlAiPlayerCharacter>(GetCharacter());
	// 获取状态
	if(!SPState) SPState = Cast<ASlAiPlayerState>(PlayerState);

	bShowMouseCursor = false;

	FInputModeGameOnly InputMode;
	InputMode.SetConsumeCaptureMouseDown(true);
	SetInputMode(InputMode);

	LeftUpperType = EUpperBody::Punch;
	RightUpperType = EUpperBody::PickUp;

	// 初始化是否按住鼠标键
	IsLeftButtonDown = false;
	IsRightButtonDown = false;
}


void ASlAiPlayerController::SetupInputComponent()
{
	// ... 省略
	InputComponent->BindAction("RightEvent", IE_Released, this, &ASlAiPlayerController::RightEventStop);
	// 绑定鼠标滑轮事件
	InputComponent->BindAction("ScrollUp", IE_Pressed, this, &ASlAiPlayerController::ScrollUpEvent);
	InputComponent->BindAction("ScrollDown", IE_Pressed, this, &ASlAiPlayerController::ScrollDownEvent);
	
}

// 补充鼠标左右键是否按下的变量更改
void ASlAiPlayerController::LeftEventStart()
{
	IsLeftButtonDown = true;
	SPCharacter->UpperType = LeftUpperType;
}

void ASlAiPlayerController::LeftEventStop()
{
	IsLeftButtonDown = false;
	SPCharacter->UpperType = EUpperBody::None;
}

void ASlAiPlayerController::RightEventStart()
{
	IsRightButtonDown = true;
	SPCharacter->UpperType = RightUpperType;
}

void ASlAiPlayerController::RightEventStop()
{
	IsRightButtonDown = false;
	SPCharacter->UpperType = EUpperBody::None;
}

void ASlAiPlayerController::ScrollUpEvent()
{
	// 如果不允许切换,直接返回
	if (!SPCharacter->IsAllowSwitch) return;

	// 如果鼠标正在按下则不允许跳转
	if (IsLeftButtonDown || IsRightButtonDown) return;

	// 告诉状态类切换快捷栏容器
	SPState->ChooseShortcut(true);
}

void ASlAiPlayerController::ScrollDownEvent()
{
	// 如果不允许切换,直接返回
	if (!SPCharacter->IsAllowSwitch) return;

	// 如果鼠标正在按下则不允许跳转
	if (IsLeftButtonDown || IsRightButtonDown) return;

	// 告诉状态类切换快捷栏容器
	SPState->ChooseShortcut(false);
}

此时运行游戏,滚动鼠标滚轮可以切换快捷栏里选中的格子,并且左右键按下后播放动作的途中不允许切换。

快捷栏切换

初步实现手上出现选中的快捷栏内物品模型

接下来实现 “角色切换到对应物品时,角色手上也出现对应物品的模型” 这个功能。

首先创建一个 C++ 的 Actor 类,路径为 /Public/Hand,命名为 SlAiHandObject,作为手上物品的基类。

随后基于这个类新建子类 SlAiHandNone、SlAiHandApple、SlAiHandAxe、SlAiHandHammer、SlAiHandMeat、SlAiHandStone、SlAiHandSword、SlAiHandWood 这八个类。

目前先简单完善下手持物品的基类,方便看效果。

SlAiHandObject.h


protected:

	UPROPERTY(EditAnywhere, Category = "SlAi")
	class UStaticMeshComponent* BaseMesh;

SlAiHandObject.cpp

// 引入头文件
#include "Components/StaticMeshComponent.h"
#include "ConstructorHelpers.h"

ASlAiHandObject::ASlAiHandObject()
{
	PrimaryActorTick.bCanEverTick = true;

	// 创建静态模型组件
	BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMesh"));
	RootComponent = BaseMesh;

	// 给模型组件添加上模型(临时代码,用于看效果)
	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Wep_Axe_01.SM_Wep_Axe_01'"));

	BaseMesh->SetStaticMesh(StaticBaseMesh.Object);
}

在角色类里添加一个手部的 UChildActorComponent,用来当手持物品。

SlAiPlayerCharacter.h

private:

	UPROPERTY(VisibleDefaultsOnly, Category = "SlAi")
	USkeletalMeshComponent* MeshFirst;

	// 手上物品
	UPROPERTY(VisibleDefaultsOnly, Category = "SlAi")
	class UChildActorComponent* HandObject;

老师提供的模型已经给角色的骨骼添加了对应的插槽,所以我们直接给这个 HandObject 绑定到插槽上就好了

SlAiPlayerCharacter.cpp

// 引入头文件
#include "Components/ChildActorComponent.h"
#include "SlAiHandObject.h"

ASlAiPlayerCharacter::ASlAiPlayerCharacter()
{
	
	MeshFirst->SetOwnerNoSee(true);

	// 实例化手上物品
	HandObject = CreateDefaultSubobject<UChildActorComponent>(TEXT("HandObject"));

	BaseTurnRate = 45.f;
	
}

void ASlAiPlayerCharacter::BeginPlay()
{
	Super::BeginPlay();

	// 把手持物品组件绑定到第三人称模型右手插槽上
	HandObject->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, FName("RHSocket"));

	// 添加 Actor 到 HandObject
	HandObject->SetChildActorClass(ASlAiHandObject::StaticClass());
}

现在运行游戏应该可以看到角色手上有一个斧头,但它的碰撞影响到角色移动,效果不太完美,后续继续再改进。

26. 物品类与碰撞事件绑定

添加一个新的物体通道 Tool,默认回应为 Overlap。

再添加一个碰撞配置

碰撞预设
然后把 PlayerProfile 的碰撞预设,对 Tool 的追踪类型改成 Ignore。

给手持物品类添加一个根组件,否则没办法对手持物体的网格体进行旋转等设置。
添加一个碰撞箱用于交互检测,再补充交互检测的两个方法。
添加手持物品 ID,用于标记这个物品。

SlAiHandObject.h

UCLASS()
class SLAICOURSE_API ASlAiHandObject : public AActor
{
	GENERATED_BODY()
	
public:	

	ASlAiHandObject();

	virtual void Tick(float DeltaTime) override;

public:

	// 物品 ID
	int ObjectIndex;

protected:

	virtual void BeginPlay() override;

	// 重写碰撞交互检测的方法
	UFUNCTION()
	virtual void OnOverlayBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	UFUNCTION()
	virtual void OnOverlayEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

protected:

	// 根组件
	class USceneComponent* RootScene;

	// 静态模型
	UPROPERTY(EditAnywhere, Category = "SlAi")
	class UStaticMeshComponent* BaseMesh;

	// 盒子碰撞体
	UPROPERTY(EditAnywhere, Category = "SlAi")
	class UBoxComponent* AffectCollision;
};

SlAiHandObject.cpp


#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"	// 引入头文件
#include "ConstructorHelpers.h"

ASlAiHandObject::ASlAiHandObject()
{
	PrimaryActorTick.bCanEverTick = true;

	// 实例化根组件
	RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
	RootComponent = RootScene;

	// 创建静态模型组件
	BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMesh"));
	BaseMesh->SetupAttachment(RootComponent);
	BaseMesh->SetCollisionProfileName(FName("NoCollision"));

	// 实现碰撞组件
	AffectCollision = CreateDefaultSubobject<UBoxComponent>(TEXT("AffectCollision"));
	AffectCollision->SetupAttachment(RootComponent);
	AffectCollision->SetCollisionProfileName(FName("ToolProfile"));

	// 初始时关闭 Overlay 检测
	// 4.26.2 已经不允许这样用了,所以这里有更改
	//AffectCollision->bGenerateOverlapEvents = false;
	AffectCollision->SetGenerateOverlapEvents(false);

	// 绑定检测方法到碰撞体
	FScriptDelegate OverlayBegin;
	OverlayBegin.BindUFunction(this, "OnOverlayBegin");
	AffectCollision->OnComponentBeginOverlap.Add(OverlayBegin);

	FScriptDelegate OverlayEnd;
	OverlayEnd.BindUFunction(this, "OnOverlayEnd");
	AffectCollision->OnComponentEndOverlap.Add(OverlayEnd);

	// 去掉之前写的用于预览的代码
	
}

virtual void OnOverlayBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{

}

virtual void OnOverlayEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{

}

关于盒体触发器的事件,如果读者不太明白的话建议看下虚幻引擎官方的一些触发器教程,这里就不作过多讲解了。

角色类这里也要去掉先前测试用的代码

SlAiPlayerCharacter.cpp

void ASlAiPlayerCharacter::BeginPlay()
{
	Super::BeginPlay();

	HandObject->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, FName("RHSocket"));

	// 添加 Actor 到 HandObject (先把这个代码注释掉)
	//HandObject->SetChildActorClass(ASlAiHandObject::StaticClass());
}

先把斧头类的内容完善下,便于观察运行效果。

SlAiHandAxe.h

public:

	ASlAiHandAxe();

protected:

	virtual void BeginPlay() override;

每个手持物品的网格体都要调整至贴合角色握持姿势的状态,同时碰撞盒也要调整至恰当,不过老师给我们准备好了数值。(实际上这一步操作也可以放到蓝图去做,因为通过代码调整实在太麻烦)

SlAiHandAxe.cpp

// 添加头文件
#include "Components/StaticMeshComponent.h"
#include "ConstructorHelpers.h"
#include "Components/BoxComponent.h"

ASlAiHandAxe::ASlAiHandAxe()
{
	// 给模型组件添加模型
	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Wep_Axe_01.SM_Wep_Axe_01'"));
	BaseMesh->SetStaticMesh(StaticBaseMesh.Object);
	
	BaseMesh->SetRelativeLocation(FVector(-28.f, -4.9f, 3.23f));
	BaseMesh->SetRelativeRotation(FRotator(0.f, -90.f, 90.f));
	BaseMesh->SetRelativeScale3D(FVector(1.f, 1.f, 1.f));

	// 设置碰撞盒属性
	AffectCollision->SetRelativeLocation(FVector(32.f, -5.f, 3.f));
	AffectCollision->SetRelativeScale3D(FVector(0.375f, 0.5f, 0.125f));
}

void ASlAiHandAxe::BeginPlay()
{
	Super::BeginPlay();

	// 定义物品序号
	ObjectIndex = 5;
}

在 Blueprint/Temp 路径下创建 SlAiHandAxe 的蓝图,取名为 AxeBP,此时在蓝图里应该可以看到位置调整好了的、带碰撞盒的斧子。

随后将另外 7 个物品也写好跟 SlAiHandAxe 一样的代码配置。此处就不贴它们的头文件了

SlAiHandApple.cpp

// 添加头文件
#include "Components/StaticMeshComponent.h"
#include "ConstructorHelpers.h"
#include "Components/BoxComponent.h"

ASlAiHandApple::ASlAiHandApple()
{
	// 给模型组件添加模型
	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Item_Fruit_02.SM_Item_Fruit_02'"));
	BaseMesh->SetStaticMesh(StaticBaseMesh.Object);
	
	BaseMesh->SetRelativeLocation(FVector(-8.f, -3.f, 7.f));
	BaseMesh->SetRelativeRotation(FRotator(-90.f, 0.f, 0.f));
	BaseMesh->SetRelativeScale3D(FVector(1.f, 1.f, 1.f));

	// 设置碰撞盒属性
	AffectCollision->SetBoxExtent(FVector(10.f, 10.f, 10.f));
	AffectCollision->SetRelativeLocation(FVector(0.f, 0.f, 10.f));
}

void ASlAiHandApple::BeginPlay()
{
	Super::BeginPlay();

	// 定义物品序号
	ObjectIndex = 3;
}

SlAiHandMeat.cpp

// 添加头文件
#include "Components/StaticMeshComponent.h"
#include "ConstructorHelpers.h"
#include "Components/BoxComponent.h"

ASlAiHandMeat::ASlAiHandMeat()
{
	// 给模型组件添加模型
	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Prop_Meat_02.SM_Prop_Meat_02'"));
	BaseMesh->SetStaticMesh(StaticBaseMesh.Object);
	
	BaseMesh->SetRelativeLocation(FVector(6.f, -7.044f, 2.62f));
	BaseMesh->SetRelativeRotation(FRotator(-50.f, 90.f, 0.f));
	BaseMesh->SetRelativeScale3D(FVector(0.75f, 0.75f, 0.75f));

	// 设置碰撞盒属性
	AffectCollision->SetBoxExtent(FVector(10.f, 10.f, 10.f));
	AffectCollision->SetRelativeLocation(FVector(0.f, 0.f, 10.f));
}

void ASlAiHandMeat::BeginPlay()
{
	Super::BeginPlay();

	// 定义物品序号
	ObjectIndex = 4;
}

SlAiHandStone.cpp

// 添加头文件
#include "Components/StaticMeshComponent.h"
#include "ConstructorHelpers.h"
#include "Components/BoxComponent.h"

ASlAiHandStone::ASlAiHandStone()
{
	// 给模型组件添加模型
	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Prop_StoneBlock_01.SM_Prop_StoneBlock_01'"));
	BaseMesh->SetStaticMesh(StaticBaseMesh.Object);
	
	BaseMesh->SetRelativeLocation(FVector(1.f, -1.414f, 7.071f));
	BaseMesh->SetRelativeRotation(FRotator(0.f, 0.f, -135.f));
	BaseMesh->SetRelativeScale3D(FVector(0.25f, 0.25f, 0.25f));

	// 设置碰撞盒属性
	AffectCollision->SetBoxExtent(FVector(10.f, 10.f, 10.f));
	AffectCollision->SetRelativeLocation(FVector(0.f, 0.f, 10.f));
}

void ASlAiHandStone::BeginPlay()
{
	Super::BeginPlay();

	// 定义物品序号
	ObjectIndex = 2;
}

SlAiHandSword.cpp

// 添加头文件
#include "Components/StaticMeshComponent.h"
#include "ConstructorHelpers.h"
#include "Components/BoxComponent.h"

ASlAiHandSword::ASlAiHandSword()
{
	// 给模型组件添加模型
	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Wep_Sword_01.SM_Wep_Sword_01'"));
	BaseMesh->SetStaticMesh(StaticBaseMesh.Object);
	
	BaseMesh->SetRelativeLocation(FVector(-15.f, 1.f, 2.f));
	BaseMesh->SetRelativeRotation(FRotator(-20.f, 90.f, -90.f));
	BaseMesh->SetRelativeScale3D(FVector(0.8f, 0.8f, 1.f));

	// 设置碰撞盒属性
	AffectCollision->SetRelativeLocation(FVector(62.f, 1.f, 2.f));
	AffectCollision->SetRelativeRotation(FRotator(0.f, 0.f, 20.f));
	AffectCollision->SetRelativeScale3D(FVector(1.5f, 0.19f, 0.1f));
}

void ASlAiHandSword::BeginPlay()
{
	Super::BeginPlay();

	// 定义物品序号
	ObjectIndex = 7;
}

SlAiHandWood.cpp

// 添加头文件
#include "Components/StaticMeshComponent.h"
#include "ConstructorHelpers.h"
#include "Components/BoxComponent.h"

ASlAiHandWood::ASlAiHandWood()
{
	// 给模型组件添加模型
	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Env_TreeLog_01.SM_Env_TreeLog_01'"));
	BaseMesh->SetStaticMesh(StaticBaseMesh.Object);
	
	BaseMesh->SetRelativeScale3D(FVector(0.1f, 0.1f, 0.1f));
	BaseMesh->SetRelativeRotation(FRotator(0.f, -20.f, 0.f));

	// 设置碰撞盒属性
	AffectCollision->SetBoxExtent(FVector(10.f, 10.f, 10.f));
	AffectCollision->SetRelativeLocation(FVector(0.f, 0.f, 10.f));

}

void ASlAiHandWood::BeginPlay()
{
	Super::BeginPlay();

	// 定义物品序号
	ObjectIndex = 1;
}

由于没有锤子的独立模型,所以锤子类的代码稍有不同。

SlAiHandHammer.h


public:

	ASlAiHandHammer();

protected:

	virtual void BeginPlay() override;

protected:

	// 作为锤头
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mesh")
	UStaticMeshComponent* ExtendMesh;

SlAiHandHammer.cpp

// 添加头文件
#include "Components/StaticMeshComponent.h"
#include "ConstructorHelpers.h"
#include "Components/BoxComponent.h"

ASlAiHandHammer::ASlAiHandHammer()
{
	// 给模型组件添加模型
	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticBaseMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Bld_FencePost_01.SM_Bld_FencePost_01'"));
	// 绑定模型到 Mesh 组件
	BaseMesh->SetStaticMesh(StaticBaseMesh.Object);

	// 添加扩展模型组件
	ExtendMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ExtendMesh"));
	ExtendMesh->SetupAttachment(RootComponent);
	ExtendMesh->SetCollisionProfileName(FName("NoCollision"));

	// 绑定模型到扩展模型组件
	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticExtendMesh(TEXT("StaticMesh'/Game/Res/PolygonAdventure/Meshes/SM_Prop_StoneBlock_01.SM_Prop_StoneBlock_01'"));
	// 绑定模型到扩 Mesh 组件
	ExtendMesh->SetStaticMesh(StaticExtendMesh.Object);
	
	BaseMesh->SetRelativeLocation(FVector(35.f, 0.f, 3.f));
	BaseMesh->SetRelativeRotation(FRotator(0.f, -90.f, -90.f));
	BaseMesh->SetRelativeScale3D(FVector(0.4f, 0.4f, 0.4f));

	ExtendMesh->SetRelativeLocation(FVector(33.f, 1.f, 3.f));
	ExtendMesh->SetRelativeRotation(FRotator(0.f, -90.f, -90.f));
	ExtendMesh->SetRelativeScale3D(FVector(0.4f, 0.4f, 0.4f));

	// 设置碰撞盒属性
	AffectCollision->SetRelativeLocation(FVector(26.f, 1.f, 3.f));
	AffectCollision->SetRelativeScale3D(FVector(0.22f, 0.44f, 0.31f));
}

void ASlAiHandHammer::BeginPlay()
{
	Super::BeginPlay();

	// 定义物品序号
	ObjectIndex = 6;

}

None 因为不需要有网格体,所以也有些区别

SlAiHandNone.cpp

// 添加头文件
#include "Components/BoxComponent.h"

ASlAiHandNone::ASlAiHandNone()
{
	// 不用绑定模型

	// 设置碰撞盒属性
	AffectCollision->SetBoxExtent(FVector(10.f, 10.f, 10.f));
	AffectCollision->SetRelativeLocation(FVector(0.f, 0.f, 10.f));
}

void ASlAiHandNone::BeginPlay()
{
	Super::BeginPlay();

	// 定义物品序号
	ObjectIndex = 0;
}

编译后可以在 Temp 目录下新建这另外 7 个类的蓝图(但是 Temp 里面的蓝图后续都用不到,现在只是通过蓝图看看效果,看完可以删除),命名格式与 AxeBP 一致。

此时可以看见所有的物品都有了自己的蓝图和模型。

手持物品一览

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值