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++ 实现简易背包的主要内容,如果未能解决你的问题,请参考以下文章
Unity UGUI有趣应用 -------------------- 背包系统(上)之简易单页背包系统及检索功能的实现
基于C++代码的UE4学习—— 自定义代理结合Timer实现道具的消失与重生
UE4 Unlua源码解析8 - Lua与C++之间的参数转换的实现原理
[工作积累] UE4 并行渲染的同步 - Sync between FParallelCommandListSet & FRHICommandListImmediate calls(代码片段