UE5多人MOBA+GAS 34、库存系统(一)

Source


添加背包唯一标识符句柄

InventoryItem中,创建一个拥有唯一标识ID的句柄

/**
 * 库存句柄,用于标识和管理库存中的物品实例
 */
USTRUCT()
struct FInventoryItemHandle
{
    
      
	GENERATED_BODY()
public:
	// 默认构造函数
	FInventoryItemHandle();
	
	// 获取无效句柄
	static FInventoryItemHandle InvalidHandle();
	
	// 创建新句柄
	static FInventoryItemHandle CreateHandle();

	// 检查句柄是否有效
	bool IsValid() const;
	
	// 获取句柄ID
	uint32 GetHandleId() const {
    
       return HandleId; }
	
private:
	// 使用指定ID构造句柄
	explicit FInventoryItemHandle(uint32 Id);

	// 句柄的唯一标识符
	UPROPERTY()
	uint32 HandleId;

	// 生成下一个句柄ID
	static uint32 GenerateNextId();
	
	// 获取无效ID值
	static uint32 GetInvalidId();
};

// 句柄相等运算符重载
bool operator==(const FInventoryItemHandle& Lhs, const FInventoryItemHandle& Rhs);

// 句柄哈希函数重载
uint32 GetTypeHash(const FInventoryItemHandle& Key);
FInventoryItemHandle::FInventoryItemHandle()
	: HandleId{
    
      GetInvalidId()}
{
    
      
}

FInventoryItemHandle FInventoryItemHandle::InvalidHandle()
{
    
      
	static FInventoryItemHandle InvalidHandle = FInventoryItemHandle();
	return InvalidHandle;
}

FInventoryItemHandle FInventoryItemHandle::CreateHandle()
{
    
      
	// 生成下一个ID的新句柄
	return FInventoryItemHandle(GenerateNextId());
}

bool FInventoryItemHandle::IsValid() const
{
    
      
	return HandleId != GetInvalidId();
}

FInventoryItemHandle::FInventoryItemHandle(uint32 Id)
	: HandleId{
    
      Id}
{
    
      
}

uint32 FInventoryItemHandle::GenerateNextId()
{
    
      
	static uint32 StaticId = 1; // 从1开始计数
	return StaticId++;
}

uint32 FInventoryItemHandle::GetInvalidId()
{
    
      
	// 无效值为0
	return 0;
}

bool operator==(const FInventoryItemHandle& Lhs, const FInventoryItemHandle& Rhs)
{
    
      
	// ID一样就一样
	return Lhs.GetHandleId() == Rhs.GetHandleId();
}

uint32 GetTypeHash(const FInventoryItemHandle& Key)
{
    
      
	return Key.GetHandleId();
}

给库存类添加初始化函数

/**
 * 库存物品类,表示玩家库存中的一个实际物品实例
 */
UCLASS()
class CRUNCH_API UInventoryItem : public UObject
{
    
      
	GENERATED_BODY()
public:
	/**
	 * 初始化物品实例
	 * @param NewHandle 分配给此物品的唯一句柄
	 * @param NewShopItem 关联的商店物品资产
	 * @param AbilitySystemComponent 拥有者的能力系统组件
	 */
	void InitItem(const FInventoryItemHandle& NewHandle, const UPDA_ShopItem* NewShopItem, UAbilitySystemComponent* AbilitySystemComponent);
	// 获取关联的商店物品资产
	const UPDA_ShopItem* GetShopItem() const {
    
       return ShopItem; }
	// 获取物品的唯一句柄
	FInventoryItemHandle GetHandle() const {
    
       return Handle; }
private:
	// 关联的商店物品资产
	UPROPERTY()
	const UPDA_ShopItem* ShopItem;
	// 物品的唯一句柄
	FInventoryItemHandle Handle;
};
void UInventoryItem::InitItem(const FInventoryItemHandle& NewHandle, const UPDA_ShopItem* NewShopItem,
	UAbilitySystemComponent* AbilitySystemComponent)
{
    
      
	Handle = NewHandle;				// 设置唯一句柄
	ShopItem = NewShopItem;			// 关联商店物品数据
}

将购买的物品存储起来

库存组件InventoryComponent中添加物品以及存储物品的Map,以及添加物品的委托

// 委托声明:当新物品添加到库存时广播
DECLARE_MULTICAST_DELEGATE_OneParam(FOnItemAddedDelegate, const UInventoryItem* /*NewItem*/);
public:
	// 物品添加事件委托
	FOnItemAddedDelegate OnItemAdded;
private:
	// 存储物品
	UPROPERTY()
	TMap<FInventoryItemHandle, UInventoryItem*> InventoryMap;
	/*********************************************************/
	/*                   Server RPCs                         */
	/*********************************************************/
	/** 向库存添加新物品 */
	void GrantItem(const UPDA_ShopItem* NewItem);
	/*********************************************************/
	/*                   Client                              */
	/*********************************************************/
private:
	UFUNCTION(Client, Reliable)
	void Client_ItemAdded(FInventoryItemHandle AssignedHandle, const UPDA_ShopItem* Item);

void UInventoryComponent::GrantItem(const UPDA_ShopItem* NewItem)
{
    
      
	if (!GetOwner()->HasAuthority()) return; // 确保服务器调用

	if (NewItem)
	{
    
      
		// 创建新物品
		UInventoryItem* InventoryItem = NewObject<UInventoryItem>();
		FInventoryItemHandle NewHandle = FInventoryItemHandle::CreateHandle();
		InventoryItem->InitItem(NewHandle, NewItem, OwnerAbilitySystemComponent);

		// 添加到库存中
		InventoryMap.Add(NewHandle, InventoryItem);
		OnItemAdded.Broadcast(InventoryItem);
		UE_LOG(LogTemp, Warning, TEXT("服务器中添加的物品: %s, 唯一ID: %d"), *(InventoryItem->GetShopItem()->GetItemName().ToString()), NewHandle.GetHandleId());

		// 通知客户端
		Client_ItemAdded(NewHandle, NewItem);
	}
}

void UInventoryComponent::Client_ItemAdded_Implementation(FInventoryItemHandle AssignedHandle,
                                                          const UPDA_ShopItem* Item)
{
    
      
	if (GetOwner()->HasAuthority()) return;// 确保客户端调用

	if (Item)
	{
    
      
		// 创建本地物品副本
		UInventoryItem* InventoryItem = NewObject<UInventoryItem>();
		InventoryItem->InitItem(AssignedHandle, Item, OwnerAbilitySystemComponent);

		// 添加到本地库存
		InventoryMap.Add(AssignedHandle, InventoryItem);
		OnItemAdded.Broadcast(InventoryItem);
		UE_LOG(LogTemp, Warning, TEXT("客户端中添加的物品: %s, 唯一ID: %d"), *(InventoryItem->GetShopItem()->GetItemName().ToString()), AssignedHandle.GetHandleId());
	}
}
void UInventoryComponent::Server_Purchase_Implementation(const UPDA_ShopItem* ItemToPurchase)
{
    
      
	if (!ItemToPurchase) return;

	// 金币不够无法购买
	if (GetGold() < ItemToPurchase->GetPrice()) return;

	// 扣掉金币
	OwnerAbilitySystemComponent->ApplyModToAttribute(UCHeroAttributeSet::GetGoldAttribute(), EGameplayModOp::Additive, -ItemToPurchase->GetPrice());
	// 添加物品
	GrantItem(ItemToPurchase);
}

在这里插入图片描述

给物品应用装备效果并赋予能力

	// 应用游戏能力系统修改(效果和能力)
	void ApplyGASModifications();

	// 拥有者的能力系统组件
	UPROPERTY()
	TObjectPtr<UAbilitySystemComponent> OwnerAbilitySystemComponent;
	

	// 应用的装备效果句柄
	FActiveGameplayEffectHandle AppliedEquipedEffectHandle;
	
	// 授予的能力规格句柄
	FGameplayAbilitySpecHandle GrantedAbiltiySpecHandle;
void UInventoryItem::InitItem(const FInventoryItemHandle& NewHandle, const UPDA_ShopItem* NewShopItem,
	UAbilitySystemComponent* AbilitySystemComponent)
{
    
      
	Handle = NewHandle;				// 设置唯一句柄
	ShopItem = NewShopItem;			// 关联商店物品数据

	// 设置能力系统组件并绑定属性变化委托
	OwnerAbilitySystemComponent = AbilitySystemComponent;

	// 应用GAS修改
	ApplyGASModifications();
}
void UInventoryItem::ApplyGASModifications()
{
    
      
	if (!GetShopItem() || !OwnerAbilitySystemComponent) return;
	if (!OwnerAbilitySystemComponent->GetOwner()->HasAuthority()) return;

	// 应用装备效果
	TSubclassOf<UGameplayEffect> EquipEffect = GetShopItem()->GetEquippedEffect();
	if (EquipEffect)
	{
    
      
		AppliedEquipedEffectHandle = OwnerAbilitySystemComponent->BP_ApplyGameplayEffectToSelf(
			EquipEffect,
			1,
			OwnerAbilitySystemComponent->MakeEffectContext()
			);
	}

	// 授予装备技能
	TSubclassOf<UGameplayAbility> GrantedAbility = GetShopItem()->GetGrantedAbility();
	if (GrantedAbility)
	{
    
      
		GrantedAbiltiySpecHandle = OwnerAbilitySystemComponent->GiveAbility(
			FGameplayAbilitySpec(GrantedAbility)
			);
	}
}

创建一个无限GE来作为装备效果
在这里插入图片描述
创建一个测试技能
在这里插入图片描述
在这里插入图片描述
数据表中设置一下
在这里插入图片描述
买了两双鞋移速加200
在这里插入图片描述
在这里插入图片描述

创建库存UI

装备格子

InventoryItemWidget
在这里插入图片描述

	UInventoryItem();
	bool IsValid() const;

	// 获取当前堆叠数量
	FORCEINLINE int32 GetStackCount() const {
    
       return StackCount; }
	
	// 设置物品在库存中的槽位
	void SetSlot(int32 NewSlot);
	
	// 获取物品在库存中的槽位
	int32 GetItemSlot() const {
    
       return Slot; }

private:
	// 当前堆叠数量
	int32 StackCount;
	
	// 在库存中的槽位索引
	int32 Slot;
UInventoryItem::UInventoryItem()
	: StackCount{
    
      1} // 默认堆叠数为1
{
    
      
}

bool UInventoryItem::IsValid() const
{
    
      
	return ShopItem != nullptr;
}

void UInventoryItem::SetSlot(int NewSlot)
{
    
      
	Slot = NewSlot;
}
#pragma once

#include "CoreMinimal.h"
#include "Inventory/InventoryItem.h"
#include "UI/Common/ItemWidget.h"
#include "InventoryItemWidget.generated.h"

/**
 * 
 */
UCLASS()
class CRUNCH_API UInventoryItemWidget : public UItemWidget
{
    
      
	GENERATED_BODY()
public:
	virtual void NativeConstruct() override;
	// 检查槽位是否为空
	bool IsEmpty() const;
	// 设置槽位编号
	void SetSlotNumber(int NewSlotNumber);
	// 更新UI显示指定物品
	void UpdateInventoryItem(const UInventoryItem* Item);
	// 清空槽位
	void EmptySlot();
	// 获取槽位编号
	FORCEINLINE int GetSlotNumber() const {
    
       return SlotNumber; }
	// 更新堆叠数量显示
	void UpdateStackCount();
private:
	// 空槽位时显示的默认纹理
	UPROPERTY(EditDefaultsOnly, Category = "Visual")
	TObjectPtr<UTexture2D> EmptyTexture;
	// UI绑定:堆叠数量文本
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UTextBlock> StackCountText;

	// UI绑定:冷却倒计时文本
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UTextBlock> CooldownCountText;

	// UI绑定:冷却总时间文本
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UTextBlock> CooldownDurationText;

	// UI绑定:法力消耗文本
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UTextBlock> ManaCostText;
	
	// 当前显示的库存物品
	UPROPERTY()
	const UInventoryItem* InventoryItem;
	// 当前槽位编号
	int32 SlotNumber;
};
#include "InventoryItemWidget.h"

void UInventoryItemWidget::NativeConstruct()
{
    
      
	Super::NativeConstruct();
	EmptySlot();
}

bool UInventoryItemWidget::IsEmpty() const
{
    
      
	return !InventoryItem || !(InventoryItem->IsValid());
}

void UInventoryItemWidget::SetSlotNumber(int NewSlotNumber)
{
    
      
	SlotNumber = NewSlotNumber;
}

void UInventoryItemWidget::UpdateInventoryItem(const UInventoryItem* Item)
{
    
      
	InventoryItem = Item;
	// 如果物品无效或数量为0,清空槽位
	if (!InventoryItem || !InventoryItem->IsValid() || InventoryItem->GetStackCount() == 0)
	{
    
      
		EmptySlot();
		return;
	}
	// 设置图标
	SetIcon(InventoryItem->GetShopItem()->GetIcon());
	// 创建提示信息
	UItemToolTip* ToolTip = SetToolTipWidget(InventoryItem->GetShopItem());
	if (ToolTip)
	{
    
      
		ToolTip->SetPrice(InventoryItem->GetShopItem()->GetSellPrice());
	}

	// 处理可堆叠物品的显示逻辑
	if (InventoryItem->GetShopItem()->GetIsStackable())
	{
    
      
		StackCountText->SetVisibility(ESlateVisibility::Visible);
		UpdateStackCount();
	}
	else
	{
    
      
		StackCountText->SetVisibility(ESlateVisibility::Hidden);
	}
	
}

void UInventoryItemWidget::EmptySlot()
{
    
      
	// 清空物品
	InventoryItem = nullptr;
	SetIcon(EmptyTexture);
	SetToolTip(nullptr);

	// 隐藏所有相关文本组件
	StackCountText->SetVisibility(ESlateVisibility::Hidden);
	ManaCostText->SetVisibility(ESlateVisibility::Hidden);
	CooldownCountText->SetVisibility(ESlateVisibility::Hidden);
	CooldownDurationText->SetVisibility(ESlateVisibility::Hidden);
}

void UInventoryItemWidget::UpdateStackCount()
{
    
      
	if (InventoryItem)
	{
    
      
		// 将堆叠数量转换为文本显示
		StackCountText->SetText(FText::AsNumber(InventoryItem->GetStackCount()));
	}
}

在这里插入图片描述
在这里插入图片描述

放装备格子的

库存组件中添加背包的格子数量以及获取格子数和插槽变换和通过句柄获取物品的函数

	// 获取库存容量
	FORCEINLINE int32 GetCapacity() const {
    
       return Capacity; }
	// 处理物品槽位变更
	void ItemSlotChanged(const FInventoryItemHandle& Handle, int32 NewSlotNumber);
	// 通过句柄获取库存物品
	UInventoryItem* GetInventoryItemByHandle(const FInventoryItemHandle& Handle) const;


	/** 库存容量(槽位数) */
	UPROPERTY(EditDefaultsOnly, Category = "Inventory")
	int32 Capacity = 6;
void UInventoryComponent::ItemSlotChanged(const FInventoryItemHandle& Handle, int32 NewSlotNumber)
{
    
      
	// 通过句柄查找物品,并为其设置新的插槽
	if (UInventoryItem* FoundItem = GetInventoryItemByHandle( Handle))
	{
    
      
		FoundItem->SetSlot(NewSlotNumber);
	}
}

UInventoryItem* UInventoryComponent::GetInventoryItemByHandle(const FInventoryItemHandle& Handle) const
{
    
      
	// 通过句柄在Map中查找
	UInventoryItem* const* FoundItem = InventoryMap.Find(Handle);
	if (FoundItem)
	{
    
      
		return *FoundItem;
	}
	return nullptr;
}

void UInventoryComponent::Server_Purchase_Implementation(const UPDA_ShopItem* ItemToPurchase)
{
    
      
	if (!ItemToPurchase) return;

	// 金币不够无法购买
	if (GetGold() < ItemToPurchase->GetPrice()) return;
	// 背包不够也不能购买
	if (InventoryMap.Num() >= GetCapacity()) return;

	// 扣掉金币
	OwnerAbilitySystemComponent->ApplyModToAttribute(UCHeroAttributeSet::GetGoldAttribute(), EGameplayModOp::Additive, -ItemToPurchase->GetPrice());
	// 添加物品
	GrantItem(ItemToPurchase);
}

InventoryWidget
在这里插入图片描述

#pragma once

#include "CoreMinimal.h"
#include "InventoryItemWidget.h"
#include "Blueprint/UserWidget.h"
#include "Components/WrapBox.h"
#include "Inventory/InventoryComponent.h"
#include "InventoryWidget.generated.h"

/**
 * 
 */
UCLASS()
class CRUNCH_API UInventoryWidget : public UUserWidget
{
    
      
	GENERATED_BODY()
public:
	// 控件初始化
	virtual void NativeConstruct() override;
	
private:
	// UI绑定:物品列表容器(使用WrapBox自动布局)
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UWrapBox> ItemList;

	// 单个物品控件类
	UPROPERTY(EditDefaultsOnly, Category = "Inventory")
	TSubclassOf<UInventoryItemWidget> ItemWidgetClass;

	// 关联的库存组件
	UPROPERTY()
	TObjectPtr<UInventoryComponent> InventoryComponent;

	// 所有物品控件实例数组
	UPROPERTY()
	TArray<TObjectPtr<UInventoryItemWidget>> ItemWidgets;
	
	// 物品句柄到控件的映射
	UPROPERTY()
	TMap<FInventoryItemHandle, TObjectPtr<UInventoryItemWidget>> PopulatedItemEntryWidgets;

	// 处理物品添加事件
	void ItemAdded(const UInventoryItem* InventoryItem);

	// 获取下一个可用槽位控件
	UInventoryItemWidget* GetNextAvailableSlot() const;
};
#include "InventoryWidget.h"

#include "Components/WrapBoxSlot.h"

void UInventoryWidget::NativeConstruct()
{
    
      
	Super::NativeConstruct();
	if (APawn* OwnerPawn = GetOwningPlayerPawn())
	{
    
      
		// 获取背包组件
		InventoryComponent = OwnerPawn->GetComponentByClass<UInventoryComponent>();
		if (InventoryComponent)
		{
    
      
			// 购买物品事件绑定
			InventoryComponent->OnItemAdded.AddUObject(this, &UInventoryWidget::ItemAdded);

			// 获取背包容量
			int32 Capacity = InventoryComponent->GetCapacity();
			// 清空背包
			ItemList->ClearChildren();
			ItemWidgets.Empty();
			for (int32 i = 0; i < Capacity; ++i)
			{
    
      
				UInventoryItemWidget* NewEmptyWidget = CreateWidget<UInventoryItemWidget>(GetOwningPlayer(), ItemWidgetClass);
				if (NewEmptyWidget)
				{
    
      
					NewEmptyWidget->SetSlotNumber(i);		//设置槽位编号
					// 添加到WarpBox
					UWrapBoxSlot* NewItemSlot = ItemList->AddChildToWrapBox(NewEmptyWidget);
					NewItemSlot->SetPadding(FMargin(2.f));	// 间隔
					ItemWidgets.Add(NewEmptyWidget);		// 添加到控件数组
					
				}
			}
		}
	}
}

void UInventoryWidget::ItemAdded(const UInventoryItem* InventoryItem)
{
    
      
	if (!InventoryItem) return;

	// 获取下一个可用槽位
	if (UInventoryItemWidget* NextAvailableSlot = GetNextAvailableSlot())
	{
    
      
		// 添加物品,将改槽位的显示刷新
		NextAvailableSlot->UpdateInventoryItem(InventoryItem);
		// 将槽位放入map
		PopulatedItemEntryWidgets.Add(InventoryItem->GetHandle(), NextAvailableSlot);

		// 通知库存组件槽位变化
		if (InventoryComponent)
		{
    
      
			InventoryComponent->ItemSlotChanged(
				InventoryItem->GetHandle(), 
				NextAvailableSlot->GetSlotNumber()
			);
		}
	}
}

UInventoryItemWidget* UInventoryWidget::GetNextAvailableSlot() const
{
    
      
	// 遍历寻找空位
	for (UInventoryItemWidget* ItemWidget: ItemWidgets)
	{
    
      
		if (ItemWidget->IsEmpty())
		{
    
      
			return ItemWidget;
		}
	}
	// 没有空位返回空
	return nullptr;
}

显示装备背包

GameplayWidget中添加背包UI

	// 背包UI
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UInventoryWidget> InventoryWidget;

继承该库存,放入一个尺寸框还有包裹框
在这里插入图片描述
并添加一下这个装备UI
在这里插入图片描述
随后把这个wbp塞进Gameplay的wbp中去,加个垂直框把金币也移过去
在这里插入图片描述
然后就可以购买商品了
在这里插入图片描述