UE5多人MOBA+GAS 36、库存系统(三)

文章目录


本文中实现了装备物品可以通过鼠标左键的点击使用消耗品以及可以激活技能,另外添加了一个UI附带两个按钮,可以通过右键点击库存中的装备弹出UI,对装备物品进行出售以及使用。

激活技能和使用消耗品

商店物品InventoryItem中添加激活技能和应用消耗以及移除装备后移除应用的ge以及ga的函数

cpp 复制代码
	// 尝试激活物品授予的能力
	bool TryActivateGrantedAbility();
	
	// 应用物品的消耗效果
	void ApplyConsumeEffect();
	
	// 移除所有应用的游戏能力系统修改
	void RemoveGASModifications();
cpp 复制代码
bool UInventoryItem::TryActivateGrantedAbility()
{
	if (!GrantedAbilitySpecHandle.IsValid()) return false;
	if (!OwnerAbilitySystemComponent) return false;

	// 激活技能,成功返回true
	return OwnerAbilitySystemComponent->TryActivateAbility(GrantedAbilitySpecHandle);
}

void UInventoryItem::ApplyConsumeEffect()
{
	if (!ShopItem) return;

	TSubclassOf<UGameplayEffect> ConsumeEffect = GetShopItem()->GetConsumeEffect();
	if (ConsumeEffect) return;
	
	// 应用消耗效果
	OwnerAbilitySystemComponent->BP_ApplyGameplayEffectToSelf(
		ConsumeEffect,
		1,
		OwnerAbilitySystemComponent->MakeEffectContext()
		);
}

void UInventoryItem::RemoveGASModifications()
{
	if (!OwnerAbilitySystemComponent) return;

	// 服务器端执行
	if (OwnerAbilitySystemComponent->GetOwner()->HasAuthority())
	{
		// 移除装备效果
		if (AppliedEquipedEffectHandle.IsValid())
		{
			OwnerAbilitySystemComponent->RemoveActiveGameplayEffect(AppliedEquipedEffectHandle);
		}

		// 移除技能
		if (GrantedAbilitySpecHandle.IsValid())
		{
			OwnerAbilitySystemComponent->SetRemoveAbilityOnEnd(GrantedAbilitySpecHandle);
		}
	}
}

UInventoryItemWidget文件中添加新的委托,用来绑定鼠标的点击事件

cpp 复制代码
// 定义委托:当按钮点击时触发
DECLARE_MULTICAST_DELEGATE_OneParam(
    FOnButtonClick, 
    const FInventoryItemHandle& /* 物品句柄 */
);

添加委托,并重载鼠标点击的函数

cpp 复制代码
	// 委托:当左键点击此物品时触发
	FOnButtonClick OnLeftButtonClicked;
	
	// 委托:当右键点击此物品时触发
	FOnButtonClick OnRightButtonClicked;

	// 右键点击事件处理
	virtual void RightButtonClicked() override;
	
	// 左键点击事件处理
	virtual void LeftButtonClicked() override;

为两个重载的点击函数广播两个委托

cpp 复制代码
void UInventoryItemWidget::RightButtonClicked()
{
	if (!IsEmpty())
		OnRightButtonClicked.Broadcast(GetItemHandle()); // 广播右键点击事件
}

void UInventoryItemWidget::LeftButtonClicked()
{
	if (!IsEmpty())
		OnLeftButtonClicked.Broadcast(GetItemHandle()); // 广播左键点击事件
}

在库存组件中调用这些函数

cpp 复制代码
// 委托声明:当物品从库存移除时广播
DECLARE_MULTICAST_DELEGATE_OneParam(FOnItemRemovedDelegate, const FInventoryItemHandle& /*ItemHandle*/);
cpp 复制代码
	// 物品移除事件委托
	FOnItemRemovedDelegate OnItemRemoved;
	
	// 尝试激活指定句柄对应的物品
	void TryActivateItem(const FInventoryItemHandle& ItemHandle);

	/** 服务器端:处理物品激活请求 */
	UFUNCTION(Server, Reliable, WithValidation)
	void Server_ActivateItem(FInventoryItemHandle ItemHandle);

	/** 消耗物品(减少堆叠或移除) */
	void ConsumeItem(UInventoryItem* Item);
	
	/** 从库存完全移除物品 */
	void RemoveItem(UInventoryItem* Item);
	
	/** 客户端:处理物品移除通知 */
	UFUNCTION(Client, Reliable)
	void Client_ItemRemoved(FInventoryItemHandle ItemHandle);
cpp 复制代码
void UInventoryComponent::TryActivateItem(const FInventoryItemHandle& ItemHandle)
{
	// 查找物品
	UInventoryItem* InventoryItem = GetInventoryItemByHandle(ItemHandle);
	if (!InventoryItem) return;
	
	// 服务器中激活物品
	Server_ActivateItem(ItemHandle);
}

void UInventoryComponent::Server_ActivateItem_Implementation(FInventoryItemHandle ItemHandle)
{
	UInventoryItem* InventoryItem = GetInventoryItemByHandle(ItemHandle);
	if (!InventoryItem) return;

	// 激活物品的技能
	InventoryItem->TryActivateGrantedAbility();
	const UPDA_ShopItem* Item = InventoryItem->GetShopItem();
	// 如果是消耗品则消耗
	if (Item->GetIsConsumable())
	{
		ConsumeItem(InventoryItem);
	}
}

bool UInventoryComponent::Server_ActivateItem_Validate(FInventoryItemHandle ItemHandle)
{
	return true;
}

void UInventoryComponent::ConsumeItem(UInventoryItem* Item)
{
	// 只在服务器中执行
	if (!GetOwner()->HasAuthority()) return;
	if (!Item) return;
	// 应用消耗
	Item->ApplyConsumeEffect();
	// 减少物品堆叠一次, 如果物品堆叠数量为0则移除物品
	if (!Item->ReduceStackCount())
	{
		RemoveItem(Item);
	}else
	{
		// 广播物品堆叠变化
		OnItemStackCountChanged.Broadcast(Item->GetHandle(), Item->GetStackCount());
		// 通知客户端物品堆叠变化
		Client_ItemStackCountChanged(Item->GetHandle(), Item->GetStackCount());
	}
}

void UInventoryComponent::RemoveItem(UInventoryItem* Item)
{
	if (!GetOwner()->HasAuthority()) return;

	// 移除GAS的效果
	Item->RemoveGASModifications();
	OnItemRemoved.Broadcast(Item->GetHandle());
	// 从背包中移除物品
	InventoryMap.Remove(Item->GetHandle());
	Client_ItemRemoved(Item->GetHandle());
}

void UInventoryComponent::Client_ItemRemoved_Implementation(FInventoryItemHandle ItemHandle)
{
	// 客户端移除物品
	if (GetOwner()->HasAuthority()) return;

	UInventoryItem* InventoryItem = GetInventoryItemByHandle(ItemHandle);
	if (!InventoryItem) return;
	// 移除GAS效果
	InventoryItem->RemoveGASModifications();
	OnItemRemoved.Broadcast(ItemHandle);
	// 从背包中移除物品
	InventoryMap.Remove(ItemHandle);
}

到库存UI中实现处理移除物品的函数

cpp 复制代码
	// 处理物品移除事件
	void ItemRemoved(const FInventoryItemHandle& ItemHandle);
cpp 复制代码
void UInventoryWidget::NativeConstruct()
{
	Super::NativeConstruct();
	if (APawn* OwnerPawn = GetOwningPlayerPawn())
	{
		// 获取背包组件
		InventoryComponent = OwnerPawn->GetComponentByClass<UInventoryComponent>();
		if (InventoryComponent)
		{
			// 购买物品事件绑定
			InventoryComponent->OnItemAdded.AddUObject(this, &UInventoryWidget::ItemAdded);
			// 背包物品堆叠数量改变事件绑定
			InventoryComponent->OnItemStackCountChanged.AddUObject(this, &UInventoryWidget::ItemStackCountChanged);
			// 移除物品事件绑定
			InventoryComponent->OnItemRemoved.AddUObject(this, &UInventoryWidget::ItemRemoved);
			
			// 获取背包容量
			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);		// 添加到控件数组

					// 绑定拖拽放置事件
					NewEmptyWidget->OnInventoryItemDropped.AddUObject(this, &UInventoryWidget::HandleItemDragDrop);
					// 绑定左键点击事件,点击时调用 InventoryComponent 的 TryActivateItem(尝试使用物品)
					NewEmptyWidget->OnLeftButtonClicked.AddUObject(
						InventoryComponent, &UInventoryComponent::TryActivateItem
					);
				}
			}
		}
	}
}
void UInventoryWidget::ItemRemoved(const FInventoryItemHandle& ItemHandle)
{
	// 查找对应的物品
	if (TObjectPtr<UInventoryItemWidget>* FoundWidget = PopulatedItemEntryWidgets.Find(ItemHandle))
	{
		if (*FoundWidget)
		{
			// 清空槽位显示
			(*FoundWidget)->EmptySlot();
			
			// 从映射表中移除
			PopulatedItemEntryWidgets.Remove(ItemHandle);
		}
	}
}

创建一个实时的药水GE

放置在使用效果

被打后可以嗑药补血

点击一下鞋子也可以激活该鞋子的技能

添加一个新的ui弄两个按钮用来使用物品以及出售物品

InventoryContextMenuWidget

cpp 复制代码
#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/Button.h"
#include "InventoryContextMenuWidget.generated.h"

/**
 * 上下文菜单 Widget,用于在物品格子上右键点击时显示"使用"和"出售"按钮
 */
UCLASS()
class UInventoryContextMenuWidget : public UUserWidget
{
	GENERATED_BODY()

public:
	/** 获取"使用"按钮的点击事件引用,外部绑定时使用 */
	FOnButtonClickedEvent& GetUseButtonClickedEvent() const;
	/** 获取"出售"按钮的点击事件引用,外部绑定时使用 */
	FOnButtonClickedEvent& GetSellButtonClickedEvent() const;
private:
	/** 绑定到 UMG 编辑器中名为 UseButton 的按钮,用于"使用"物品 */
	UPROPERTY(meta = (BindWidget))
	UButton* UseButton;

	/** 绑定到 UMG 编辑器中名为 SellButton 的按钮,用于"出售"物品 */
	UPROPERTY(meta = (BindWidget))
	UButton* SellButton;
};
cpp 复制代码
#include "InventoryContextMenuWidget.h"

FOnButtonClickedEvent& UInventoryContextMenuWidget::GetUseButtonClickedEvent() const
{
	return UseButton->OnClicked;
}

FOnButtonClickedEvent& UInventoryContextMenuWidget::GetSellButtonClickedEvent() const
{
	return SellButton->OnClicked;
}

到库存UIUInventoryWidget中添加这个上下文

cpp 复制代码
public:
	// 焦点变化事件处理
	virtual void NativeOnFocusChanging(
		const FWeakWidgetPath& PreviousFocusPath, 
		const FWidgetPath& NewWidgetPath, 
		const FFocusEvent& InFocusEvent
	) override;
private:
	// 上下文菜单控件类(内含使用和售出两个按钮)
	UPROPERTY(EditDefaultsOnly, Category = "Inventory")
	TSubclassOf<UInventoryContextMenuWidget> ContextMenuWidgetClass;

	// 当前显示的上下文菜单实例
	UPROPERTY()
	TObjectPtr<UInventoryContextMenuWidget> ContextMenuWidget;

	// 创建上下文菜单
	void SpawnContextMenu();
	
	// 出售当前焦点物品
	UFUNCTION()
	void SellFocusedItem();
	
	// 使用当前焦点物品
	UFUNCTION()
	void UseFocusedItem();

	// 设置上下文菜单可见性
	void SetContextMenuVisible(bool bContextMenuVisible);
	
	// 切换上下文菜单显示状态
	void ToggleContextMenu(const FInventoryItemHandle& ItemHandle);
	
	// 清除上下文菜单
	void ClearContextMenu();
	
	// 当前焦点物品句柄
	FInventoryItemHandle CurrentFocusedItemHandle;

绑定右键打开这个菜单

cpp 复制代码
void UInventoryWidget::NativeConstruct()
{
	Super::NativeConstruct();
	if (APawn* OwnerPawn = GetOwningPlayerPawn())
	{
		// 获取背包组件
		InventoryComponent = OwnerPawn->GetComponentByClass<UInventoryComponent>();
		if (InventoryComponent)
		{
			// 购买物品事件绑定
			InventoryComponent->OnItemAdded.AddUObject(this, &UInventoryWidget::ItemAdded);
			// 背包物品堆叠数量改变事件绑定
			InventoryComponent->OnItemStackCountChanged.AddUObject(this, &UInventoryWidget::ItemStackCountChanged);
			// 移除物品事件绑定
			InventoryComponent->OnItemRemoved.AddUObject(this, &UInventoryWidget::ItemRemoved);
			
			// 获取背包容量
			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);		// 添加到控件数组

					// 绑定拖拽放置事件
					NewEmptyWidget->OnInventoryItemDropped.AddUObject(this, &UInventoryWidget::HandleItemDragDrop);
					// 绑定左键点击事件,点击时调用 InventoryComponent 的 TryActivateItem(尝试使用物品)
					NewEmptyWidget->OnLeftButtonClicked.AddUObject(
						InventoryComponent, &UInventoryComponent::TryActivateItem
					);
					// 绑定右键点击事件,点击时在此 Widget 中切换显示上下文菜单
					NewEmptyWidget->OnRightButtonClicked.AddUObject(
						this, &UInventoryWidget::ToggleContextMenu
					);
				}
			}
			// 在界面中生成一个上下文菜单(初始状态为隐藏)
			SpawnContextMenu();
		}
	}
}
void UInventoryWidget::NativeOnFocusChanging(const FWeakWidgetPath& PreviousFocusPath, const FWidgetPath& NewWidgetPath,
	const FFocusEvent& InFocusEvent)
{
	Super::NativeOnFocusChanging(PreviousFocusPath, NewWidgetPath, InFocusEvent);

	// 如果焦点不在上下文菜单上,关闭菜单(使用和售出的按钮)
	if (!NewWidgetPath.ContainsWidget(ContextMenuWidget->GetCachedWidget().Get()))
	{
		ClearContextMenu();
	}
}

void UInventoryWidget::SpawnContextMenu()
{
	if (!ContextMenuWidgetClass) return;
	
	ContextMenuWidget = CreateWidget<UInventoryContextMenuWidget>(this, ContextMenuWidgetClass);
	if (ContextMenuWidget)
	{
		// 绑定使用按钮和售出按钮
		ContextMenuWidget->GetUseButtonClickedEvent().AddDynamic(this, &UInventoryWidget::UseFocusedItem);
		ContextMenuWidget->GetSellButtonClickedEvent().AddDynamic(this, &UInventoryWidget::SellFocusedItem);

		// 将上下文菜单添加到视口,设置层级为 1,以确保浮于其他 UI 之上
		ContextMenuWidget->AddToViewport(1);
		// 初始时将上下文菜单隐藏
		SetContextMenuVisible(false);
	}
}

void UInventoryWidget::SellFocusedItem()
{
	// 通知库存组件出售物品
	InventoryComponent->SellItem(CurrentFocusedItemHandle);
	SetContextMenuVisible(false); // 关闭菜单
}

void UInventoryWidget::UseFocusedItem()
{
	// 通知库存组件激活物品
	InventoryComponent->TryActivateItem(CurrentFocusedItemHandle);
	SetContextMenuVisible(false); // 关闭菜单
}

void UInventoryWidget::SetContextMenuVisible(bool bContextMenuVisible)
{
	if (ContextMenuWidget)
	{
		ContextMenuWidget->SetVisibility(
			bContextMenuVisible ? 
			ESlateVisibility::Visible : 
			ESlateVisibility::Hidden
		);
	}
}

void UInventoryWidget::ToggleContextMenu(const FInventoryItemHandle& ItemHandle)
{
	// 如果点击的是当前焦点物品,则关闭菜单
	if (CurrentFocusedItemHandle == ItemHandle)
	{
		ClearContextMenu();
		return;
	}

	// 设置新的焦点物品
	CurrentFocusedItemHandle = ItemHandle;
	
	// 查找对应的物品控件
	TObjectPtr<UInventoryItemWidget>* ItemWidgetPtrPtr = PopulatedItemEntryWidgets.Find(ItemHandle);
	if (!ItemWidgetPtrPtr) return;
	
	UInventoryItemWidget* ItemWidget = *ItemWidgetPtrPtr;
	if (!ItemWidget) return;

	// 显示菜单
	SetContextMenuVisible(true);

	// 计算菜单位置(物品右侧中心点)
	FVector2D ItemAbsPos = ItemWidget->GetCachedGeometry().GetAbsolutePositionAtCoordinates(FVector2D{1.f, 0.5f});

	// 转换为视口坐标
	FVector2D ItemWidgetPixelPos, ItemWidgetViewportPos;
	// 将绝对屏幕坐标转换为视口坐标系中的位置。
	USlateBlueprintLibrary::AbsoluteToViewport(this, ItemAbsPos, ItemWidgetPixelPos, ItemWidgetViewportPos);

	// 获取玩家控制器,以便查询视口尺寸,防止菜单被拉出屏幕
	if (APlayerController* OwningPlayerController = GetOwningPlayer())
	{
		int32 ViewportSizeX, ViewportSizeY;
		OwningPlayerController->GetViewportSize(ViewportSizeX, ViewportSizeY);
		// 获取当前 UI 缩放比例(DPI 缩放)
		float Scale = UWidgetLayoutLibrary::GetViewportScale(this);

		// 计算菜单底部是否超出屏幕下缘:
		// Overshoot = (菜单顶部Y坐标 + 菜单高度 * 缩放) - 屏幕高度
		float MenuHeightScaled = ContextMenuWidget->GetDesiredSize().Y * Scale;
		float Overshoot = ItemWidgetPixelPos.Y + MenuHeightScaled - ViewportSizeY;
		
		// 如果超出,则将菜单向上移动 Overshoot 像素,保证完全可见
		if (Overshoot > 0.f)
		{
			ItemWidgetPixelPos.Y -= Overshoot;
		}
	}

	// 设置菜单位置
	ContextMenuWidget->SetPositionInViewport(ItemWidgetPixelPos);
}

void UInventoryWidget::ClearContextMenu()
{
	SetContextMenuVisible(false);
	CurrentFocusedItemHandle = FInventoryItemHandle::InvalidHandle(); // 重置焦点物品
}

到库存组件中补充出售物品的处理

cpp 复制代码
public:
	// 出售指定物品
	void SellItem(const FInventoryItemHandle& ItemHandle);
private:
	/** 服务器端:处理物品出售请求 */
	UFUNCTION(Server, Reliable, WithValidation)
	void Server_SellItem(FInventoryItemHandle ItemHandle);
cpp 复制代码
void UInventoryComponent::SellItem(const FInventoryItemHandle& ItemHandle)
{
	// 请求服务器出售物品
	Server_SellItem(ItemHandle);
}

void UInventoryComponent::Server_SellItem_Implementation(FInventoryItemHandle ItemHandle)
{
	// 服务端出售物品
	UInventoryItem* InventoryItem = GetInventoryItemByHandle(ItemHandle);
	if (!InventoryItem || !InventoryItem->IsValid()) return;
	if (!OwnerAbilitySystemComponent) return;

	// 获取售出价格,并给玩家添加金币
	float SellPrice = InventoryItem->GetShopItem()->GetSellPrice();
	OwnerAbilitySystemComponent->ApplyModToAttribute(UCHeroAttributeSet::GetGoldAttribute(), EGameplayModOp::Additive, SellPrice * InventoryItem->GetStackCount());
	// 移除物品
	RemoveItem(InventoryItem);
}

bool UInventoryComponent::Server_SellItem_Validate(FInventoryItemHandle ItemHandle)
{
	return true;
}

调用移除的时候也会通知客户端

添加蓝图版本

添加一个垂直框还有两个按钮,每个按钮放个文字

修改命名,添加填充

添加个尺寸框重载高度

添加个边界,设置笔刷颜色可以调色

库存UI中设置一下这个类

买了四双鞋子破产了

卖一双赚50