一、UArcInventoryComponent_Bag
最朴素的背包
1.1、属性
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = Inventory)
int32 BagSlotCount;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Inventory", meta = (AllowPrivateAccess = "true"))
FGameplayAttribute InventorySizeAttribute;
1.1.1、BagSlotCount
插槽数量
1.1.2、InventorySizeAttribute
插槽数量(由Attribute控制),当InventorySizeAttribute
存在时使用InventorySizeAttribute
二、接口
virtual void UpdateBagInventorySlots();
UFUNCTION(BlueprintCallable, Category = "Inventory | Item Queries", meta = (ScriptName = "ItemQuery_GetAllBagSlots"))
1.2、UpdateBagInventorySlots
作用:
根据插槽数量动态更新插槽
void UArcInventoryComponent_Bag::UpdateBagInventorySlots()
{
//1、获取到当前插槽数量
FGameplayTag BagTag = GetDefault<UArcInventoryDeveloperSettings>()->BagItemSlotTag;
TArray<FArcInventoryItemSlotReference> Slots;
Query_GetAllBagSlots(Slots);
int32 SlotCount = Slots.Num();
//2、计算应有数量与当前数量的差值
int32 BagSlotsToCreate = BagSlotCount - SlotCount;
//3、多退少补
if (BagSlotsToCreate > 0){
for (int32 i = 0; i < BagSlotsToCreate; i++){
CreateInventorySlot(BagTag.GetSingleTagContainer(), nullptr);}}
else if(BagSlotsToCreate < 0){
//remove the bottom slots
for (int i = 0; i < FMath::Abs(BagSlotsToCreate); i++){
int32 SlotToRemove = SlotCount - 1 - i;
RemoveInventorySlot(Slots[SlotToRemove]);}}
}
实现:
计算所需插槽数-当前插槽数的差值,多了的话从最后一个插槽开始删除,少了的话添加新的
1.3、总结
只有一个功能:
根据插槽数量添加/删除背包插槽
由于大部分功能父类已经实现,UArcInventoryComponent_Bag
仅仅需要确定背包插槽数量并调用父类的创建和删除就行
二、UArcInventoryComponent_Equippable
可以放置装备的背包组件
2.1、属性
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "Abilities", Replicated, meta = (AllowPrivateAccess = "true"))
TArray< FArcInventoryItemInfoEntry> EquippedItemAbilityInfos;
2.1.1、EquippedItemAbilityInfos
作用:
一个储存了装备信息的数组
USTRUCT(BlueprintType)
struct FArcInventoryItemInfoEntry
{
UPROPERTY(BlueprintReadOnly, Category = "Abilities")
FArcInventoryItemSlotReference ItemSlotRef;
UPROPERTY(BlueprintReadOnly, Category = "Abilities")
FArcEquippedItemInfo EquippedItemInfo;
};
实现:
记录ItemRef
以及其增加的GA
、GE
、AttributeSet
留待装备被移出自己的插槽时回退。
2.1.2、FArcEquippedItemInfo
储存GA
、GE
、AttributeSet
的集合
之后会以
AbilityInfo
描述GA
、GE
、AttributeSet
的集合
//储存GA、GE、AttributeSet
USTRUCT(BlueprintType)
struct ARCINVENTORY_API FArcEquippedItemInfo
{
public:
FArcEquippedItemInfo(){}
UPROPERTY()
TArray<FGameplayAbilitySpecHandle> AddedAbilities;
UPROPERTY()
TArray<UAttributeSet*> InstancedAttributeSets;
UPROPERTY()
TArray< FActiveGameplayEffectHandle> AddedGameplayEffects;
};
2.2、接口
UFUNCTION()
virtual void OnItemEquipped(class UArcInventoryComponent* Inventory, const FArcInventoryItemSlotReference& ItemSlotRef, UArcItemStack* ItemStack, UArcItemStack* PreviousItemStack);
2.2.1、当物品更换时:OnItemEquipped
作用:
装备放入/取出与交换的入口。所有ItemSlot
都绑定了该函数
或许叫OnEquipItemChanged更合适
void UArcInventoryComponent_Equippable::OnItemEquipped(class UArcInventoryComponent* Inventory, const FArcInventoryItemSlotReference& ItemSlotRef, UArcItemStack* ItemStack, UArcItemStack* PreviousItemStack)
{
if (IsValid(PreviousItemStack) && IsEquippedItemSlot(ItemSlotRef)){
MakeItemUnequipped_Internal(ItemSlotRef, PreviousItemStack);}
if (IsValid(ItemStack) && IsEquippedItemSlot(ItemSlotRef)){
MakeItemEquipped_Internal(ItemSlotRef, ItemStack);}
}
实现:
只要物品槽有变动就会调用,如果是装备栏则根据取出/放入操作执行对应的内部函数
2.2.2、放入装备:MakeItemEquipped_Internal
作用:
给放入装备插槽的物品
bool UArcInventoryComponent_Equippable::MakeItemEquipped_Internal(const FArcInventoryItemSlotReference& ItemSlot, UArcItemStack* ItemStack)
{
//1、获取Definition
TSubclassOf<UArcItemDefinition_Equipment> ItemDefinition(ItemStack->GetItemDefinition());
if (!IsValid(ItemDefinition)){
return false;}
//2、获取Entry
//2.1、已经有了则获取
FArcInventoryItemInfoEntry* Entry = EquippedItemAbilityInfos.FindByPredicate([ItemSlot](FArcInventoryItemInfoEntry& x) {
return x.ItemSlotRef.SlotId == ItemSlot.SlotId;});
//2.2、没有则创建
if (Entry == nullptr){
Entry = &EquippedItemAbilityInfos.Add_GetRef(FArcInventoryItemInfoEntry(ItemSlot));}
//3、应用Definition中的Ability和AttributeSet
bool bSuccess = ApplyAbilityInfo_Internal(ItemDefinition.GetDefaultObject()->EquippedItemAbilityInfo, (*Entry).EquippedItemInfo, ItemStack);
if (bSuccess){
//应用Attribute修改器
ApplyPerks(ItemStack, ItemSlot);
//广播修改
OnEquippedItem.Broadcast(this, ItemSlot, ItemStack);}
return bSuccess;
}
实现:
获取物品的AbilityInfo并应用,将Entry放入
2.2.2、取出装备:MakeItemUnequipped_Internal
bool UArcInventoryComponent_Equippable::MakeItemUnequipped_Internal(const FArcInventoryItemSlotReference& ItemSlot, UArcItemStack* ItemStack)
{
//1、取出ItemDefinition和Entry
TSubclassOf<UArcItemDefinition_Equipment> ItemDefinition(ItemStack->GetItemDefinition());
FArcInventoryItemInfoEntry* Entry = EquippedItemAbilityInfos.FindByPredicate([ItemSlot](FArcInventoryItemInfoEntry& x) {
return x.ItemSlotRef.SlotId == ItemSlot.SlotId;
});
//2、移除Perk以及清空AbilityInfo
RemovePerks(ItemStack, ItemSlot);
bool bSuccess = ClearAbilityInfo_Internal(ItemDefinition.GetDefaultObject()->EquippedItemAbilityInfo, (*Entry).EquippedItemInfo);
//3、广播
if (bSuccess){
OnUnEquippedItem.Broadcast(this, ItemSlot, ItemStack);}
return bSuccess;
}
2.3、总结
装备背包的实现非常简单。根据Tag查找可以装备的ItemSlot
,当ItemSlot
变换时将对应ItemStack
的AbilityInfo
(GA
、GE
、AttributeSet
的集合)进行应用或移除。
可以看到,无论变换的是什么背包,ItemSlot
的类型始终是不变的,
三、UArcInventoryComponent_Active
激活物背包,只有一个物品能被激活
用于放置武器等互斥物品以及对应的切换操作
目前较为复杂的部分,也是深入理解ArcInventoryComponent的重要部分
3.1、属性
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, ReplicatedUsing = OnRep_ActiveItemSlot, Category = "Inventory")
int32 ActiveItemSlot;
UPROPERTY(VisibleInstanceOnly, BlueprintReadWrite, ReplicatedUsing = OnRep_PendingItemSlot, Category = "Inventory")
int32 PendingItemSlot;
TArray<FArcInventoryItemSlotReference> CachedActiveItemSlots;
3.1.1、ActiveItemSlot
正在激活中的SlotID
3.1.2、PendingItemSlot
准备激活的SlotID
3.1.3、CachedActiveItemSlots
所有的可激活物品引用
3.2、接口
void UArcInventoryComponent_Active::UpdateActiveItemSlots(UArcInventoryComponent* InventoryComp);
3.2.1、UpdateActiveItemSlots
作用:
将满足ActiveSlotTag
的ItemSlot
放入CachedActiveItemSlots
中
void UArcInventoryComponent_Active::UpdateActiveItemSlots(UArcInventoryComponent* InventoryComp)
{
FGameplayTag ActiveSlotTag = GetDefault<UArcInventoryDeveloperSettings>()->ActiveItemSlotTag;
CachedActiveItemSlots.Empty(CachedActiveItemSlots.Num());
Query_GetAllSlots(FArcInventoryQuery::QuerySlotMatchingTag(ActiveSlotTag), CachedActiveItemSlots);
}
实现:
调用父类的Query_GetAllSlots
将查到的ItemSlot
塞进CachedActiveItemSlots
中
3.2.2、BeginPlay
作用:
激活默认ActiveItem
void UArcInventoryComponent_Active::BeginPlay()
{
//激活默认物品
GetWorld()->GetTimerManager().SetTimerForNextTick([this]()
{
//检查第一个插槽是否是被Active,是的话激活(客户端方法)
if (PendingItemSlot != INDEX_NONE){
this->SwitchToPendingItemSlot();}
//有时在客户端ActiveItemSlot已经有值但PendingItemSlot还没有被同步过来,则直接执行MakeItemActive
else if (ActiveItemSlot != INDEX_NONE && GetOwnerRole() != ROLE_Authority){
this->MakeItemActive(ActiveItemSlot);}});
Super::BeginPlay();
}
3.2.3、SwitchToPendingItemSlot
作用:
切换激活物品为PendingItemSlot
void UArcInventoryComponent_Active::SwitchToPendingItemSlot()
{
//1、尝试让ASC自行处理
bool bActivatedAbility = false;
if (UAbilitySystemComponent* ASC = UAbilitySystemGlobals::Get().GetAbilitySystemComponentFromActor(GetOwner())){
FGameplayTag SwitchWeaponsAbilityTag = FGameplayTag::RequestGameplayTag(TEXT("Ability.ItemSwitch.Pending"), false);
if (SwitchWeaponsAbilityTag.IsValid()){
bActivatedAbility = ASC->TryActivateAbilitiesByTag(FGameplayTagContainer(SwitchWeaponsAbilityTag));}}
//2、无法自行处理:先注销源ItemSlot,再激活PendingItemSlot
if (!bActivatedAbility)//Otherwise just quickly switch the item{
MakeItemInactive();
MakeItemActive(PendingItemSlot);}
//3、重置PendingItemSlot
PendingItemSlot = INDEX_NONE;
}
方法:
尝试让ASC自行处理,如果无法处理则先注销再激活
3.2.4、激活物品
作用:
根据插槽ID激活物品对应的AbilityInfo
MakeItemActive
void UArcInventoryComponent_Active::MakeItemActive(int32 NewActiveItemSlot)
{
//1、根据SlotID获取SlotRef
FArcInventoryItemSlotReference ItemSlotRef = CachedActiveItemSlots[NewActiveItemSlot];
//2、执行内部激活函数
if (!MakeItemActive_Internal(ItemSlotRef)){
if (IsValid(GetItemInSlot(ItemSlotRef))){
//激活失败但是Slot确实存在,则重置NewActiveItemSlot
NewActiveItemSlot = INDEX_NONE;}}
//3、设置ActiveItemSlot
ActiveItemSlot = NewActiveItemSlot;
}
MakeItemActive_Internal(const FArcInventoryItemSlotReference& ItemSlot)
bool UArcInventoryComponent_Active::MakeItemActive_Internal(const FArcInventoryItemSlotReference& ItemSlot)
{
UArcItemStack* ItemStack = GetItemInSlot(ItemSlot);
return MakeItemActive_Internal(ItemSlot, ItemStack);
}
MakeItemActive_Internal(const FArcInventoryItemSlotReference& ItemSlot, UArcItemStack* ItemStack)
bool UArcInventoryComponent_Active::MakeItemActive_Internal(const FArcInventoryItemSlotReference& ItemSlot, UArcItemStack* ItemStack)
{
//1、获取Definition
TSubclassOf<UArcItemDefinition_Active> ItemDefinition(ItemStack->GetItemDefinition());
//2、获取Entry,有的话直接拿,没有的话创建一个
FArcInventoryItemInfoEntry* Entry = ActiveItemAbilityInfos.FindByPredicate([ItemSlot](const FArcInventoryItemInfoEntry& x) {
return x.ItemSlotRef.SlotId == ItemSlot.SlotId;});
if (Entry == nullptr){
Entry = &ActiveItemAbilityInfos.Add_GetRef(FArcInventoryItemInfoEntry(ItemSlot));}
//3、应用能力与Perk,调用Equipment的ApplyAbilityInfo_Internal
bool bSuccess = ApplyAbilityInfo_Internal(ItemDefinition.GetDefaultObject()->ActiveItemAbilityInfo, (*Entry).EquippedItemInfo, ItemStack);
if (bSuccess){
ApplyPerks(ItemStack, ItemSlot);}
//4、广播激活
OnItemActive.Broadcast(this, ItemStack);
return bSuccess;
}
实现:
根据ID查找到Stack内部的ItemInfo并应用
3.2.5、注销物品
作用:
将目前正在激活的物品注销
MakeItemInactive
void UArcInventoryComponent_Active::MakeItemInactive()
{
//1、执行内部注销函数
MakeItemInactive_Internal(GetActiveItemSlot());
//2、没有新的武器切换,则重置ActiveItemSlot
if (/*GetOwnerRole() == ROLE_Authority &&*/ !bSwitchingWeapons){
ActiveItemSlot = INDEX_NONE;}
}
MakeItemInactive_Internal(1参数)
bool UArcInventoryComponent_Active::MakeItemInactive_Internal(const FArcInventoryItemSlotReference& ItemSlot)
{
UArcItemStack* ItemStack = GetItemInSlot(ItemSlot);
return MakeItemInactive_Internal(ItemSlot, ItemStack);
}
MakeItemInactive_Internal(2参数)
bool UArcInventoryComponent_Active::MakeItemInactive_Internal(const FArcInventoryItemSlotReference& ItemSlot, UArcItemStack* ItemStack)
{
//1、处理当服务器已经移除了当前ItemSlot但客户端还没有移除时,仅仅调用Inactive的广播
if (!IsValid(ItemStack)){
if (IsValid(ItemSlot)){
OnItemInactive.Broadcast(this, ItemStack);}
return false;}
//2、获取ItemDefinition与缓存在ActiveItemAbilityInfos的Entry
TSubclassOf<UArcItemDefinition_Active> ItemDefinition(ItemStack->GetItemDefinition());
FArcInventoryItemInfoEntry* Entry = ActiveItemAbilityInfos.FindByPredicate([ItemSlot](const FArcInventoryItemInfoEntry& x) {
return x.ItemSlotRef.SlotId == ItemSlot.SlotId;});
//3、移除Perk和所有装备信息
RemovePerks(ItemStack, ItemSlot);
bool bSuccess = ClearAbilityInfo_Internal(ItemDefinition.GetDefaultObject()->ActiveItemAbilityInfo, (*Entry).EquippedItemInfo);
//4、广播已经被移除
OnItemInactive.Broadcast(this, ItemStack);
return bSuccess;
}
实现:
直接找到当前激活Item并移除AbilityInfo
3.2.6、回调重写OnItemEquipped
作用:
当物品被装备/卸下时回调
void UArcInventoryComponent_Active::OnItemEquipped(class UArcInventoryComponent* Inventory, const FArcInventoryItemSlotReference& ItemSlotRef, UArcItemStack* ItemStack, UArcItemStack* PreviousItemStack)
{
Super::OnItemEquipped(Inventory, ItemSlotRef, ItemStack, PreviousItemStack);
//1、如果是放入,当是第一个ActiveItem时,激活该物品
if (ActiveItemSlot == INDEX_NONE && IsActiveItemSlot(ItemSlotRef) && IsValid(ItemStack)){
int32 ItemSlotIndex = GetActiveItemIndexBySlotRef(ItemSlotRef);
PendingItemSlot = ItemSlotIndex;
if (HasBegunPlay()){
SwitchToPendingItemSlot();}}
//2、当移除激活的物品,激活下一个可以激活的物品
if (!IsValid(ItemStack)){
int32 ItemSlotIndex = GetActiveItemIndexBySlotRef(ItemSlotRef);
if (ItemSlotIndex == ActiveItemSlot){
PendingItemSlot = GetNextValidActiveItemSlot();
MakeItemInactive_Internal(ItemSlotRef, PreviousItemStack);
SwitchToPendingItemSlot();}}}
实现:
当放入的是一个可激活物时,自动装备
当移除的是当前激活物时,自动装备下一件物品
3.2.7、交换
作用:
交换激活的物品
void UArcInventoryComponent_Active::SwapActiveItems(int32 NewItemSlot){
bSwitchingWeapons = true;
MakeItemInactive();
MakeItemActive(NewItemSlot);
bSwitchingWeapons = false;
}
实现:
移除原有激活物,激活新激活物
3.2.8、获取
函数 | 作用 |
---|---|
GetActiveItemStack | 获取当前激活的ItemStack |
GetNextActiveItemSlot | 获取下一个ActiveItemSlot |
GetPreviousActiveItemSlot | 获取上一个ActiveItemSlot |
GetNextValidActiveItemSlot | 获取下一个存在ItemStack 的ActiveItemSlot |
GetPreviousValidActiveItemSlot | 获取上一个存在ItemStack 的ActiveItemSlot |
3.3、总结
提供了基础的物品激活功能
对于Equip进行限制的产物,仅允许一种激活物品存在
激活物最麻烦的是同步与联机问题
四、总结
UArcInventoryComponent_Bag
:根据数量动态设置普通ItemSlot
UArcInventoryComponent_Equipable
:接入GAS,能将装备放入ItemSlot
中并应用AbilityInfo
UArcInventoryComponent_Active
:仅允许一个AbilityInfo
被激活
基本完成了ArcInventory的拼图