零、资料
插件地址:
https://www.unrealengine.com/marketplace/zh-CN/product/arc-inventory
文档地址:
https://redirect.epicgames.com/?redirectTo=https://punyhumangames.github.io/ArcPluginsDocumentation/arcinventory/
一、UArcInventoryComponent
1.1 主要属性
FArcInventoryItemSlotArray BagInventory;
TArray<FArcInventoryItemSlotReference> AllReferences;
确定了InventoryComp的核心:背包
1.1.1 BagInventory
这是存储所有库存的地方,只具有储存的同步功能
结构体定义:
USTRUCT()
struct FArcInventoryItemSlotArray : public FFastArraySerializer
{
GENERATED_USTRUCT_BODY()
public:
UPROPERTY()
TArray< FArcInventoryItemSlot> Slots;
UPROPERTY()
UArcInventoryComponent* Owner;
bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
{
return FFastArraySerializer::FastArrayDeltaSerialize<FArcInventoryItemSlot, FArcInventoryItemSlotArray>(Slots, DeltaParms, *this);
}
};
主要负责存储插槽数组,并使用了FFastArraySerializer
来优化结构体数组的同步问题
FFastArraySerializer资料:
官方文档
https://docs.unrealengine.com/4.27/en-US/API/Runtime/Engine/Engine/FFastArraySerializer/
UE4Guide
https://ikrima.dev/ue4guide/networking/network-replication/fast-tarray-replication/
1.1.2、AllReferences
作用:
是外部获取Slot的Handle,包含了所有BagInventory里Slot的引用
//仅仅是主要结构
USTRUCT(BlueprintType)
struct ARCINVENTORY_API FArcInventoryItemSlotReference
{
GENERATED_USTRUCT_BODY()
public:
//...不同类型的构造函数
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Inventory)
int32 SlotId;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = Inventory)
FGameplayTagContainer SlotTags;
UPROPERTY(BlueprintReadWrite, VisibleInstanceOnly, Category = Inventory)
class UArcInventoryComponent* ParentInventory;
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
bool operator==(const FArcInventoryItemSlotReference& Other) const;
bool operator==(const FArcInventoryItemSlot& Other) const;
};
实现
通过记录Inventory指针+索引Id来记录Slot引用,方便网络复制以及查找
相应地,提供==重载简化操作
1.2、主要接口
核心函数望文生义:背包基本的增删改查操作
//增
UFUNCTION(BlueprintCallable, Category="Arc|Inventory")
virtual bool LootItem(UArcItemStack* Item);
UFUNCTION(BlueprintCallable, Category = "Arc|Inventory")
virtual bool PlaceItemIntoSlot(UArcItemStack* Item, const FArcInventoryItemSlotReference& ItemSlot);
//删
UFUNCTION(BlueprintCallable, Category = "Arc|Inventory")
virtual bool RemoveItemFromInventory(const FArcInventoryItemSlotReference& ItemSlot);
UFUNCTION(BlueprintCallable, Category = "Arc|Inventory")
virtual bool RemoveAllItemsFromInventory(TArray<UArcItemStack*>& OutItemsRemoved);
//改(交换)
UFUNCTION(BlueprintCallable, Category = "Arc|Inventory")
virtual bool SwapItemSlots(const FArcInventoryItemSlotReference& FromSlot, const FArcInventoryItemSlotReference& ToSlot);
//查
UFUNCTION(BlueprintCallable, Category = "Arc|Inventory")
virtual bool IsValidItemSlot(const FArcInventoryItemSlotReference& Slot);
UFUNCTION(BlueprintPure, Category = "Arc|Inventory")
virtual TArray<FArcInventoryItemSlotReference> GetAllSlotReferences();
virtual FArcInventoryItemSlot& GetItemSlot(const FArcInventoryItemSlotReference& RefSlot);
virtual UArcItemStack* GetItemInSlot(const FArcInventoryItemSlotReference& Reference);
UFUNCTION(BlueprintPure, Category = "Arc|Inventory")
virtual int32 GetInventorySize();
1.2.1、增
作用
将物品放入背包中,目前是两种方式:放置到固定位置+自动放置到任意符合的位置
bool LootItem(UArcItemStack* Item){
for (auto Slot : BagInventory.Slots){
FArcInventoryItemSlotReference SlotRef(Slot, this);
if (AcceptsItem(Item, SlotRef) && PlaceItemIntoSlot(Item, SlotRef)){
return true;}}
return false;}
bool PlaceItemIntoSlot(UArcItemStack* Item, const FArcInventoryItemSlotReference& ItemSlot){
//1.检查是否能塞进去
if (!AcceptsItem(Item, ItemSlot)){
return false;}
if (IsValid(Item) && Item->GetOwner() != GetOwner()){
UArcItemStack::TransferStackOwnership(Item, GetOwner());}
//2.赋值
FArcInventoryItemSlot& Slot = GetItemSlot(ItemSlot);
UArcItemStack* PreviousItem = Slot.ItemStack;
Slot.ItemStack = Item;
//3.广播和网络更新
BagInventory.MarkItemDirty(Slot);
OnInventoryUpdate.Broadcast(this);
OnItemSlotChange.Broadcast(this, ItemSlot, Item, PreviousItem);
GetOwner()->ForceNetUpdate();
return true;
}
实现:
加入库存的是ItemStack类型,加入方式简单直接。
ItemStack是ItemSlot的内部储存实现,可以被重载生成自己的插槽类型
1.2.2、删
作用
根据Reference删除特定Slot内的元素,或者删除所有元素
bool UArcInventoryComponent::RemoveItemFromInventory(const FArcInventoryItemSlotReference& ItemSlot)
{
//1.检查
if (!IsValid(ItemSlot)){
return false;}
FArcInventoryItemSlot& Item = GetItemSlot(ItemSlot);
UArcItemStack* PreviousItem = Item.ItemStack;
if (!IsValid(Item.ItemStack)){
return false;}
//2.删除
Item.ItemStack = nullptr;
//3.广播
BagInventory.MarkItemDirty(Item);
BagInventory.MarkArrayDirty();
OnInventoryUpdate.Broadcast(this);
OnItemSlotChange.Broadcast(this, ItemSlot, Item.ItemStack, PreviousItem);
GetOwner()->ForceNetUpdate();
return true;
}
bool UArcInventoryComponent::RemoveAllItemsFromInventory(TArray<UArcItemStack*>& OutItemsRemoved){
for (FArcInventoryItemSlot& ItemSlot : BagInventory.Slots){
if (!IsValid(ItemSlot.ItemStack)){
continue;}
OutItemsRemoved.Add(ItemSlot.ItemStack);
RemoveItemFromInventory(FArcInventoryItemSlotReference(ItemSlot, this));}
return true;}
实现:
利用Reference进行简单的查询与删除
1.2.3、改
作用:
主要是交换逻辑
bool UArcInventoryComponent::SwapItemSlots(const FArcInventoryItemSlotReference& SourceSlot, const FArcInventoryItemSlotReference& DestSlot){
//1、大量检查
if (!IsValid(SourceSlot) || !IsValid(DestSlot)){
return false;}
//1.1、最少有一个插槽在本库存
UArcInventoryComponent* SourceInventory = SourceSlot.ParentInventory;
UArcInventoryComponent* DestinationInventory = DestSlot.ParentInventory;
if (SourceInventory != this && DestinationInventory != this){
return false;}
//1.2、根据标签确定两个插槽可以Swap
UArcItemStack* SourceItem = SourceInventory->GetItemInSlot(SourceSlot);
UArcItemStack* DestItem = DestinationInventory->GetItemInSlot(DestSlot);
if (!DestinationInventory->AcceptsItem_AssumeEmptySlot(SourceItem, DestSlot) || !SourceInventory->AcceptsItem_AssumeEmptySlot(DestItem, SourceSlot)){
return false;}
//2、先删后增
SourceInventory->RemoveItemFromInventory(SourceSlot);
DestinationInventory->RemoveItemFromInventory(DestSlot);
SourceInventory->PlaceItemIntoSlot(DestItem, SourceSlot);
DestinationInventory->PlaceItemIntoSlot(SourceItem, DestSlot);
return true;
}
实现:
检查插槽,然后交换插槽
1.2.4、查
作用:
进行状态查询以及通过Reference找到对应插槽
共三个作用:
查询插槽是否存在
获取到特定插槽(物品),目前仅是单个插槽
获取到所有库存/引用
获取库存大小
FArcInventoryItemSlot& UArcInventoryComponent::GetItemSlot(const FArcInventoryItemSlotReference& RefSlot){
for (FArcInventoryItemSlot& SlotSlot : BagInventory.Slots){
if (RefSlot == SlotSlot){
BagInventory.MarkItemDirty(SlotSlot);
return SlotSlot;}}
return BagInventory.Slots[0];}
实现:
单个查询使用遍历+==,其他查询基本是直接输出变量
1.3、ArcInventoryComponent
总结
1.3.1 背包组件:
ArcInventoryComponent
实现了基础的增删改查,满足一个背包基础的功能搭建
1.3.2 ArcInventory的核心
ArcInventoryComponent
为成为了系统的中心与桥梁。其主要处理的问题:
1.3.2.1、数据同步
贯穿增删改查的是数据同步。
ArcInventoryComponent
采用了针对结构体优化的FFastArraySerializer
来储存背包数据。同时,采用更利于同步的FArcInventoryItemSlotReference
作为Handle进行外部通讯以及RPC的工具,减轻了同步负担的同时也增加了数据安全性
1.3.2.2、灵活度
将背包-插槽-物品-Handle全部拆分,带来了高度的灵活性。
背包:ArcInventoryComponent
作为库存操作的整体与内外系统交互
插槽:‘ArcItemSlot’:库存保存的插槽实例,并不保存实际数据,而是建立物品放入以及使用的规则
物品:’ArcItemStack‘真正保存了插槽的数据,与’ArcItemSlot’通过指针连接,记录所有真正的数据,包括最大堆栈数量、物品信息等
Handle:ArcItemReference
用于作为Handle供外部调用,是外部系统获取插槽的重要途经,通过使用轻量化的Handle传输而不是直接Slot或Stack传输可以大大减轻网络的系统压力,并使外部系统更难去介入Inventory内部实现
以上所有类都可以通过继承实现更丰富的功能
二、FArcInventoryItemSlot
继承自FFastArraySerializerItem
,优化网络传输
2.1、基础属性
主要是:物品/上一个物品、筛选/被筛选Tag、自身信息
UPROPERTY(BlueprintReadWrite, VisibleInstanceOnly, Category = Inventory)
UArcItemStack* ItemStack;
TWeakObjectPtr<UArcItemStack> OldItemStack;
UPROPERTY()
FArcInventoryItemSlotFilterHandle ItemSlotFilter;
UPROPERTY(BlueprintReadWrite, VisibleInstanceOnly, Category = Inventory)
FGameplayTagContainer SlotTags;
UPROPERTY(BlueprintReadOnly, VisibleInstanceOnly, Category = Inventory)
int32 SlotId;
UArcInventoryComponent* Owner;
2.1.1、ItemStack
与OldItemStack
存储物品的现有信息和上一个信息,详情请看第二篇
2.1.2、ItemSlotFilter
与SlotTags
作为筛选器以及被筛选器使用,双向筛选确定物品是否能放入当前Slot
2.1.3、Owner
与SlotId
记录自身信息,方便Component获取
2.2、接口
并没有外部需要调用的接口,仅仅用于数据检查以及网络复制
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
void PreReplicatedRemove(const struct FArcInventoryItemSlotArray& InArraySerializer);
void PostReplicatedAdd(const struct FArcInventoryItemSlotArray& InArraySerializer);
void PostReplicatedChange(const struct FArcInventoryItemSlotArray& InArraySerializer);
bool operator==(FArcInventoryItemSlot const& Other) const
2.3、FArcInventoryItemSlot
总结
需要判断确定什么东西能进该插槽,以及确定插槽类型,将数据交给了Stack储存,实现插槽数据与控制的解耦
三、总结
UArcInventoryComponent
、FArcInventoryItemSlot
以及FArcInventoryItemSlotReference
共同构成了库存的核心基础操作
3.1、UArcInventoryComponent
整个库存的中枢
专心处理背包插槽的逻辑:增删改查,所有的操作又基于背包与物品的中间层FArcInventoryItemSlot
,完全不需要知道物品的具体实现细节。
3.2、FArcInventoryItemSlot
库存本身与物品实体之间的桥梁,库存的最小单位
作为插槽的具体实现,依赖于UArcInventoryComponent
而不是实际物品,同时又与物品(‘ItemStack’)进行交互,获取自己想要的信息。
3.3、FArcInventoryItemSlotReference
库存与外界实体交互的桥梁
其可以看作是一个插槽的Handle,记录插槽的引用信息供内外部调用。方便使用、将内部实现与外部接口进行了隔离,方便数据传输以及RPC调用。