全网最详细UE5C++背包系统(附开源代码)

全网最详细UE5C++背包系统(附开源代码)

前言

起因是up想要锻炼自己的c++代码能力,顺便把它运用到自己的项目里,无奈网上找了一圈没有发现一个认真用c++做背包的教程,只好按照以前用蓝图做背包的逻辑一点点写。通过五天的努力终于实现了一些基础功能。做完之后才发现蓝图在某些地方并不是很好用,处理某些逻辑非常冗杂,反而c++不仅节省性能,代码逻辑也一目了然。出这个教程的目的一方面填补空白,另一方面也加深下自己对代码逻辑的印象,好在面试时展示出来。废话不多说了,下面是从头到尾的流程,一点点跟着敲就行了。

(温馨提示:本教程适合对UEC++以及GamePlay框架有一定了解的同学,如果是新手小白的话推荐先看蓝图实现背包的教程)

教程链接:ue5教程25:背包系统详解(1)背包系统基本概念与界面创建_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV13P4y1F7Qk/?spm_id_from=333.1387.favlist.content.click&vd_source=4e360c8b9acfb97ebe1f8385ad108e1b

项目代码(已开源):

https://github.com/Gamer7Flower/backpackcpp.git

创建所有需要用到的类

我们先把所需要用到的类先初始化完,再去实现功能。

在写代码之前我们现在.cs文件夹里添加所需的几个模块,按照我这样写就行了。

//BackPack_Cpp.Build.cs // Copyright Epic Games, Inc. All Rights Reserved. using UnrealBuildTool; public class BackPack_Cpp : ModuleRules { public BackPack_Cpp(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput","UMG", }); PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); } }

一、GameInstance类

我们知道GameInstance是用来存储全局变量的地方,而我们需要用到的背包数组不能说你换个地图之后就消失对吧,在GameInstance里保存下来才是最合理的。在这个Demo里我没有在编辑器里创建结构体还有数据表那些,因为在引擎里创建的东西不好直接与C++联动,所以我直接在GameInstance里创建了这个物品的结构体,方便别的类去调用。OK我们在蓝图里创建C++类,父类选择GameInstance。

创建好之后在MyBaceGameInstance.h文件中创建结构体,以及基于结构体的TArray数组命名为BasePack_Array这个非常重要,之后有好多变量都是BacePackArray,要区分开来。

// MyBaseGameInstance.h // Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Engine/GameInstance.h" #include "Engine/DataTable.h" #include "MyBaseGameInstance.generated.h" /** * 将背包物品保存在游戏实例中,因为这个背包物品数组是全局实时变化的 */ class UMeshComponent; // 物品类型枚举 UENUM(BlueprintType) enum class EItem_Type : uint8 { Weapon UMETA(DisplayName = "Weapon"), Prop UMETA(DisplayName = "Prop"), Food UMETA(DisplayName = "Food") }; // 物品结构体 USTRUCT(BlueprintType) struct FItem_Struct : public FTableRowBase // 标记,在蓝图中创建DataTable { GENERATED_BODY() UPROPERTY(EditAnywhere,BlueprintReadWrite) FString Name; // 物品名 UPROPERTY(EditAnywhere,BlueprintReadWrite) int32 Index; // 索引 UPROPERTY(EditAnywhere,BlueprintReadWrite) UTexture2D* Icon; // 图标 UPROPERTY(EditAnywhere,BlueprintReadWrite) int32 Count; // 当前数量 UPROPERTY(EditAnywhere,BlueprintReadWrite) int32 Max_Count; // 最大数量 UPROPERTY(EditAnywhere,BlueprintReadWrite) bool Can_Stack; // 是否可堆叠 UPROPERTY(EditAnywhere,BlueprintReadWrite) EItem_Type Item_Type; // 物品类型 }; UCLASS() class BACKPACK_CPP_API UMyBaseGameInstance : public UGameInstance { GENERATED_BODY() public: UPROPERTY(EditAnywhere,BlueprintReadWrite) TArray<FItem_Struct> BackPack_Array; // 游戏实例中的背包物品,可全局保存和使用 };

写完之后在引擎里编译一下,编译完成之后到项目设置里把游戏实例改为我们的MyBaseGameInstance类,这里必须选我们自己写的实例类,不然读取不到里边的数据。

二、Item类

在一个游戏中肯定有好多物品且物品的类别有很多,在demo里我分为Weapon,Food,Prop这三类,并且每个类别有若干个子物品(可以不分类,我这里便于以后拓展),知道这我们去创建Item的基类。创建C++类,父类选择Actor,命名为ItemBase

因为每个物品都有自己的信息,所以我们直接声明一个结构体变量为 FItem_Struct CurrentItemState并用UPROPRETY标记;然后创建两个组件,一个是SphereComp,一个是MeshComp(这两个可以在蓝图里创建,实际代码中用不着),然后写两个重叠事件(Actor开始重叠时,Actor结束重叠时)。

// ItemBase.h // Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Components/SphereComponent.h" #include "GameFramework/Actor.h" #include "Engine/DataTable.h" #include "Gameplay/MyBaseGameInstance.h" #include "ItemBase.generated.h" UCLASS() class BACKPACK_CPP_API AItemBase : public AActor { GENERATED_BODY() public: // 构造函数 AItemBase(); UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="BackPackSystem") FItem_Struct CurrentItemState; // 存储当前物品的所有信息 UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Comp") USphereComponent* SphereComp; UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Comp") UStaticMeshComponent* MeshComp; protected: // 重写重叠事件 UFUNCTION() void OnSphereBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); void OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); };

然后在cpp文件的构造函数里对变量进行初始化

// ItemBase.h #include "BackPackSystem/ItemBase.h" AItemBase::AItemBase() { SphereComp = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere")); RootComponent = SphereComp; // 设置根组件 MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh")); MeshComp->SetupAttachment(SphereComp); // 绑定事件 SphereComp->OnComponentBeginOverlap.AddDynamic(this, &AItemBase::OnSphereBeginOverlap); } 

进入编译器编译并保存。然后以ItemBase创建几个子类蓝图(我这里创建五个,几个都可以)

然后再细节面板里填该物品的信息就行了。(蓝图那部分仅用于调试可删)

几个子类都需要填,填完保存。我们继续打开ItemBase.cpp文件,在里边写重叠和结束重叠时的逻辑。

两个函数实现分别是

//ItemBase.cpp void AItemBase::OnSphereBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { if (OtherActor && OtherActor!= this) { if (OtherActor->ActorHasTag("Player")) // 直接用标签去判断 { ABackPack_CppCharacter* Player = Cast<ABackPack_CppCharacter>(OtherActor); if (Player && IsValid(Player->BackPackComponent)) { // 将Character中的FocusedItem的变量设置成自己 Player->FocusedItem = this; } } } } void AItemBase::OnSphereEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { if (OtherActor && OtherActor->ActorHasTag("Player")) { ABackPack_CppCharacter* Player = Cast<ABackPack_CppCharacter>(OtherActor); if (Player && Player->FocusedItem == this) { Player->FocusedItem = nullptr; // 离开时清除焦点 } } }

这里我们需要去玩家蓝图里加一个标签

三、绑定输入映射

因为我设置的是按下“E”键才能拾取东西,所以需要去角色类里设置一些东西。会涉及到一些增强输入的东西。还有之后会用到Tab键绑定背包开关界面,我们就顺便一起绑定了。

默认文件是项目名+Character(我这里BackPack_CPPCharacter),我们打开它,在头文件里添加以下东西。

//BackPack_CPPCharacter.h public: // 绑定输入操作 UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true")) UInputAction* OpenBPAction; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true")) UInputAction* InteractAction; bool bIsOpenBackPack = false; // 标记背包是否打开,默认是未打开 UPROPERTY() UBackPackUI* BackPackUI; // 这里可以先注释掉,等之后创建了BackPackUI类之后再解除,不然现在编译会报错。 UFUNCTION(BlueprintCallable,Category= "BackPackSystem") /** Tab交互 **/ void BackPackUIController(); bool bIsOpenBackPack = false; UPROPERTY() UBackPackUI* BackPackUI; // !!!在UClass()外需要用class UBackPackUI;标记一下 UFUNCTION(BlueprintCallable,Category= "BackPackSystem") void BackPackUIController(); UPROPERTY() AItemBase* FocusedItem; // 用于标记我们按E拾取的是哪个物品 需要class AItemBase;标记 /** E交互 **/ void Interact();

在cpp文件里只需要添加两行星号内的东西就行

//BackPack_CPPCharacter.cpp void ABackPack_CppCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { // Add Input Mapping Context if (APlayerController* PlayerController = Cast<APlayerController>(GetController())) { if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer())) { Subsystem->AddMappingContext(DefaultMappingContext, 0); } } // Set up action bindings if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) { // Jumping EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump); EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping); // Moving EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ABackPack_CppCharacter::Move); // Looking EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ABackPack_CppCharacter::Look); // ****************************************************************************************************** // OpenBackPack(Tab) EnhancedInputComponent->BindAction(OpenBPAction, ETriggerEvent::Triggered, this, &ABackPack_CppCharacter::BackPackUIController); // Interact(E) EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Triggered, this, &ABackPack_CppCharacter::Interact); // ****************************************************************************************************** } else { UE_LOG(LogTemplateCharacter, Error, TEXT("'%s' Failed to find an Enhanced Input component! This template is built to use the Enhanced Input system. If you intend to use the legacy system, then you will need to update this C++ file."), *GetNameSafe(this)); } } // ****************************************************************************************************** /***************BackPcakSystem*******************/ void ABackPack_CppCharacter::Interact() { // 先空实现,等会再去处理 } void ABackPack_CppCharacter::BackPackUIController() { // 先空实现,等会再去处理 } // ******************************************************************************************************

写完之后编译保存,回到蓝图中在/ThirdPerson/Input/Actions文件目录下添加输入操作。

细节面板里一定要设置已按下,两个一样。

在IMC_Default中添加输入映射(名字一定要输对)

最后打开角色蓝图的细节面板在里边设置好就可以了。

四、BackPackComponent类

这里我解释下为什么要单独写一个组件类,其实关于背包功能的逻辑,你写到哪里都可以,我之前项目是写在Player里的,但是实际开发中最好还是每一个小系统的东西用组件封装起来,一方面是美观,另一方面能够更好地去管理这些功能,就是说如果有别的类也需要某个系统,我们直接去绑定就好了,不用再去重写功能函数。

在Demo里我们这个组件类的作用有:1.添加功能(添加物品,丢弃物品),2.管理控件(控制UI开关等)

我们创建c++类,继承自Actor组件,命名为BackPackComponent。

这里先进行绑定,在Character头文件里添加变量,然后去构造函数中初始化。

//BackPackCPP_Character.h public: UPROPERTY(EditAnywhere,BlueprintReadWrite,Category= "BackPackSystem") UBackPackComponent* BackPackComponent; // 背包组件
//BackPackCPP_Character.cpp ABackPack_CppCharacter::ABackPack_CppCharacter() { ...... // 绑定背包组件 BackPackComponent = CreateDefaultSubobject<UBackPackComponent>(TEXT("BackPackComp")); ...... }

五、UI类

在背包里我们要有一个背包界面以及背包格子,还有鼠标右键点击格子出现的操作界面,这里我们一一创建。

1.BackPackSlot

这个格子包含几个信息,有物品图,物品数,格子的Index(这个可能比较乱,之后遇到了我会详细解释),还需要一个函数,SetSlot去设置格子的内容。

依旧新建c++类,继承自UserWidget(不要继承错了,不然有的函数用不了),命名为BackPackSlot。下面是对应代码。

这里的meta = (BindWidget),必须要有,用于关联蓝图中的同名控件。

//BackPackSlot.h // Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Blueprint/UserWidget.h" #include "BackPackSlot.generated.h" /** * */ class UTexture2D; class UTextBlock; class UImage; UCLASS() class BACKPACK_CPP_API UBackPackSlot : public UUserWidget { GENERATED_BODY() public: virtual void NativeConstruct() override; // 用于操作的控件 UPROPERTY(EditAnywhere,BlueprintReadWrite,Category = "BackPackSystem" , meta = (BindWidget)) TObjectPtr<UImage> SlotImage; UPROPERTY(EditAnywhere,BlueprintReadWrite,Category = "BackPackSystem" , meta = (BindWidget)) TObjectPtr<UTextBlock> Number_Text; UPROPERTY(EditAnywhere,BlueprintReadWrite,Category = "BackPackSystem") int32 Index = -1; // 外部传入,如果有东西就是对应物品在数组中的索引,如果没有就是-1 UPROPERTY(EditAnywhere,BlueprintReadWrite,Category = "BackPackSystem") int32 Number = 0; // 外部传入,物品数量 UPROPERTY(EditAnywhere,BlueprintReadWrite,Category = "BackPackSystem") TObjectPtr<UTexture2D> Image; // 外部传入,物品图标 void SetSlot(); // 设置格子内容 };

先去实现SetSlot函数,解释一下逻辑:Number是传入格子内的数量,如果是大于1就让它显示出来,主要是通过设置透明度来操作,下面通过设置笔刷来设置图片是因为传入的Image是Texture2d类型而我们Slot中设置的是Image格式,所以需要去转化一下,不能直接设置。

//BackPackSlot.cpp void UBackPackSlot::SetSlot() { /* 主要去渲染格子内容,通过Number判断 */ if (Number > 1) { // 需要显示出右下角数字 Number_Text->SetText(FText::FromString(FString(std::to_string(Number).c_str()))); Number_Text->SetOpacity(1.0f); } else { // 不需要显示数字,直接隐藏掉 Number_Text->SetOpacity(0.0f); } // 将Texture2D类型传递给自定义的Image组件去渲染 FSlateBrush InBrush; InBrush.SetResourceObject(Image); SlotImage->SetBrush(InBrush); } 

效果是这样的

其它功能先不写。

在蓝图中我们创建子类蓝图名为UMG_BackPackSlot

SlotImage的锚点一定要设置成居中。这个Number_Text文本内容也要先设置成空。

2.BackPackUI

依旧新建c++类,继承自UserWidget,命名为BackPackUI这个就是我们背包的主界面。

创建组件

//BackPackUI.h public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BackPackSytstem",meta = (BindWidget)) TObjectPtr<UUniformGridPanel> BackPackPanel; // 统一网格面板 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BackPackSytstem",meta = (BindWidget)) TObjectPtr<UButton> CloseButton; // 关闭背包按钮 int32 SlotCount = 16; //背包允许存在格子数量(可以暴露给蓝图然后自己酌情修改)

编译保存之后在蓝图里我们先给UI模板做好。创建一个继承自BackPackUI的蓝图命名为UMG_BackPack

我们按照这个结构组装好就行了

这里我们可以先把上边的slot蓝图添加到Panel里做一个预览方便调整,这里我觉得四行四列正好,就是16个格子,所以上边变量我设置的是16,可以自己修改,弄完我们把这些东西删了。

3.OperateUI

这个UI主要就是用于操作单个物品,是使用,丢弃或者是别的,这里我只做了“丢弃”功能,因为“使用”功能我这里要搭配GAS使用,太多了就先略过了。

还是继承UserWidget类创建,命名为OperateUI

//OperateUI.h // Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Blueprint/UserWidget.h" #include "Components/Button.h" #include "OperateUI.generated.h" /** * */ UCLASS() class BACKPACK_CPP_API UOperateUI : public UUserWidget { GENERATED_BODY() public: virtual void NativeConstruct() override; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BackPackSytstem",meta = (BindWidget)) TObjectPtr<UButton> UseButton; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BackPackSytstem",meta = (BindWidget)) TObjectPtr<UButton> ThrowButton; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BackPackSytstem") int32 SlotIndex; // 在使用物品时,通过这个SlotIndex去定位数组中的这个物品 UFUNCTION(BlueprintCallable,Category="BackPackSystem") void OnUseButtonClick(); UFUNCTION(BlueprintCallable,Category="BackPackSystem") void OnThrowButtonClick(); };

回到蓝图绑定控件

这里我用垂直框了,便于美观。

实现具体功能

前期工作做完了,现在可以写具体功能了

1.打开&关闭背包

我们点Tab键触发BackPackUIController(在BackPackComponent里),创建BackPackUI控件,将其添加到屏幕上,BackPackUI执行Refresh函数进行初始化,通过for循环遍历结构体,每个循环创建一个Slot,如果这个索引下东西就将信息传递给Slot,将其添加到Panel中,如果没有东西就跳过赋值这一步。

具体代码如下(Tab):

//BackPack_CppCharacter.h public: UFUNCTION(BlueprintCallable,Category= "BackPackSystem") void BackPackUIController();
//BackPack_CppCharacter.cpp void ABackPack_CppCharacter::BackPackUIController() { // 调用组件中的UI控制函数 BackPackComponent->BackPackUIController(); }

//BackPackComponent.h UPROPERTY() UBackPackUI* BackPackUI; bool bIsOpenBackPack = false; // 标识BackPackUI是否存在 UFUNCTION(BlueprintCallable,Category= "BackPackSystem") void BackPackUIController();
//BackPackComponent.cpp void UBackPackComponent::BackPackUIController() { //GEngine->AddOnScreenDebugMessage(1, 5.f, FColor::Red, TEXT("CreateBackPack!!!")); // 输出信息调试(可删) APlayerController* PlayerController = GetWorld()->GetFirstPlayerController(); // 获取玩家控制器 if (!bIsOpenBackPack) { // 如果UI不存在,这里直接创建 if (!IsValid(GetWorld())) return; APlayerController*OwningPlayer = GetWorld()->GetFirstPlayerController(); // 这里去到编辑器里复制UMG_BackPack,右键复制引用,最后的_C一定要加,用于标识引用的是蓝图 TSubclassOf<UUserWidget> WidgetClass = LoadClass<UBackPackUI>(nullptr, TEXT("/Game/BackPackSystem/UMG/UMG_BackPack.UMG_BackPack_C")); BackPackUI = CreateWidget<UBackPackUI>(OwningPlayer, WidgetClass); if (BackPackUI) { BackPackUI->BackPackTransfer = Cast<UMyBaseGameInstance>(GetWorld()->GetGameInstance())->BackPack_Array; // GameInstance里存放的数组 BackPackUI->AddToViewport(); bIsOpenBackPack = true; PlayerController->SetShowMouseCursor(true); PlayerController->SetInputMode(FInputModeUIOnly()); } } else { if (BackPackUI) { BackPackUI->RemoveFromParent(); } PlayerController->SetInputMode(FInputModeGameOnly()); PlayerController->SetShowMouseCursor(false); bIsOpenBackPack = false; if (OperateUI) OperateUI->RemoveFromParent(); // 如果这个OperateUI也存在,也要一并Remove } }

//BackPackUI.h public: virtual void NativeConstruct() override; // 构造函数(一定要加) UPROPERTY() UBackPackSlot* BackPackSlot; // 声明SlotUI,这里也要去UClass()外添加class UBackPackSlot UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BackPackSytstem") TArray<FItem_Struct> BackPackTransfer; // 用于构造时从GameInstance中复制数组数据 UFUNCTION(BlueprintCallable,Category="BackPackSystem") void RefreshBackPack(); //BackPackUI创建时执行 UFUNCTION(BlueprintCallable,Category="BackPackSystem") void OnCloseButtonClick(); // 点击关闭按钮执行
//BackPackUI.cpp void UBackPackUI::NativeConstruct() { Super::NativeConstruct(); UMyBaseGameInstance* MyGameInstance = Cast<UMyBaseGameInstance>(GetWorld()->GetGameInstance()); BackPackTransfer = MyGameInstance->BackPack_Array; // 每次构造时都要先获取具体值,因为GameInstance中的BackPack_Array一直在更新 RefreshBackPack(); // 构造时执行刷新所有格子 CloseButton->OnClicked.AddDynamic(this,&UBackPackUI::OnCloseButtonClick); // 绑定事件 } void UBackPackUI::RefreshBackPack() { /* UI每次构造时调用函数刷新背包内所有的格子 */ if (!IsValid(GetWorld())) return; APlayerController*OwningPlayer = GetWorld()->GetFirstPlayerController(); // 这个路径时BackPackSlot的,具体可以看模板中是哪个类就是谁的路径 TSubclassOf<UBackPackSlot> WidgetClass = LoadClass<UBackPackSlot>(nullptr, TEXT("/Game/BackPackSystem/UMG/UMG_BackPackSlot.UMG_BackPackSlot_C")); BackPackPanel->ClearChildren(); // 首先要清除子项 for (int32 i = 0 ; i < SlotCount ; i++) { BackPackSlot = CreateWidget<UBackPackSlot>(OwningPlayer,WidgetClass); // 创建Slot if (BackPackTransfer.IsValidIndex(i)) { // 这个位置存有东西,需要将Image,Index,Number这些参数传递个Slot去初始化 BackPackSlot->Image = BackPackTransfer[i].Icon; // 设置图标 BackPackSlot->Index = i; // 设置格子的Index BackPackSlot->Number = BackPackTransfer[i].Count; // 设置数量 BackPackSlot->SetSlot(); } BackPackPanel->AddChildToUniformGrid(BackPackSlot,(i / 4),(i % 4)); } } void UBackPackUI::OnCloseButtonClick() { ABackPack_CppCharacter* Player = Cast<ABackPack_CppCharacter>(GetWorld()->GetFirstPlayerController()->GetPawn()); if (!Player) return; Player->BackPackUIController(); // 这时候的bbIsOpenBackPack是true,所以再次执行时,判断BackPackUI是存在的,就会去关闭它 }

2.拾取物品

当我们按E键时,执行Interact函数,通过它调用BackPackComponent里的AddItem,然后销毁它,在AddItem函数里,就是通过当前拾取的这个物品的名字,去判断背包里有没有同名物体,有并且可以堆叠,且数量小于最大数量,就将他的Count加一,如果不是,就当作一个新物体,把它加到结构体的最后边。(这只适用于处理简单的单个物品拾取的逻辑,更复杂的还是自行探索)

//BackPack_CppCharacter.h public: UPROPERTY() AItemBase* FocusedItem; void Interact();
//BackPack_CppCharacter.cpp void ABackPack_CppCharacter::Interact() { if (FocusedItem && IsValid(BackPackComponent)) { // 调用物品的拾取逻辑或直接调用背包添加 BackPackComponent->AddItem(FocusedItem->CurrentItemState); FocusedItem->Destroy(); FocusedItem = nullptr; } }

//BackPackComponent.h public: void AddItem(FItem_Struct AddItem);
//BackPackComponent.cpp void UBackPackComponent::AddItem(FItem_Struct NewItem) { UWorld*world = GetWorld(); if (!world) return; UMyBaseGameInstance* MyGameInstance = Cast<UMyBaseGameInstance>(GetWorld()->GetGameInstance()); if (!MyGameInstance) return; TArray<FItem_Struct>& BackpackArray = MyGameInstance->BackPack_Array; // 查找背包中是否有相同名称的物品 for (auto& Item : BackpackArray) { if (Item.Name == NewItem.Name && Item.Can_Stack && Item.Count < Item.Max_Count) { Item.Count += NewItem.Count; return; } } // 如果没有找到匹配项或者物品不可堆叠,则作为新物品添加 NewItem.Index = BackpackArray.Num(); BackpackArray.Add(NewItem); //GEngine->AddOnScreenDebugMessage(1, 5.f, FColor::Red, TEXT("Add!")); // 用于调试可不加 }

3.丢弃物品

我们点击Slot会生成OperateUI,在OperateUI里点丢弃会调用OnThrowButtonClick函数,这个函数会调用ThrowItem,同时将该Slot内存放的Index传出去因为我们要根据这个Index找到对应的物品,再通过物品名找到该生成哪个物体。

//BackPackSlot.h public: UPROPERTY() ABackPack_CppCharacter* Player; //用class ABackPack_CppCharacter;标识 protected: // 重写函数:当鼠标按下时 virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
//BackPackSlot.cpp void UBackPackSlot::NativeConstruct() { Super::NativeConstruct(); Player = Cast<ABackPack_CppCharacter>(GetWorld()->GetFirstPlayerController()->GetPawn()); // 在构造函数里先获取到玩家 } FReply UBackPackSlot::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) { Player->BackPackComponent->bHasOperateUI = false; if (InMouseEvent.GetEffectingButton() == EKeys::RightMouseButton ) // 当点击右键时 { Player->BackPackComponent->SetOperateUI(Index); // 调用背包组件中的SetOpetateUI函数 return FReply::Handled(); } else { // 点击的不是右键,会触发拖拽(这个后边再说) Player->BackPackComponent->bHasOperateUI = true; Player->BackPackComponent->SetOperateUI(Index); // 调用背包组件中的SetOpetateUI函数 return UWidgetBlueprintLibrary::DetectDragIfPressed(InMouseEvent, this, EKeys::LeftMouseButton).NativeReply; } }

//BackPackComponent.h public: // ***管理控件*** UPROPERTY() UOperateUI* OperateUI; bool bHasOperateUI; // 标识OperateUI是否存在 void SetOperateUI(int32 SlotIndex);
//BackPackComponent.cpp void UBackPackComponent::SetOperateUI(int32 SlotIndex) { if (!IsValid(GetWorld())) return; APlayerController*OwningPlayer = GetWorld()->GetFirstPlayerController(); TSubclassOf<UOperateUI> WidgetClass = LoadClass<UOperateUI>(nullptr, TEXT("/Game/BackPackSystem/UMG/UMG_OperateUI.UMG_OperateUI_C")); if (!bHasOperateUI && SlotIndex >= 0) { if (IsValid(OperateUI)) OperateUI->RemoveFromParent(); OperateUI = CreateWidget<UOperateUI>(OwningPlayer, WidgetClass); if (OperateUI) // 根据鼠标点击位置去设置OperateUI的位置 { OperateUI->AddToViewport(); float MouseX,MouseY; OwningPlayer->GetMousePosition(MouseX,MouseY); OperateUI->SetPositionInViewport(FVector2D(MouseX,MouseY),true); OperateUI->SlotIndex = SlotIndex; // OperateUI是用于丢弃/使用时指定物品的 } } else { if (OperateUI) { OperateUI->RemoveFromParent(); OperateUI = nullptr; } } }

//OperateUI.cpp void UOperateUI::OnThrowButtonClick() { if (!IsValid(GetWorld())) return; ABackPack_CppCharacter* Player = Cast<ABackPack_CppCharacter>(GetWorld()->GetFirstPlayerController()->GetPawn()); UBackPackComponent* BackPackComponent = Player->BackPackComponent; if (SlotIndex >= 0) // 基本的边界检查 { BackPackComponent->ThrowItem(SlotIndex); } RemoveFromParent(); }

//BackPackComponent.h public: void ThrowItem(int32 SlotIndex); // 指定丢弃物品(参数SlotIndex是OperateUI中传入的) AActor* SpawnActorClass(const FString &Name); // 执行生成物品(通过物品名指定)
//BackPackComponent.cpp void UBackPackComponent::ThrowItem(int32 SlotIndex) { if (!IsValid(GetWorld())) return; UMyBaseGameInstance* MyGameInstance = Cast<UMyBaseGameInstance>(GetWorld()->GetGameInstance()); TArray<FItem_Struct>& BackPackArray = MyGameInstance->BackPack_Array; // 通过引用直接使用 GameInstance 的数组 // 边界检查 if (SlotIndex < 0 || SlotIndex >= BackPackArray.Num()) return; if (BackPackArray.Num() > 0) { FString Name = BackPackArray[SlotIndex].Name; AActor* SpawnActor = SpawnActorClass(Name); if (SpawnActor) { APlayerController* PlayerController = GetWorld()->GetFirstPlayerController(); if (PlayerController) { APawn* PlayerPawn = PlayerController->GetPawn(); if (PlayerPawn) { FVector Location = PlayerPawn->GetActorLocation(); // 通过Pawn才能获取到玩家位置 FVector SpawnLocation = Location + FVector(0.f, 0.f, -80.f); // 在玩家脚下 SpawnActor->SetActorLocation(SpawnLocation); // 设置Actor的位置 } // 修改原始数组 BackPackArray[SlotIndex].Count--; // 生成之后要把数量减1 // 如果没了,就把它移除 if (BackPackArray[SlotIndex].Count <= 0) BackPackArray.RemoveAt(SlotIndex); } // 更新 UI if (BackPackUI) { BackPackUI->BackPackTransfer = BackPackArray; // 更新一下数组 BackPackUI->RefreshBackPack(); // 让所有格子重新刷新一下 } } } } AActor* UBackPackComponent::SpawnActorClass(const FString& Name) { if (!IsValid(GetWorld())) return nullptr; // 通过物品名称加载蓝图路径 // 这里提醒一下,蓝图名一定要与结构体中的物品名相同,不然加载不到 FString BlueprintPath = FString::Printf(TEXT("/Game/BackPackSystem/Item/%s.%s_C"),*Name,*Name); // 加载对应蓝图类 UClass*ActorClass = LoadClass<AItemBase>(nullptr,*BlueprintPath); if (ActorClass) { // 如果成功加载了,就把加载的东西返回出去 AActor* SpawnedActor = GetWorld()->SpawnActor<AActor>(ActorClass); return SpawnedActor; } // 没有找到返回空指针 return nullptr; }

4.交换物品

交换物品在BackPackUI中执行,原理是通过拖动Slot,把它放置到另一个物品的Slot上,结构体数组中的两个物体位置互换,然后通过新数组更新所有格子。

//BackPackSlot.h public: UPROPERTY() UBackPackSlot* Payload; protected: // 重写函数:当检测到拖动时 virtual void NativeOnDragDetected(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation) override; // 重写函数:当拖动释放时 virtual bool NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) override;

这里我把上边的函数也拿了过来,因为有关联。

//BackPackSlot.cpp FReply UBackPackSlot::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) { Player->BackPackComponent->bHasOperateUI = false; if (InMouseEvent.GetEffectingButton() == EKeys::RightMouseButton ) { Player->BackPackComponent->SetOperateUI(Index); // 调用背包组件中的SetOpetateUI函数 return FReply::Handled(); } else { Player->BackPackComponent->bHasOperateUI = true; Player->BackPackComponent->SetOperateUI(Index); // 调用背包组件中的SetOpetateUI函数 // 这里检测到鼠标左键,创建拖动事件,会执行NativeOnDragDetected return UWidgetBlueprintLibrary::DetectDragIfPressed(InMouseEvent, this, EKeys::LeftMouseButton).NativeReply; } } void UBackPackSlot::NativeOnDragDetected(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation) { if (Index == -1) return; // 如果格子的Index是-1,说明是个空格子无法执行拖动,也无法被放置 UDragDropOperation* DragOp = UWidgetBlueprintLibrary::CreateDragDropOperation(UDragDropOperation::StaticClass()); if (DragOp) { DragOp->Payload = this; DragOp->DefaultDragVisual =SlotImage; // 拖动时的预览图,根你的纹理文件有关,纹理越大它就越大,不然就一小点,可以去掉 DragOp->Pivot = EDragPivot::BottomRight; // 不要设置到居中不然鼠标指针落点会判定不上 DragOp->Offset = FVector2D(0.f, 0.f); } OutOperation = DragOp; // 直接赋值给输出参数 } bool UBackPackSlot::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation ) { // BackPackSlot是拖动的目标格子,不是拖动的那个函数 UBackPackSlot* BackPackSlot = Cast<UBackPackSlot>(InOperation->Payload); if (BackPackSlot) { UWorld* World = GetWorld(); if (!World) return false; Player = Cast<ABackPack_CppCharacter>(World->GetFirstPlayerController()->GetCharacter()); if (Player && Player->BackPackComponent) { Player->BackPackComponent->SwapItem(BackPackSlot->Index, this->Index); return true; } } return false; }

//BackPackComponent.h public: UFUNCTION(BlueprintCallable,Category= "BackPackSystem") void SwapItem(int32 DragIndex , int32 DropIndex);
//BackPackComponent.cpp void UBackPackComponent::SwapItem(int32 DragIndex, int32 DropIndex) { // DragIndex:拖动的格子的Index ; DropIndex:放置的格子的Index if (!IsValid(GetWorld())) return; UMyBaseGameInstance* MyGameInstance = Cast<UMyBaseGameInstance>(GetWorld()->GetGameInstance()); TArray<FItem_Struct> BackPackArray = MyGameInstance->BackPack_Array; // 简单的三元交换 FItem_Struct TempItem = BackPackArray[DragIndex]; BackPackArray[DragIndex] = BackPackArray[DropIndex]; BackPackArray[DropIndex] = TempItem; // 交换完成更新GameInstance中的数组 MyGameInstance->BackPack_Array = BackPackArray; if (BackPackUI) { BackPackUI->BackPackTransfer = BackPackArray; BackPackUI->RefreshBackPack(); // 重新刷新所有格子 } } 

所有代码写完就可以到编辑器里编译保存了,接下来就可以测试了(这个我就不演示了)。

总结

其实代码量并不是很多,我起初也是根据蓝图逻辑去写的,然后对某些逻辑进行了一些修改,从头写还好,一旦代码堆积起来,其中的逻辑就不好串联了,所以我上边的代码都是按照逻辑顺序写的。其中的一些函数我也是第一次接触,可能对大多数初学者,或者只用蓝图写游戏的同学不是很友好,所以大家看的时候可以多对着敲敲,而不是复制粘贴。这个背包系统自己前前后后也花了一周时间一点点研究才写出来,因为期间用错函数,类参数传递还有各种奇葩问题,耽误好长时间,问ai,在网上找都没找到解决方案,还是看源码才发现问题。最后希望这个项目能够对大家有帮助吧,不管是学习或者是写进自己项目里,代码已经开源了,大家可以随时下载参考,有问题或者改进意见的话都可以在评论区交流。另外一些功能如果有时间我还会继续研究更新的。

Read more

Java IO 流进阶:Buffer 与 Channel 核心概念解析及与传统 IO 的本质区别

Java IO 流进阶:Buffer 与 Channel 核心概念解析及与传统 IO 的本质区别

在 Java IO 编程中,传统的字节流与字符流大家都不陌生,但当面对高并发、大文件处理等场景时,NIO(New IO)中的 Buffer 与 Channel 逐渐成为性能优化的关键。本文将深入剖析 Buffer 与 Channel 的核心概念,通过对比传统 IO 流,带你理解它们为何能显著提升 IO 效率,并配合直观的图示帮你建立清晰的认知。 一、传统 IO 流的局限性:为什么需要 Buffer/Channel?         在了解 Buffer 与 Channel 之前,我们先回顾传统 IO 流的工作方式。传统 IO 流分为字节流(InputStream/OutputStream) 和字符流(Reader/Writer)

By Ne0inhk
飞算JavaAI:人工智能与Java的创新融合与应用前景

飞算JavaAI:人工智能与Java的创新融合与应用前景

目录 引言 一、飞算JavaAI的背景与发展 二、飞算JavaAI的技术架构 1. 核心模块: 2. AI算法库: 3. 模型训练与调优: 4. 接口与集成: 三、飞算JavaAI的创新特点 1. 高效的数据处理能力: 2. 与Java生态的深度结合: 3. 自动化模型调优: 4. 可扩展性与灵活性: 四、真实体验—智能引导功能 五、飞算JavaAI的应用场景 1. 金融领域: 2. 智能制造: 3. 医疗健康: 4. 智能推荐系统: 六、飞算JavaAI面临的挑战 1. 计算资源要求: 2. 技术门槛: 3. 跨平台支持: 七、总结 正文开始—— 引言 随着人工智能(

By Ne0inhk
基于飞算JavaAI的在线图书借阅平台设计与实现(深度实践版)

基于飞算JavaAI的在线图书借阅平台设计与实现(深度实践版)

摘要: 本文以从概念到落地,完整构建一个“在线图书借阅平台”的全过程。文章不仅覆盖了环境配置、需求分析、接口设计、数据库建模等基础流程,更着重于展示AI自动生成的项目核心代码,并在此基础上进行了详尽的功能扩展和代码优化。通过对用户管理、图书管理、借阅与归还等关键业务模块的详细代码实现与注释,本文旨在全面、深入地展现飞算JavaAI在真实项目开发中的强大能力,探讨其如何重塑传统Java开发范式,显著提升开发效率与代码质量。 一、引言 在软件工程领域,随着业务逻辑的日益复杂化和市场对产品迭代速度的严苛要求,传统的纯手动编码模式正面临前所未有的挑战。开发周期长、人力成本高、代码质量参差不齐、技术债累积等问题,成为制约项目成功的重要因素。正是在这样的背景下,人工智能辅助编程(AI-Assisted Programming)应运而生,它通过将大型语言模型与软件工程知识深度融合,旨在自动化处理开发流程中的重复性、模式化任务,使开发者能够聚焦于更具创造性的核心业务逻辑。 飞算科技推出的飞算JavaAI,正是这一变革浪潮中的杰出代表。它作为一款深度集成于IntelliJ IDEA的智能插件,能够

By Ne0inhk