New Inventory System Plugin
通过 Item的SphereComponent + LineTrace结合起来 检测物体
原理:先进行SphereOverlap检测所有重叠的Item并存储到OverlappingItems,
然后再进行射线检测,因为射线检测可以穿墙,所以射线检测只检测OverlappingItems里面的物品,
如果检测到则将CurrentItem设置为检测到的值,否则设置为OverlappingItems[0]的值
创建InteractComponent,放置在PlayerController上:

创建USceneComponent,放置在Item上,USceneComponent可以设置SphereComponent:

创建HUD,用于设置中心圆点和PickupMessage:

在ProjectSetting中创建ItemTraceChannel和Item的Preset,用于射线检测:


创建Component:

配置InteractComponent:

将BPC_Inv_ItemComponent添加到Item中:

将Item的StaticMesh的CollisionPreset设置为Item:

将BPC_Inv_InteractComponent添加到PlayerController中:

添加PostProcessVolume并设置Material从而达到高亮效果:

Inv_InteractComponent.h:

Inv_InteractComponent.cpp:




Inv_ItemComponent.h:

Inv_ItemComponent.cpp:

Inv_HUD.h:

Inv_HUD.cpp:

创建SpatialInventory,InventoryGrid,GridSlot三个Widget并通过创建InventoryComponent实现开关库存操作
创建BaseInventory用作父类:

创建子类SpatialInventory并继承BaseInventory:

创建UUSerWidget类的InventoryGrid:

创建UUSerWidget类的GridSlot:

创建InventoryComponent用于切换库存:

InventoryComponent.h:
初始化SpatialInventory并设置ToggleInventory函数用于切换库存

InventoryComponent.cpp:


InventoryGrid.h:
在InventoryGrid中创建具体行数和列数的GridSlot:

InventoryGrid.cpp:

GridSlot.h:
在GridSlot中存储当前的Index:

InteractComponent.cpp:
在InteractComponent中创建ToggleInventory动作并存储InventoryComponent然后调用InventoryComponent中的ToggleInventory:

创建三个Widget:

设置SpatialInventory:

设置InventoryGrid:

设置GridSlot:

设置InventoryComponent:

将InventoryComponent添加到PlayerController上:

创建UObject的抽象类用于存储数据

创建Fast Array Serialize

根据FastArraySerializer的步骤来建立FastArray:

将InventoryItem作为存储数据放在Entry的结构体中
在FastArray结构体中声明Entries用于存储所有Entry,
添加AddEntry和RemoveEntry的函数
添加自带的PostReplicateAdd和PreReplicatedRemove函数,用于在复制新添加/新删除的元素前调用:

在PostReplicateAdd和PreReplicatedRemove函数中调用InventoryComponent的委托:

在InventoryComponent中创建InventoryItemChange和NoRoomInInventory的委托,并添加TryAddItem的函数:

创建TryAddItem函数并调用OnNoRoomInInventory的委托:

在InteractComponent中调用TryAddItem函数:

创建InfoMessage用于显示NoRoom的Message:


创建FadeAnimation:

在蓝图中实现ShowInfoMessage和HideInfoMessage的函数:

将InfoMessage添加到HUD中:

在HUD中初始化委托并设置InfoMessage:


创建SlotAvailability和GridAvailabilityResult来判断库存是否有空间
GridAvailabilityResult用来判断整个库存还能放下多少个该物品(TotalRoomToFill)、是否可堆叠、还有多少放不下(Remainder)并列出具体要放置的槽位SlotAvailabilities
SlotAvailability计算不同的格子要放多少个该物品,指出目标索引、是否已有物品、还能填多少(AmountToFill)
FInv_SlotAvailability 加起来的放置数量就等于GridAvailabilityResult的总的AmountToFill数量

在BaseInventory中创建判断库存中是否有空间:

在SpatialInventory中继承父类函数:

创建ItemManifest描述Item的属性
拾取物 (UInv_ItemComponent) 在蓝图中设置 FInv_ItemManifest
当要把物品放进玩家 UInv_InventoryComponent 时,会通过 FInv_FastArray::AddEntry 新建 UInv_InventoryItem,复制 Manifest 数据 并 注册成可复制子对象
在ItemComponent中创建ItemManifest并在蓝图中设置:

在InventoryItem中创建用来保存 FInv_ItemManifest 的实例,并添加设置该实例的函数,以及获取ItemManifest的函数:

在ItemManifest中创建CreateInventoryItem的函数,创建InventoryItem 并 把当前的ItemManifest数据复制进新对象:

在InventoryComponent中创建FastArray并添加AddRepSubObject函数和Server函数:

将子对象加入复制队列,确保客户端能收到该子对象

完善FastArray中的AddEntry(UInv_ItemComponent ItemComponent)函数,通过ItemComponent中的Manifest中的CreateInventoryItem函数来新建 UInv_InventoryItem,并在该函数中复制 Manifest 数据,并注册成可复制子对象:*

创建FGameplayTag类型的ItemType



在ItemManifest中创建FGameplayTag类型的变量并限制Categories只能为Items:

在ItemComponent中的Manifest中设置ItemType:

创建AddItem函数绑定委托,并让其在客户端和服务器都能被调用
由于FastArray的PostReplicatedAdd和PreReplicatedRemove只会在客户端调用并调用OnInventoryItemAdded委托的Broadcast,所以需要在服务器上单独Broadcast:

在AddEntry之后,只在服务器单独调用Broadcast:

在InventoryComponent开头初始化InventoryList的OwnerComponent为InventoryComponent:

在InventoryGrid中创建用于绑定的委托函数AddItem:


创建ItemFragment用于设置加入网格的物品的属性
添加BaseClass和ChildClass,由于Struct存在继承,所以在BaseClass中设置析构函数
创建FragmentTag用于确定当前的Fragment类型
GridFragment用于设置物品添加到网格的大小是1x1还是2x3
ImageFramgent用于设置物品添加到网格的图标:



在Manifest中创建ItemFragments的实例化数组并排除父类选项:

在Manifest中添加Fragments:

设置HasRoomForItem函数
在Grid中创建执行具体逻辑的HasRoomForItem函数,然后在SpatialInventory中创建具体的Grid并调用该Grid的HasRoomForItem函数:
创建3个重载函数,其中以Manifest为参数的函数作为执行具体逻辑的函数,其他两个函数直接调用Manifest函数:

在AddItem中调用HasRoomForItem函数来获取Result从而获取物品应该放在网格位置的具体信息:

在SpatialInventory中创建具体的Grid:

HasRoomForItem函数调用具体Grid的HasRoomForItem函数:

Add SlottedItem To Canvas
在Manifest中创建模板函数,用于通过Tag来获取具体的Fragment(例如GridFragment,ImageFragment):

创建SlottedItem Widget,用于将物品图标添加到Canvas:



在InventoryGrid中创建SlottedItemClass和用于存储的SlottedItems:

在AddItem函数中调用AddItemToIndices,将物品添加到需要添加的每个索引(堆叠物品分散到不同槽位,则会有多个Availability)
如果是多格物品,只会在左上角索引创建一个SlottedItem
在AddItemAtIndex函数中通过Manifest获取GridFragment和ImageFragment
通过Fragment来设置SlottedItem的位置和图标
调用AddSlottedItemToCanvas将SlottedItem添加到Canvas:

在InventoryGrid中的HasRoomForItem函数中暂时创建Index为0的SlotAvailability用于测试:

在蓝图中创建SlottedItem类的Widget:

在InventoryGrid中设置SlotteItem类:

效果:

添加GridSlot占用Texture
在GridSlot中创建背景图片,并创建GridSlotState的枚举类:

对于不同的State,设置不同的背景Brush:

在AddItemToIndices中创建UpdateGridSlotsState函数用于更新Slot的状态
若为多格物品,则设置每个格子索引的State为占用:

效果:

创建StackCount
创建StackableFragment,用于设置物品的 单格最大堆叠数 和 捡起一个物品的堆叠数:

创建用于StackableFragment的Tags:


在SlottedItem中创建Text用于显示堆叠数:

创建更新堆叠数的函数:

在创建SlottedItem的函数中调用UpdateStackCount函数:

在HasRoomForItem函数中暂时硬编码创建GridAvailabilityResult:

在蓝图中创建Stackable Fragment:

在蓝图中添加Text_StackCount:

效果:

Update GridSlot
由于一个物品可能会占用多个网格,
需要将被占用的网格标记为不可用
并且左上角格子记录堆叠数:
在GridSlot中添加StackCount、UpperLeftIndex、bAvailable:

在UpdateGridSlotsState中设置对应的Slot:

完善HasRoomForItem函数
完善HasRoomForItem的检查:

在HasRoomForItem中先判断是否存在可堆叠的同类物品,如果有,直接填充:

Result中的InventoryItem是用于判断当前库存中是否存在相同类型的物品才会设置这个变量:

在FastArray中添加查找第一个类型匹配的物品的函数:

通过FindByPredicate查找:

在TryAddItem函数中,如果在FastArray中找到相同类型的物品,则设置Result中的InventoryItem,并执行Server_AddStacksToItem,否则执行Server_AddNewItem:

完善AddStackToItem函数
在InventoryItem中创建TotalStackCount用于记录库存中当前Item的堆叠总数:


在ItemComponent中创建DestroyItem函数用于销毁Item:

在InventoryComponent中创建委托,用于当StackChange时,改变库存Widget:

在添加StacksToItem之前调用委托:

更新TotalStackCount,如果有Remainder,则更新剩余物品的StackableFragment,否则销毁Item:

绑定委托,遍历SlotAvailabilities,如果当前格子有物品,则直接更新SlottedItem的StackCount,否则添加新的SlottedItem:

将物品的Replicates设置为True:

设置拖拽、合并
在GridSlot中添加鼠标进入和离开事件,用于鼠标移动到对应格子时,格子会高亮:


在SlottedItem中添加鼠标按下,进入和离开事件,用于拖拽SlottedItem,创建委托并在InventoryGrid中设置委托:

鼠标按下时调用检测拖拽,在拖拽时创建用于视觉效果的SlottedItem:

InventoryGrid中创建用于委托的函数:

拖拽委托被触发时,将bIsDragging设置为true,用于在Tick中实现拖拽:
取消拖拽委托被触发时,将判断是否可以放置或交换:

交换物品:

移除Inventory中的SlottedItem:

在Tick中执行拖拽逻辑,先通过鼠标所在象限和物品尺寸计算物品左上角起始坐标,然后判断是否可以放置或交换:


判断是否可以放置或交换:


通过鼠标所在象限和物品尺寸计算物品左上角起始坐标:

计算鼠标在哪个格子并且计算在当前格子的左半边和上半边:

创建PopUpMenu和SplitMenu
创建PopUpMenu的Widget:

绑定Consume和Drop的函数:

创建Split的Widget:

绑定Split和Cancel的函数:

再SlottedItem中创建RightButtonDown的函数,用于判断是否在SlottedItem上按下右键:


在InventoryComponent中创建DropItem和ConsumeItem的服务器函数,并创建SpawnDroppedItem函数:

在服务器更新Consume和Drop后的库存变化:

在Manifest中声明PickupActorClass用于Drop到世界:

在ItemFragment中创建Consume的Fragment:


创建ConsumableFragment的Tag:


在SpatialInventory中创建CanvasPanel,创建关闭PopUpMenu和SplitMenu的函数:


在Grid中获取Consumable的Tag用于判断物品是否可以Consume,创建PopUpMenu和SplitMenu:


绑定SlottedItem的RightButtonDown,创建PopUpMenu并判断是否是Consumable

按键按下时,调用Server函数,并在本地更新网格占用:

放置物品时,先判断是否可以Split:

如果Shift按下,则可以Split:

Split和Cancel的按钮按下:

将ItemComponent设置为可复制的:

创建PopUpMenu:

创建SplitMenu:

设置Grid:

为可以Consume的物品添加具体的Consumable Fragment ———— Heal Consumable Fragment:

设置ItemComponent为可复制的:

添加ItemDescription
创建ItemDescription Wiget类:


在SlottedItem中创建鼠标进入和离开事件:

在鼠标事件中调用BaseInventory的函数:

在BaseInventory中创建虚函数:

在SpatialInvventory中实现虚函数:

在SpatialInventory中的Canvas中创建ItemDescription,并根据鼠标位置和Canvas的大小来设置Widget的位置,确保Widget不会超过边界:

当Hover时,延迟出现,当UnHover时,隐藏:

完善ItemDescription
复合模式:
1.CompositeBase(复合基类)
·定义所有组件的公共接口
·声明 DoWork() 等操作
2.Composite(复合类)
·包含子复合类Composite 或 叶子节点Leaf(Children)
·DoWork() 遍历子组件并调用它们的 DoWork()
3.Leaf(叶子节点)
·实现基础操作,不包含子组件
·DoWork() 执行实际工作

创建所有的复合Widget的类:



在CompositeBase中创建关闭和显示Widget的函数,并创建ApplyFunction函数:


在Composite中创建Children数组,并继承Base中所有的函数:

通过WidgetTree来设置Children,ApplyFunction和Collapse让所有的Children调用对应的函数:

在Leaf中创建对应的FragmentTag,以及ApplyFunction:

真正调用Function的位置:

将ItemDescription改为继承自Composite:

在创建ItemDescription后,调用AssimilateInventoryFragments函数:通过遍历所有专门为Similate设置的类型的Fragments,查找是否有Fragment与Composite上的Fragment匹配,如果有,则将Fragment的数据应用到对应的Composite上,从而在Description上显示对应的Leaf
创建Leaf_Image:

添加Image和SizeBox:


添加专门用于Assimilate的Fragment,并让ImageFragment继承自AssimilateFragment:

Assimilate函数:将Fragment的数据应用到对应的UI上,父函数判断是否与标签匹配并显示Widget:

在Manifest中创建将Fragment数据应用到对应的Composite的函数,以及获取所有指定类型的Fragments的函数:

获取所有指定类型的Fragments的函数:

遍历所有FInv_AssimilateFragment类型的Fragment,每个Fragment调用Assimilate,找到匹配的Leaf并应用数据:

在SpatialInventory创建ItemDescription时调用AssimilateInventoryFragments函数:

在蓝图中创建Leaf_Icon并继承自Leaf_Image:

设置Leaf_Icon的FragmentTag:

在ItemDescription中添加WBP_Leaf_Icon:

效果:

创建Leaf_Text并在蓝图中继承自Leaf_Text创建ItemName的Widget
创建Leaf_Text:


在NativePreConstruct中设置Text的字体大小:

创建新的TextFragment并继承自AssimilateFragment:

配置Assimilate函数:

添加ItemNameFragment的FragmentTag:


在蓝图中创建Leaf_Text的Widget类:

设置ItemName的FragmentTag:

添加到ItemDescription上:

在物品的Manifest中添加TextFragment,并将FragmentTag设置为ItemNameFragment:

效果:

创建Label + Number:
创建Leaf类的LabeledNumber:

创建Tag:


在Leaf中添加两个Text,一个用于显示Label,一个用于显示Value:


创建LabeledNumber的Fragment,并继承Assimilate和InitializeFragment函数,添加随机数让Value的值随机:

初始化Fragment函数中让Value随机化:

在Manifest中创建InventoryItem时,初始化所有的Fragments:

在蓝图中创建LabeledNumber:




添加Modifiers,使Consumable实现Description显示的Value的值 和 真正增加的Value的值一样:
Consumable调用所有的Modifier的函数,Modifier继承自LabeledNumberFragment,用来具体设置Label的值并重写OnConsume函数从而让两个Value相同:
添加Modifier父类继承自LabeledNumber并添加OnConsume虚函数,然后再添加子类并重写各自的OnConsume函数
修改ConsumableFragment的父类为AssimilateFragment,并添加ConsumeModifiers的数组,在函数中调用每个Modifier的函数:

在所有的Assimilate函数中添加判断,因为父类判断的只是父类自己的Fragment,子类还需要再次判断:

在InitializeFragment函数中随机化Value,在ConsumableFragment的函数中分别调用所有的Modifier函数:

在Description中添加3个Label并配置不同的标签:

为Item设置ConsumableFragment:

效果:


添加装备槽位
创建EquippedGridSlot并继承自GridSlot:

创建EquippedSlottedItem并继承自SlottedItem:

创建EquipmentComponent专门用于装备物品:

在SlottedItem中存储当前Item所处的InventoryGird或EquippedGridSlot:

在创建视觉反馈时设置:

在SpatialInventory中创建EquippedGridSlot,
创建绑定函数,将SlottedItem的拖拽触发统一放到SpatialInventory中来配置:

遍历所有创建的EquippedGridSlot并将其存储在EquippedGridSlots中,
绑定函数将分别调用所有槽位对应的绑定函数:

在InventoryGrid中创建绑定函数,用于在SpatialInventory中调用:

判断是否是EquippedSlottedItem,如果是则取消装备并清空EquippedGridSlot上的物品,然后将物品添加到InventoryGrid中:

创建拖拽事件:

获取SpatialInventory:

在Tick中通过拖拽的鼠标位置来判断是否需要高亮:

将物品拖拽到EquippedGridSlot上后,判断当前物品的标签是否匹配,并判断装备槽中是否已经存在物品:

创建EquippedSlottedItem到EquippedGridSlot:

创建EquipmentFragment和EquipModifier:


在InventoryComponent中创建OnItemEquipped和OnItemUnEquipped的委托:

在Multicast中调用委托:

在EquipmentComponent中绑定委托:

在EquipmentComponent中调用EquipmentFragment的函数:

在蓝图中创建:



将EquipmentComponent添加到PlayerController中:

在SpatialInventory中添加不同类型的EquippedGridSlot:

在物品中创建Equipment Fragment:

效果:

添加EquipActor:真正装备的物品
创建Actor类:

在EquipActor中添加用于辨识的ItemType:

启用复制:

在EquipmentComponent中Spawn和Destroy生成的EquipActor:

初始化Character时,由于可能BeginPlay时Character还没准备好,所以需要添加添加OnPossessedPawnChanged回调:

在Spawn函数中,调用EquipmentFragment中的生成函数,并设置EquipActor的ItemType用于辨识,最后添加到EquipActors数组中,
在Destroy函数中,通过数组的FindByPredicate函数查找标签匹配的EquipActor,并调用Destroy函数:

在OnItemEquipped和OnItemUnEquipped函数中分别调用:

在EquipmentFragment中配置AttachToPoint和EquipActorClass并创建生成EquipActor的函数:

通过AttachMesh来生成EquipActor,并绑定到AttachToPoint:

创建EquipActor蓝图类:


为EquipmentFragment配置AttachToPoint和EquipActorClass:

效果:

添加ProxyMesh和CharacterDisplay
创建ProxyMesh:

在EquipmentComponent中添加判断石否是ProxyMesh:

如果是ProxyMesh则不调用OnEquip函数,并且将EquipActor设置为不复制:

在ProxyMesh中添加EquipmentComponent和ProxyMesh:

通过不断的查找来初始化ProxyMesh和EquipmentComponent:

创建CharacterDisplay的Widget:

通过查找ProxyMesh来获取Mesh,当鼠标拖拽时,旋转角色:

在SpatialInventory中添加CharacterDisplay的Widget,并添加鼠标松开函数:

当鼠标在CharacterDisplay外面松开时,将CharacterDisplay的bIsDragging设置为false:

在蓝图中创建ProxyMesh,并将其拖到世界中:

在蓝图中创建CharacterDisplay:

创建Render Target:


配置大小:

创建Material并将Texture改为Render Target:

为Material设置背景透明:

将CharacterDisplay的Image设置为刚刚的材质:

在Spatial Widget中添加CharacterDisplay Widget:

在ProxyMesh中添加SpringArm和SceneCaptureComponent2D,并将Texture设置为Render Target,并隐藏Atmosphere:

将FiledOfView改为60:

将ProxyMesh拖到世界中即可,如果修改了ProxyMesh,则需要将世界中的ProxyMesh删除后重新添加到世界:
只有在打包后才会在多人游戏中正确运行 或者 取消Run Under One Process进行测试:

效果:

添加AttachComponent,用于附加到角色Mesh上
将衣服直接附加到角色Mesh上,让衣服可以跟随角色的Animation动起来:
在EquipmentFragment中添加ClothingMeshAsset用于存储用于附加的衣服网格,当bAttachToMesh为True时,显示ClothingMeshAsset,隐藏AttachToPoint和EquipActorClass:

在Component中添加Map用于存储Component:

如果AttachToMesh,则创建SkeletalMeshComponent并附加到Mesh,
否则使用EquipActor:


在EquipmentFragment中有两种选择:


Fix Bug: 拾取物品时,若还有剩余,物品剩余数量却没有减少
添加获取Manifest Mutable类型的函数,确保Manifest可以被修改:


Bug:在设置另一个ItemDescription时,没有提前清除上一个的ItemDescription
获取所有的子类:

重写SetVisibility函数,当设置Description的Visibility时,先将所有子类先关闭:

调换两个函数位置,先销毁,后显现:

Bug:
需要初始化剩余物品,否则提前返回后剩余物品为0:

Rotate Item
旋转物品:
1.当物品被捡起后,横着无法放下时,尝试旋转物品
2.当按下R键时,旋转物品
先在SlotAvailability中添加bIsRotated标志来判断是否被旋转了:

在HasRoomForItem中尝试旋转物品:

如果物品被旋转了,则更改SlottedItem的GridDimensions,物品的原先尺寸存储在GridFragment中的GridDimensions,
所以只需要判断这两个GridDimensions是否相同即可知道物品是否被旋转:

如果需要旋转,则只需要旋转Widget即可:

当R键按下时,调用InventoryGrid中的RotateDraggedItem函数,并添加判断,是否在拖拽时旋转:

背包网格
添加BackpackGrid:

在Spatial中初始化,并绑定事件用于同一管理:

在Spatial中创建绑定函数
拖拽检测函数分别判断是否是BaseGrid,BackpackGrid,并处理EquippedGridSlots:

拖拽取消函数,分别判断不同事件,并分为同一网格中移动,和不同网格中跨网格移动:

获取鼠标位置,判断鼠标在哪个网格内:

总的HasRoomForItem,优先填充BaseGrid,然后填充BackpackGrid:
添加Item时,通过总的判断结果来分别对BaseGrid和BackpackGrid进行填充:

改变Item的Statck,分配Grid:
从总的Result中分离出Backpack专有的Result:


在开始时只初始化BaseGrid:

添加判断,需要保证HoveredItem的InventoryGrid就是当前的Grid,否则不同网格之间的会导致问题:

不能将装备栏中的背包拖到它自己中,背包里还有物品就不能放置在网格中:

分割物品需要判断是否在不同网格之间分割:


处理不同网格之间的跨网格移动:

初始化BackpackGrid,先保存当前背包中所有的存储物品,之后清除所有格子和UI,重新创建格子并将旧物品按位置放回去:


更换背包时,高亮闪烁背包变大或变小的格子:



浙公网安备 33010602011771号