UE4 C++ 实现简易背包

Posted Vincent_0000

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UE4 C++ 实现简易背包相关的知识,希望对你有一定的参考价值。

效果展示

按E拾取之后,物品会出现在背包中,我们可以按I进行背包的开关。

对点击Use会提示你使用了该物品。

点击Drop之后可以将物品丢弃

左键可以移动物品

移动到另一个物品的时候会融合成一个新的物体:

然后丢掉~
(直接把我的方块弹飞了)

实现的整体思路

  • 将可拾取物体的一些基本功能用C++实现出来,然后添加一个识别物体的UI,提示用E拾取物品。
  • 拾取之后要存储到背包中,将背包的UI实现,背包的UI分为两个部分,一个是大框架,另一个是小结构,可以通过函数调用,将物品按照小结构的形式放到大框架中。
  • 实现小结构的功能,可以拖拽、融合、丢弃、使用。

创建可交互物体

新建一个C++第三人称项目,打开之后新建继承Actor的C++类,命名为:Interactable(为了方便后面理解,这里就给出一个示例名字。)

Interactable

.h 文件

  • 声明他的名称和动作行为变量。
UPROPERTY(EditDefaultsOnly)
	FString Name;

UPROPERTY(EditDefaultsOnly)
	FString _Action;
  • 封装物品信息
UFUNCTION(BlueprintCallable, Category = "Pickup")
	FString GetUseText() const { return FString::Printf(TEXT("%s : Press E to %s"), *Name, *_Action);}

.cpp文件

  • 构造函数

初始化变量信息

Name = "Name not set";
_Action = "Interact";

pickup

回到编译器,右键我们刚刚创建的那个类,进行继承,创建C++,命名为:pickup(这里创建的时候忘记把P大写了····)

.h文件

  • 声明静态网格体,并放在保护类型中。

写代码要谨慎一点,万一被别人发现了这个变量是公开可修改的,一个外挂杀过来直接GG。

protected:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		UStaticMeshComponent* PickupMesh;

.cpp文件

  • 构造函数

创建静态网格体实例,赋予他物理属性,这个有利于我们后面进行丢弃的真实感。

PickupMesh = CreateDefaultSubobject<UStaticMeshComponent>("PickupMesh");
PickupMesh->SetSimulatePhysics(true);

回到编译器在Content 中新建文件夹Blueprint,右键pickup,继承它创建蓝图。命名为:BP_pickup, 我们需要创建三个实例,右击该蓝图,继承它创建蓝图,命名为:BP_Item_Cube, 静态网格体给个方块。
同理创建BP_Item_chair,给他一个椅子,创建BP_Item_sphere,给它一个球。

创建自定义玩家控制器

为什么要创建自定义玩家控制器?
因为我们的UI是通过玩家控制器来展现在用户面前的。

新建继承PlayerController类的C++文件,命名为:GameplayController

.h文件

  • 声明公开变量,储存的是控制当前所指的可交互对象。
UPROPERTY(BlueprintReadWrite, VisibleAnywhere)
	class AInteractable* CurrentInteractable;

回到编辑器,右击他创建蓝图。命名为:BP_GameplayController

为角色增添功能

.h文件

  • 在保护类型中声明射线识别函数声明
protected:
	void CheckForInteractables();
  • 每一帧都要对物体进行射线检测,所以要重写Tick函数。
public:
	virtual void Tick(float DeltaTime) override;

.cpp 文件

  • CheckForInteractables()

添加头文件:

#include "GameplayController.h"
#include "Interactable.h"

这个函数的整体思路就是通过调用LineTraceSingleByChannel,从StartTrace点开始到EndTrace点形成的一个射线中检测是否有接触到设置为可见的、符合QueryParams碰撞信息的对象,对象信息储存在HitResult中。如果有,通过这个碰撞结果去获取对象,判断是否为可交互的对象,如果是的,那么就将玩家控制器中的待拾取物体设置为当前这个物体。

// 射线拾取
FHitResult HitResult; // 存储碰撞的结果的变量

FVector StartTrace = FollowCamera->GetComponentLocation(); // 射线起始点
FVector EndTrace = (FollowCamera->GetForwardVector() * 300) + StartTrace; // 射线终止点。

FCollisionQueryParams QueryParams; // 储存了碰撞相关的信息
QueryParams.AddIgnoredActor(this); // 将我们角色自身忽略掉,减少性能开销

AGameplayController* controller = Cast<AGameplayController>(GetController()); // 玩家控制器

if (GetWorld()->LineTraceSingleByChannel(HitResult, StartTrace, EndTrace, ECC_Visibility, QueryParams) && controller) {
	//检查我们点击的项目是否是一个可交互的项目
	if (AInteractable* Interactable = Cast<AInteractable>(HitResult.GetActor())) {
		controller->CurrentInteractable = Interactable;
		return;
	}
}

//如果我们没有击中任何东西,或者我们击中的东西不是一个可交互的,设置currentinteractable为nullptr
controller->CurrentInteractable = nullptr;

创建UI

在content中新建文件夹,命名为:UI, 右击空白处,新建蓝图控件。

命名为:WB_Ingame,这个UI用来识别物体。

在common中找到Text,拖到图形中去。

一个放在正中间,text内容为一个点,主要作用是方便我们镜头移动去识别。另一个放在旁边,表示我们识别物体的信息。

点击展示物体信息的文本,细节面板中找到:

然后创建绑定:
按照这个蓝图去是实现

具体思路就是先获取我们玩家的控制器,看能不能成功转化为我们自定义的控制器类型,通过自定义的玩家控制器去获取我们现在的所指的可交互对象,调用可交互对象中的返回物体信息的字符串,然后输送到UI的展示界面。

接下来我们需要将界面展现到我们的游戏中:
点击打开关卡蓝图:

然后蓝图这么画:

思路:创建组件,然后展示到界面中。

创建自定义游戏模式

因为只有我们自定义的游戏模式才可以修改属性,把我们的玩家控制器放进去。
创建继承GameMode的C++文件,命名为:GameplayGameMode
然后右击它,创建蓝图。命名为:BP_GameplayGameMode
打开我们的项目设置


第一部分完成。

实现背包界面

创建大框架UI

在UI中新建UI控件,命名为:WB_Inventory
创建内容,设计UI:

图片颜色设置为黑色,稍微透明一点,然后在加上一个Wrap Box。

创建小组件UI

在UI文件夹中新建控件:C_InventorySlot

创建数据类型

我们可拾取的物品有很多,如果一个一个创建小组件UI会很繁琐,这就与计算机方便性相违背。
这时候我们就需要采用数据表来辅助我们,但是在创建数据表之前,先要定义数据类型。

数据类型定义在第三人称角色中(具体原因不知道为什么。)

// 数据表中的类型定义,数据表如果采用了下面结构体的类型,数据表中就会显示他的所有数据,就有点类似于继承。
USTRUCT(BlueprintType)  // 声明为蓝图类型
struct FInventoryItem : public FTableRowBase {
	GENERATED_BODY();

public:

	FInventoryItem() { // 构造函数 变量进行初始化。
		Name = FText::FromString("Item");
		Action = FText::FromString("Use");
		Description = FText::FromString("Please enter a description for this item");
		Value = 10;
	}


	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FName ItemID;  // 物品的ID

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		TSubclassOf<class Apickup> Itempickup; // 拾取类型对象

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FText Name; // 对象名字

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FText Action; // 对象作用

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		int32 Value;  // 对象的值

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		UTexture2D* Thumbnail; // 储存对象图片信息

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FText Description; // 对该数据的描述

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		TArray<FCraftingInfo> CraftCombinations; // 储存可以相互融合的物品信息

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		bool bCanBeUsed; // 是否可以被使用

	bool operator == (const FInventoryItem& Item) const { // 重载等于号,如果他们ID相同,就说明他们两个是属于同一种类型。
		if (ItemID == Item.ItemID) return true;
		else return false;
	}
};

接下来我们完善一下融合类型的声明,这个类型写在FInventoryItem 类型的前面。

// 融合类型的定义
USTRUCT(BlueprintType)
struct FCraftingInfo : public FTableRowBase {
	GENERATED_BODY();

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FName ComponentID; // 可以融合的物品ID

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FName ProductID; // 融合之后的物品ID

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		bool bDestroyItemA; //  是否销毁物品A 物品A就是ComponentID所代表的物品

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		bool bDestroyItemB; //  是否销毁物品B 
};

创建数据表

在此之前,我们需要导入几张图片,做为他们他们标志图片。


图片从哪里来的呢?

双击进去:
取消Grid之后,移动位置进行截图保存在桌面,然后导入即可。

创建文件夹data, 然后再创建一个文件夹Thumbnail, 在这个文件夹中导入图片。
在data中创建我们的数据表,右击

进入之后点击加号:
按照这个形式进行填写:


在这个里面还需要加一个东西,代表他可以和其他东西融合。


这样就解决了之前的麻烦了。
接下来实现按下E键之后,我们的物品到我们的背包中。

拾取操作

打开GameplayGameMode文件
添加:

public:

	class UDataTable* GetItemDB() const { return ItemDB; }

protected:

	UPROPERTY(EditDefaultsOnly)
		class UDataTable* ItemDB;

编译之后,打开BP_GameplayGameMode,在细节面板中添加我们的数据表:

打开C_InventorySlot
创建变量,命名为:Item

设置类型为数据表中的类型

这个眼睛要处于打开状态,这样其他地方就能够访问到他

点击图片,绑定图片,

点击文本,绑定名字:

打开pickup文件:
头文件添加:

protected:
	// 储存的是我们物品的ID,我们是通过物品ID去添加我到我们的背包
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		FName ItemID;

在构造函数中初始化他ItemID = FName("Please enter an ID");

打开我们的Interactable文件, 添加一个公开函数:

UFUNCTION(BlueprintImplementableEvent)
	void Interact(APlayerController* controller);

代表的是交互操作,这个函数到蓝图中实现,为什么要这么写?
因为交互操作有很多,你可能在这个项目中看不到他的优势,当他不仅仅是按E键拾取物品这个功能的时候,他在蓝图中实现的优势就体现出来了。不直接在C++中写死,让我们的代码更加有拓展性。

那它在我们这个项目中功能的实现在哪里呢?
打开BP_pickup, 在蓝图中,重新实现它。

思路:先拿到控制器,控制器中有一个函数就是通过ID去添加这个物品到背包中(这个待会就讲),添加完成之后就把自己删除了。

去绑定按键,项目设置Input中。

然后就到了这部分的重头戏:

GameplayController函数添加

  • 在头文件中添加:

需要添加头文件,第三人称角色的头文件,类型在第三人称头文件中。

public:
	// 通过数据ID查找并添加到我们的数据数组中
	UFUNCTION(BlueprintCallable, Category = "Utils")
		void AddItemToInventoryByID(FName ID);

	// 储存背包中的内容
	UPROPERTY(BlueprintReadWrite, VisibleAnywhere)
		TArray<FInventoryItem> Inventory;

protected:
	// 物品交互操作
	void Interact();

	virtual void SetupInputComponent() override;
  • 在CPP文件中添加

添加角色头文件、
#include "GameplayGameMode.h"
#include "Interactable.h"

void AGameplayController::AddItemToInventoryByID(FName ID)
{
	// 从世界中获取真实的游戏模式
	AGameplayGameMode* GameMode = Cast<AGameplayGameMode>(GetWorld()->GetAuthGameMode());
	// 将游戏模式里面定义的数据表拿出来
	UDataTable* ItemTable = GameMode->GetItemDB();

	// 从数据表中查找数据
	FInventoryItem* ItemToAdd = ItemTable->FindRow<FInventoryItem>(ID, "");

	// 如果找到了,那么就添加到我们UI展示的列表中
	if (ItemToAdd) {
		Inventory.Add(*ItemToAdd);
	}
}

void AGameplayController::Interact()
{
	// 按键一按下,如果我们当前识别到了物体,那么就调用物体的交互函数。
	if (CurrentInteractable) {
		CurrentInteractable->Interact(this);
	}
}

void AGameplayController::SetupInputComponent()
{
	Super::SetupInputComponent();
	//将按键与交互函数绑定
	InputComponent->BindAction("Use", IE_Pressed, this, &AGameplayController::Interact);
}

这个时候我们任务移动到物体位置,按下E键,物体会消失,物体到哪里去了呢,背包里啊!背包怎么打开啊,你快告诉我啊。

打开背包

绑定按键:

然后在BP_GameplayController蓝图中修改添加:

思路:按一次走A,创建背包控件,按第二次的时候就走B,销毁背包。

但是这个时候打开并没有什么东西出现,这是因为大框架里面的东西都还没有绑定。
接下来完善背包:
打开WB_Inventory
创建新函数:


然后在事件图中实现下面的操作:

思路:当我们的背包打开的时候,将鼠标显示出来,然后加载背包内容,背包关闭的时候把鼠标取消显示。

这样写会有一点小瑕疵,可以在打开背包的时候只认UI输入,关闭背包的时候只认游戏模式输入,然后需要加一个按钮(或者在其他地方加个条件打开背包的时候不能使用按键,是不是太麻烦了,不知道那些游戏里面是怎么实现的)。

实现背包内的操作

实现使用功能

打开C_InventorySlot,点击Use按钮,
设置为变量,然后拉到最下面,点击加号:

然后按照下面这个蓝图进行实现:

思路:点击使用之后,我们要把背包里面的我们点击的这个元素给删除掉(就是FIND那一块的操作),然后把当前显示到背包中的卡片给删除掉,你会发现他生成了一个Actor然后又把他删掉,主要的作用在于调用OnUsed。

OnUsed 这个函数的意思就是用来实现使用这个Actor之后的效果,比如说,我们人物用了血包,人物要加血,加血这个动作,就是在这个函数中实现。

我们先在pickup.h中给他声明:

UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "Pickup")
	void OnUsed();

然后我们就可以在它的蓝图中实现效果,比如说:可以在BP_Item_Cube中添加:

那么我们使用的时候就会打印这个字符。

实现丢弃功能

这个和之前那个差不多,只是没有把我们生成的Actor删除而已。

实现拖拽功能

先在我们的Blueprint文件夹中新建一个蓝图:

继承DragDropOperation, 它是实现拖拽基础功能的类。

命名为:InventoryDragDropOperation

创建变量:


这个变量是保存我们拖拽物体的信息,用于物体于物体之间的融合。


在UI文件夹中新建一个UI控件,命名为:C_InventorySlot_Drag,

sizeBox需要设置:

这里是设置拖拽的时候的图标,然后给图片绑定图片变量,

创建变量, Thumbnail,表示拖拽时候的图片:

点击图片,查看细节面板,绑定变量:

打开C_InventorySlot
打开蓝图,重载鼠标左键按键反应:

重写OnDragDetected

思路:将我们设计好的拖拽UI,放到拖拽蓝图中,还要记得把拖拽物体的信息也传输进去。

重写Drop

思路:实现当拖拽停止的时候调用融合函数。

融合函数实现:
打开C++文件:GameplayController
头文件:

public:

	//重新加载玩家背包-当你对玩家库存做了更改时调用这个, 并且在蓝图可以直接实现他的逻辑。
	UFUNCTION(BlueprintImplementableEvent)
		void ReloadInventory();
		
	// 融合A、B元素 物品A代表正在拖拽的物品, 物品B表示物品B拖到的函数位置所代表的物品
	UFUNCTION(BlueprintCallable, Category = "Utils")
		void CraftItem(FInventoryItem ItemA, FInventoryItem ItemB, AGameplayController* controller);

CPP文件中添加:

void AGameplayController::CraftItem(FInventoryItem ItemA, FInventoryItem ItemB, AGameplayController* controller)
{
	//检查我们是否做了一个操作,或者如果物品不正确,什么都没有做
	// 先在我们待融合的物品B的融合条件中寻找是否有物体A
	for (auto Craft : ItemB.CraftCombinations) {
		// 如果两个可以融合,那么就将融合之后的物品加入到我们的背包中,根据设定判断是否要把融合的两个物体删除。
		if (Craft.ComponentID == ItemA.ItemID) {
			if (Craft.bDestroyItemA) {
				Inventory.RemoveSingle(ItemA);
			}
			if (Craft.bDestroyItemB) {
				Inventory.RemoveSingle(ItemB);
			}
			AddItemToInventoryByID(Craft.ProductID);

			ReloadInventory(); // 更新背包
		}
	}
}

出现了一个蓝图中实现的函数,接下来实现一下:
打开BP_GameplayController
添加:

完结撒花!!!

以上是关于UE4 C++ 实现简易背包的主要内容,如果未能解决你的问题,请参考以下文章

UE4中使用蓝图实现简易的游戏暂停功能

Unity UGUI有趣应用 -------------------- 背包系统(上)之简易单页背包系统及检索功能的实现

UE4蓝图与C++交互——射击游戏中多武器系统的实现

基于C++代码的UE4学习—— 自定义代理结合Timer实现道具的消失与重生

UE4 Unlua源码解析8 - Lua与C++之间的参数转换的实现原理

[工作积累] UE4 并行渲染的同步 - Sync between FParallelCommandListSet & FRHICommandListImmediate calls(代码片段